Skip to content

fix: scope fuzzy-edit normalization to matched region only (#89994)#96814

Closed
jiahjian wants to merge 1 commit into
openclaw:mainfrom
jiahjian:fix/p0-issue-89994-fuzzy-edit-silent-rewrite
Closed

fix: scope fuzzy-edit normalization to matched region only (#89994)#96814
jiahjian wants to merge 1 commit into
openclaw:mainfrom
jiahjian:fix/p0-issue-89994-fuzzy-edit-silent-rewrite

Conversation

@jiahjian

@jiahjian jiahjian commented Jun 25, 2026

Copy link
Copy Markdown

What Problem This Solves

Fixes P0 issue #89994: when any edit in the edit tool falls back to fuzzy matching, normalizeForFuzzyMatch was applied to the entire file instead of just the matched region. This silently mutates unrelated lines — trailing whitespace stripped, smart quotes/dashes/spaces converted to ASCII, NFKC recomposed — and the diff shown to the model is computed against the already-normalized base, making the corruption invisible to both the model and the user.

Root cause (in applyEditsToNormalizedContent):

// Line 218-219: when ANY edit needs fuzzy match, normalize EVERYTHING
const baseContent = initialMatches.some((match) => match.usedFuzzyMatch)
  ? normalizeForFuzzyMatch(normalizedContent)   // BUG
  : normalizedContent;

Then newContent is spliced from this already-normalized base, writing the corrupted content to disk.

Fix: Use normalizeForFuzzyMatch only to locate match positions. Add findOriginalPosition (prefix-based binary search with >= boundary) and findOriginalSpan to map fuzzy match positions back to the original content. baseContent is now always the original LF-normalized content, so untouched lines are preserved byte-for-byte.

Evidence

Real terminal output — proof script (npx tsx scripts/proof-issue-89994.ts)

============================================================
TEST 1: Fuzzy edit preserves em dash & trailing whitespace
============================================================

Original file (JSON-escaped):
"const label = \"a — b\";\nlet x = 1;   \n\nfunction bar() {\n  return 2;\n}\n"

File after edit (JSON-escaped):
"const label = \"a — b\";\nlet x = 1;   \n\nfunction bar() {\n  return 3;\n}\n"

Diff shown to model:
 1 const label = "a — b";
 2 let x = 1;   
 3 
 4 function bar() {
-5   return 2;
+5   return 3;
 6 }

Checks:
  em dash preserved:       PASS
  trailing spaces preserved: PASS
  edit applied:            PASS
  old text removed:        PASS
  baseContent is original:  PASS

  => ALL CHECKS PASSED

============================================================
TEST 2: Fuzzy edit preserves smart quotes
============================================================

Original (JSON): "const msg = \"“help”\";\n\nfunction foo() {\n  return 1;\n}\n"
After edit (JSON): "const msg = \"“help”\";\n\nfunction foo() {\n  return 2;\n}\n"

  smart quotes preserved: PASS

============================================================
TEST 3: Fuzzy edit preserves NFKC ligature
============================================================

Original (JSON): "const ligature = \"ffi\";\n\nfunction foo() {\n  return 1;\n}\n"
After edit (JSON): "const ligature = \"ffi\";\n\nfunction foo() {\n  return 2;\n}\n"
  ligature preserved:     PASS
  NOT decomposed to ffi:  PASS

============================================================
SUMMARY: All fuzzy edits preserve unrelated lines byte-for-byte.
Fix verified on 2026-06-25T15:21:49.972Z
============================================================

Key observations

  • Em dash () preserved — not converted to ASCII hyphen
  • Trailing whitespace preservedlet x = 1; keeps its 3 trailing spaces
  • Smart quotes (/) preserved — not converted to straight quotes
  • NFKC ligature () preserved — not decomposed to ffi
  • Diff is honest — only the intended change (return 2;return 3;) appears as +/-, unrelated lines appear only as context

Unit test results — 7/7 passing

 ✓ |agents-core| edit-diff.test.ts (7 tests)
   ✓ preserves em dash and trailing whitespace on unrelated lines during fuzzy edit
   ✓ preserves smart quotes on unrelated lines during fuzzy edit
   ✓ preserves NFKC-relevant Unicode on unrelated lines during fuzzy edit
   ✓ preserves exact-match lines when another edit in the same batch uses fuzzy
   ✓ produces a diff that reflects only the intended change, not normalization
   ✓ exact match preserves all content unchanged except the edited region
   ✓ still throws when oldText is not found even with fuzzy matching

 Test Files  1 passed (1)
      Tests  7 passed (7)

🤖 Generated with Claude Code

@openclaw-barnacle openclaw-barnacle Bot added agents Agent runtime and tooling size: M triage: needs-pr-context Candidate: external PR body lacks required problem context or evidence. labels Jun 25, 2026
@clawsweeper

clawsweeper Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Thanks for the context here. I swept through the related work, and this is now duplicate or superseded.

Close: this PR targets the real fuzzy-edit corruption bug, but it is superseded by #96636, which is the safer open landing candidate with sufficient proof, a merge ref, and green current checks; this branch still has a normalized-offset mapper bug.

Root-cause cluster
Relationship: superseded
Canonical: #96636
Summary: This PR is a same-root candidate for the fuzzy-edit whole-file normalization bug, but #96636 is the safer open landing branch with sufficient proof, a merge ref, green current checks, and a more complete mapper.

Members:

Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything.

Canonical path: Continue the fix on #96636 or an equivalent canonical branch with segment-aware normalized-offset mapping, and close this duplicate branch.

So I’m closing this here and keeping the remaining discussion on #96636.

Review details

Best possible solution:

Continue the fix on #96636 or an equivalent canonical branch with segment-aware normalized-offset mapping, and close this duplicate branch.

Do we have a high-confidence way to reproduce the issue?

Yes. The linked issue has a high-confidence source reproduction on current main, and a no-write probe of this PR's mapper shows a concrete zero-length splice for a fuzzy match inside .

Is this the best way to solve the issue?

No. Keeping the original write/diff base is the right direction, but this branch's prefix binary-search mapper is not the best fix; the canonical branch should use segment-aware normalized-offset mapping.

Security review:

Security review cleared: The diff changes TypeScript edit-helper logic, colocated Vitest coverage, and a local proof script only; it adds no dependencies, workflows, package metadata, secrets handling, or downloaded code.

AGENTS.md: found and applied where relevant.

What I checked:

  • Current main still has the reported bug path: Current main still switches baseContent to whole-file normalizeForFuzzyMatch(normalizedContent) whenever any initial edit used fuzzy matching, so the underlying issue is not implemented on main. (src/agents/sessions/tools/edit-diff.ts:218, 31a0f97dd944)
  • Runtime caller writes and diffs helper output: The edit tool writes newContent returned by applyEditsToNormalizedContent and generates the model/user diff from baseContent versus newContent, so this helper controls both disk bytes and diff honesty. (src/agents/sessions/tools/edit.ts:412, 31a0f97dd944)
  • This PR's mapper can produce zero-length fuzzy spans: At PR head, findOriginalPosition uses the same prefix-length >= boundary for both span start and end, which maps a fuzzy match inside an NFKC-expanded character to the end of that original character for both boundaries. (src/agents/sessions/tools/edit-diff.ts:85, aa43d9b01f3f)
  • No-write probe of PR mapper defect: A direct probe of the PR mapper logic maps oldText "fi" inside original "const x = ffi;" to start=end=11 and produces "const x = ffiZZ;", inserting without consuming the fuzzy-matched ligature expansion. (aa43d9b01f3f)
  • Superseding candidate is viable: fix(edit): preserve unrelated lines during fuzzy text matching #96636 is open, non-draft, maintainer-editable, labeled proof:sufficient, has no non-success current PR checks, and exposes refs/pull/96636/merge. (62692de5e64e)
  • Latest release still contains the old whole-file normalization: Tag v2026.6.10 still contains the same whole-file fuzzy baseContent selection, so the canonical issue should remain open until a fix lands and ships. (src/agents/sessions/tools/edit-diff.ts:218, aa69b12d0086)

Likely related people:

  • vincentkoc: GitHub path history shows recent maintenance on the affected edit-diff helper, and local blame in this checkout maps the current helper snapshot to Vincent Koc. (role: recent area contributor; confidence: medium; commits: 0455028a3cd2, 3ab8d6aa609a; files: src/agents/sessions/tools/edit-diff.ts)
  • steipete: The affected session tool files were carried through the broad agent-runtime internalization refactor, which is relevant routing context for this core edit-tool path. (role: runtime refactor provenance; confidence: medium; commits: bb46b79d3c14; files: src/agents/sessions/tools/edit-diff.ts, src/agents/sessions/tools/edit.ts)
  • SebTardif: Authored the older open same-root PR that first preserved unrelated lines and added mapper coverage for this canonical fuzzy-edit corruption report. (role: same-root candidate fix author; confidence: high; commits: 502fb5ae7ea0; files: src/agents/sessions/tools/edit-diff.ts, src/agents/sessions/tools/edit-diff.test.ts)
  • rizwan-saddal: Authored the current superseding candidate PR with sufficient real behavior proof and a more complete mapper for the same bug cluster. (role: canonical replacement branch author; confidence: high; commits: 62692de5e64e; files: src/agents/sessions/tools/edit-diff.ts, src/agents/sessions/tools/edit-diff.test.ts)

Codex review notes: model internal, reasoning high; reviewed against 31a0f97dd944.

@openclaw-barnacle openclaw-barnacle Bot removed the triage: needs-pr-context Candidate: external PR body lacks required problem context or evidence. label Jun 25, 2026
@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P0 Emergency: data loss, security bypass, crash loop, or unusable core runtime. merge-risk: 🚨 other 🚨 Merging this PR has meaningful risk outside the owned taxonomy. labels Jun 25, 2026
@jiahjian
jiahjian force-pushed the fix/p0-issue-89994-fuzzy-edit-silent-rewrite branch 2 times, most recently from 164c331 to caa0d5f Compare June 25, 2026 15:20
…89994)

When any edit falls back to fuzzy matching, normalizeForFuzzyMatch was
applied to the entire file, silently mutating unrelated lines (trailing
whitespace stripped, smart quotes/dashes/spaces converted to ASCII,
NFKC recomposed). The model-facing diff was computed against the
already-normalized base, making the corruption invisible.

Fix: use normalizeForFuzzyMatch only to locate match positions. Add
findOriginalPosition (prefix-based binary search) and findOriginalSpan
to map fuzzy match positions back to original content. Keep baseContent
as the original LF-normalized content so untouched lines are preserved
byte-for-byte.

Closes openclaw#89994.

Co-Authored-By: Claude <[email protected]>
@jiahjian
jiahjian force-pushed the fix/p0-issue-89994-fuzzy-edit-silent-rewrite branch from caa0d5f to aa43d9b Compare June 25, 2026 15:22
@openclaw-barnacle openclaw-barnacle Bot added the scripts Repository scripts label Jun 25, 2026
@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. and removed status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Jun 25, 2026
@clawsweeper

clawsweeper Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

ClawSweeper applied the proposed close for this PR.

@clawsweeper clawsweeper Bot closed this Jun 25, 2026
@jiahjian
jiahjian deleted the fix/p0-issue-89994-fuzzy-edit-silent-rewrite branch June 26, 2026 01:19
@jiahjian
jiahjian restored the fix/p0-issue-89994-fuzzy-edit-silent-rewrite branch June 26, 2026 01:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling merge-risk: 🚨 other 🚨 Merging this PR has meaningful risk outside the owned taxonomy. P0 Emergency: data loss, security bypass, crash loop, or unusable core runtime. proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. scripts Repository scripts size: M status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants