Skip to content

fix(edit): show best matching region on mismatch instead of raw file head#56857

Closed
imwyvern wants to merge 1 commit into
openclaw:mainfrom
imwyvern:fix/edit-fuzzy-match-hint
Closed

fix(edit): show best matching region on mismatch instead of raw file head#56857
imwyvern wants to merge 1 commit into
openclaw:mainfrom
imwyvern:fix/edit-fuzzy-match-hint

Conversation

@imwyvern

Copy link
Copy Markdown
Contributor

Problem

When the edit tool's oldText doesn't match, the error currently shows the first 800 characters of the file. For large files where the intended edit target is deep in the content, this is unhelpful — the model has to re-read the file and guess where the mismatch occurred.

Solution

Replace the raw file-head fallback with a best-match region finder that uses line-level sliding window comparison:

  • Multi-line oldText: slides a window of the same line count across the file, scoring each position by line-level overlap (exact match = 1.0, whitespace-only diff = 0.5)
  • Single-line oldText: falls back to substring search across all lines
  • Below 20% similarity: preserves the original file-head behavior as fallback

Example error output (before)

Could not find the exact text in src/foo.ts.
Current file contents:
import { ... } from '...';
... (truncated after 800 chars)

Example error output (after)

Could not find the exact text in src/foo.ts.
Best matching region (66% similar) near line 42:
41| function hello() {
42|   console.log("hello");
43|   console.log("world");
44| }
Hint: check for whitespace differences, extra/missing lines, or outdated content in your oldText.

Changes

  • src/agents/pi-tools.host-edit.ts: Added findBestMatchRegion() and updated appendMismatchHint() to pass oldText and use region matching
  • src/agents/pi-tools.host-edit.fuzzy-hint.test.ts: 5 new tests covering partial match, fallback, single-line match, line numbers, and missing oldText

Testing

All 5 new tests pass. Existing edit tool tests unaffected — this only enhances error messages on mismatch.

…head

When the edit tool's oldText doesn't match, the error now includes the
most similar region in the file with line numbers and a similarity score,
helping the model self-correct more efficiently.

The fuzzy hint uses line-level sliding window comparison:
- Multi-line oldText: finds the window with highest line overlap
- Single-line oldText: falls back to substring search across all lines
- Below 20% similarity: preserves the original file-head behavior
@openclaw-barnacle openclaw-barnacle Bot added agents Agent runtime and tooling size: M r: too-many-prs Auto-close: author has more than twenty active PRs. labels Mar 29, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 41764af9d4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +136 to +138
contentLine.trim() === oldLine.trim() ||
contentLine.includes(oldLine.trim()) ||
oldLine.includes(contentLine.trim())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Skip empty lines when computing partial-line similarity

The partial-match branch treats blank oldText lines as a match for every content line because oldLine.trim() can be "" and contentLine.includes("") is always true. With trailing newlines or intentional blank lines in oldText, each window gets artificial score credit, so unrelated regions can exceed the 20% threshold and suppress the Current file contents fallback. This makes mismatch hints point to arbitrary locations instead of useful context.

Useful? React with 👍 / 👎.

Comment on lines +176 to +179
const snippet = contentLines
.slice(contextStart, contextEnd)
.map((l, idx) => `${contextStart + idx + 1}| ${l}`)
.join("\n");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Cap best-match snippet length to prevent oversized errors

The new best-match path no longer enforces EDIT_MISMATCH_HINT_LIMIT, and the snippet size scales with oldText line count (windowSize). If oldText is large (for example, a big multi-line replacement), the thrown error can include hundreds or thousands of lines, which can bloat tool responses and exceed practical model/context limits. The fallback path still caps output to 800 chars, so this is a regression in mismatch-error size control.

Useful? React with 👍 / 👎.

@greptile-apps

greptile-apps Bot commented Mar 29, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR replaces the raw-file-head fallback in edit-tool mismatch errors with a findBestMatchRegion() helper that slides a window across the file and scores line-level similarity, giving the model a labelled snippet near the likely edit site instead of unrelated file content.

Key changes:

  • findBestMatchRegion() added to pi-tools.host-edit.ts: multi-line sliding window + single-line substring fallback, 20% similarity threshold before falling back to the old behaviour.
  • appendMismatchHint() updated to accept oldText and call the new helper.
  • 5 new tests in pi-tools.host-edit.fuzzy-hint.test.ts covering partial match, fallback, single-line, line-number format, and missing oldText.

One behavioural issue was found: for single-line oldText the substring scan runs unconditionally after the window loop and returns on the first hit (with a hardcoded score of 0.9), silently discarding any higher-ranked window position that the loop already computed. In files where the trimmed search token appears as a substring early on a less-related line, the model will be shown the wrong region at an inflated similarity percentage. See the inline comment for a minimal fix.

Confidence Score: 4/5

Safe to merge after the single-line path ordering is corrected; the change is error-message-only and cannot corrupt file content.

The multi-line sliding window and snippet generation are correct. The one P2 issue — the single-line substring path always overrides the sliding window result and reports a misleading 90% score — can cause the model to be pointed at the wrong file region in certain common patterns. It does not block functionality but undermines the primary goal of showing the best matching region.

src/agents/pi-tools.host-edit.ts — lines 151–166 (single-line substring early-return logic)

Important Files Changed

Filename Overview
src/agents/pi-tools.host-edit.ts Adds findBestMatchRegion() using a sliding window + single-line substring fallback. The multi-line sliding window and the context snippet/line-numbering are correct. One issue: for single-line oldText the substring path always runs first (before the bestScore threshold), ignoring a potentially better sliding window result and reporting a hardcoded 90% score regardless of actual similarity.
src/agents/pi-tools.host-edit.fuzzy-hint.test.ts Five new targeted tests covering the happy path (partial match), no-match fallback, single-line substring match, line-number formatting, and missing oldText. Tests are well-structured and exercise the new code paths cleanly.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/agents/pi-tools.host-edit.ts
Line: 151-166

Comment:
**Single-line path ignores sliding window result and reports misleading score**

The single-line substring block runs unconditionally after the sliding window loop and returns early — completely discarding `bestScore`/`bestStart` — even when the window found a more similar position. Consider this scenario:

- `oldText = "baz"`
- File line 1: `"foo bar baz qux"` (contains "baz" as substring — poor match)
- File line 5: `"  baz"` (trimmed exact match, sliding window score 0.5)

The substring scan hits line 1 first and returns `lineStart: 1, score: 0.9`, while the sliding window correctly ranked line 5 higher. The model is shown the wrong region with an inflated confidence percentage.

Additionally, the hardcoded `score: 0.9` bears no relationship to actual line similarity; a file that contains `"bazaar"` would be reported as "90% similar" to `oldText = "baz"` even though the line content is unrelated.

A minimal fix is to only enter the substring path when the sliding window produced a weak or no result:

```typescript
if (windowSize === 1 && bestScore < 0.5) {
  const trimmedOld = oldLines[0].trim();
  if (trimmedOld.length > 0) {
    for (let i = 0; i < contentLines.length; i++) {
      if (contentLines[i].includes(trimmedOld)) {
        return {
          lineStart: i + 1,
          snippet: contentLines
            .slice(Math.max(0, i - 1), i + 2)
            .map((l, idx) => `${Math.max(1, i) + idx}| ${l}`)
            .join("\n"),
          score: 0.9,
        };
      }
    }
  }
}
```

This lets an exact-trimmed window match (score 0.5) take priority over a mere substring hit anywhere in the file.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix(edit): show best matching region on ..." | Re-trigger Greptile

Comment on lines +151 to +166
if (windowSize === 1) {
const trimmedOld = oldLines[0].trim();
if (trimmedOld.length > 0) {
for (let i = 0; i < contentLines.length; i++) {
if (contentLines[i].includes(trimmedOld)) {
return {
lineStart: i + 1,
snippet: contentLines
.slice(Math.max(0, i - 1), i + 2)
.map((l, idx) => `${Math.max(1, i) + idx}| ${l}`)
.join("\n"),
score: 0.9,
};
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Single-line path ignores sliding window result and reports misleading score

The single-line substring block runs unconditionally after the sliding window loop and returns early — completely discarding bestScore/bestStart — even when the window found a more similar position. Consider this scenario:

  • oldText = "baz"
  • File line 1: "foo bar baz qux" (contains "baz" as substring — poor match)
  • File line 5: " baz" (trimmed exact match, sliding window score 0.5)

The substring scan hits line 1 first and returns lineStart: 1, score: 0.9, while the sliding window correctly ranked line 5 higher. The model is shown the wrong region with an inflated confidence percentage.

Additionally, the hardcoded score: 0.9 bears no relationship to actual line similarity; a file that contains "bazaar" would be reported as "90% similar" to oldText = "baz" even though the line content is unrelated.

A minimal fix is to only enter the substring path when the sliding window produced a weak or no result:

if (windowSize === 1 && bestScore < 0.5) {
  const trimmedOld = oldLines[0].trim();
  if (trimmedOld.length > 0) {
    for (let i = 0; i < contentLines.length; i++) {
      if (contentLines[i].includes(trimmedOld)) {
        return {
          lineStart: i + 1,
          snippet: contentLines
            .slice(Math.max(0, i - 1), i + 2)
            .map((l, idx) => `${Math.max(1, i) + idx}| ${l}`)
            .join("\n"),
          score: 0.9,
        };
      }
    }
  }
}

This lets an exact-trimmed window match (score 0.5) take priority over a mere substring hit anywhere in the file.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/pi-tools.host-edit.ts
Line: 151-166

Comment:
**Single-line path ignores sliding window result and reports misleading score**

The single-line substring block runs unconditionally after the sliding window loop and returns early — completely discarding `bestScore`/`bestStart` — even when the window found a more similar position. Consider this scenario:

- `oldText = "baz"`
- File line 1: `"foo bar baz qux"` (contains "baz" as substring — poor match)
- File line 5: `"  baz"` (trimmed exact match, sliding window score 0.5)

The substring scan hits line 1 first and returns `lineStart: 1, score: 0.9`, while the sliding window correctly ranked line 5 higher. The model is shown the wrong region with an inflated confidence percentage.

Additionally, the hardcoded `score: 0.9` bears no relationship to actual line similarity; a file that contains `"bazaar"` would be reported as "90% similar" to `oldText = "baz"` even though the line content is unrelated.

A minimal fix is to only enter the substring path when the sliding window produced a weak or no result:

```typescript
if (windowSize === 1 && bestScore < 0.5) {
  const trimmedOld = oldLines[0].trim();
  if (trimmedOld.length > 0) {
    for (let i = 0; i < contentLines.length; i++) {
      if (contentLines[i].includes(trimmedOld)) {
        return {
          lineStart: i + 1,
          snippet: contentLines
            .slice(Math.max(0, i - 1), i + 2)
            .map((l, idx) => `${Math.max(1, i) + idx}| ${l}`)
            .join("\n"),
          score: 0.9,
        };
      }
    }
  }
}
```

This lets an exact-trimmed window match (score 0.5) take priority over a mere substring hit anywhere in the file.

How can I resolve this? If you propose a fix, please make it concise.

@openclaw-barnacle

Copy link
Copy Markdown

Closing this PR because the author has more than 10 active PRs in this repo. Please reduce the active PR queue and reopen or resubmit once it is back under the limit. You can close your own PRs to get back under the limit.

imwyvern pushed a commit to imwyvern/openclaw that referenced this pull request May 24, 2026
…head

When edit oldText doesn't match exactly, the error hint now locates and
displays the most similar region in the file (with line numbers) instead
of dumping the first 800 characters. This helps the model quickly spot
which lines diverged from its expectation.

Falls back to the current full-file-head behavior when no region scores
above zero shared lines.

Combines the intent of openclaw#56857 and openclaw#42265.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling r: too-many-prs Auto-close: author has more than twenty active PRs. size: M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant