Repository navigation
Description
Summary
The edit tool cannot match oldString retyped with common multi-character typographic drift — -- for an em dash —, -> for →, * for ·, leftover read-style 12: line prefixes — and the failure is total: the existing fuzzy chain never covers single-line candidates or these equivalences. This is very frequent for markdown files containing Unicode punctuation and costs a read→edit→fail→retry round-trip per occurrence. I'd like a Unicode-normalization fallback in the matcher (and/or nearest-match snippets in the error, see #42908).
Environment
- opencode version: v2.0.30
- OS: Windows_NT 10.0.26300 (win32 x64)
- Terminal: Unavailable (agent-driven sessions; TUI not involved in the failure)
- Shell: cmd.exe
- Install/channel: latest
- Active plugins: a user-side workaround for this exact gap (see Additional Context); the failure matrix below was captured with that plugin disabled
Reproduction
demo.md (note the real U+2014, U+2192, U+00B7 and the blank middle line):
Phase 2 — deploy the service and register receivers
Lockscreen → unlock on tap with care please
Battery threshold set to 15% · do not disturb mode enabled
Config block alpha — value one here
Config block beta — value two more
Trailing line for uniqueness xyz
Call edit the way models naturally emit it after seeing the file through read (they retype, and ASCII-ify):
| # | oldString used (vs file text above) |
built-in result |
|---|---|---|
| A | Phase 2 -- deploy the service and register receivers |
❌ Could not find oldString |
| B | Lockscreen -> unlock on tap with care please |
❌ Could not find oldString |
| C | Battery threshold set to 15% * do not disturb mode enabled |
❌ Could not find oldString |
| D | Config block alpha -- value one here\n\nConfig block beta -- value two more |
❌ Could not find oldString |
| E | 4: Config block alpha — value one here (read line-number prefix left in) |
❌ Could not find oldString |
All five were verified failing with the workaround plugin disabled and succeeding with it enabled.
Cases A–C show the single-line path has no tolerance at all beyond exact/1:1-variant matching (BlockAnchorReplacer is block-based), and the retry's "typographic dash/quote variants" mapping is 1:1 per character, so length-changing equivalences (-- ↔ —, -> ↔ →, * ↔ ·) never match. E (line prefixes) has no handling anywhere. D is plausibly the blank-line denominator bug in #45199.
Expected Behavior
- Before failing, attempt a canonicalized fallback: compare a whitespace-stripped, NFKD-normalized form of
oldStringagainst the file with a small equivalence table (all dashes →-,→/⇒→->, curly quotes → straight,…→...,·/•→*, …). Edit the byte-exact on-disk substring found; existing uniqueness/ambiguity rules stay as they are. This should also cover 1–2-lineoldString, not only multi-line blocks, and should ignore accidentally includedN:read prefixes. - When nothing matches even fuzzily, include one or two closest regions (
file:line+ verbatim snippet) in the error so the model self-corrects in one step instead of re-reading the whole file.
Actual Behavior
Every drift class above fails outright with:
Could not find oldString in demo.md. It must match exactly, including whitespace and indentation.
…which (per #44996) also misstates the real contract, and gives no correction hint (see also #42908).
Additional Context
- Frequency: on one handoff-style markdown with 63
—, 16→, and assorted·/±/≥, 77 of 78 synthetically drifted lines recover to the exact file text under a normalization fallback; the 78th is correctly refused as ambiguous. So a compatibility-normalized fallback is both effective and safe with the existing uniqueness rules. - I currently mitigate with a global plugin (
tool.execute.beforerewritingevent.input.oldStringto the exact on-disk substring via such a skeleton match, refusing ambiguous/tiny/pathological matches). It works, but a built-in fix is better: other clients and models hit the same wall, and hook-level input rewriting is second-class. Happy to contribute the matching core as a PR if maintainers want it. - Related, all in the fuzzy-edit family: [FEATURE]: port V1 fuzzy edit matching to the V2 edit tool #51706 (V1 fuzzy port, closed — the current chain), edit: blank lines inside a block make BlockAnchorReplacer reject near-identical matches #45199 (open — blank-line scoring bug, likely case D), The
edittool intermittently reports "Could not find oldString" for text that is verifiably present in the target file #52067 (open — verifiably-present oldString intermittently not found; distinct: there the text is byte-present, here it genuinely differs),edit: not-found error claims exact whitespace matching, but matching normalizes whitespace #44996 (open — error text misstates matching contract),edittool: failed oldString match returns no context — models abandon the edit instead of retrying #42908 (closed — not-found error has no context), [FEATURE]: Make fuzzy edit feedback stricter to improve SWE-bench reliability #34424 (stricter fuzzy feedback), [BUG] Edit tools don't normalize Unicode (NFC vs NFD) causing edit failures on macOS #31786 (closed — NFC/NFD normalization).
Activity
opencode-agent commentedon Oct 16, 2026
Thanks for the detailed write-up and the failure matrix!
To sum up the request: when the
edittool finds no exact match, it should fall back to a normalized comparison. That comparison would treat substitutions that change length (--/—,->/→,*/·,.../…) as equal, cover one- and two-lineoldStringvalues, ignore leftoverN:prefixes fromread, and keep the existing uniqueness rules. If nothing matches, the error would show the closest regions of the file.Today the tool's retry swaps only one character for one character (typographic quotes, dashes and spaces), plus trailing whitespace, so most of your cases fall outside what it currently handles. I've filed this as a feature request, and maintainers will consider it, including your offer to contribute the matching code. Your
tool.execute.beforeplugin is a reasonable way to handle this in the meantime.Related issues:
edittool: failed oldString match returns no context — models abandon the edit instead of retrying #42908: request for context in the not-found erroredit: not-found error claims exact whitespace matching, but matching normalizes whitespace #44996: not-found error text does not match how matching actually worksedittool intermittently reports "Could not find oldString" for text that is verifiably present in the target file #52067: text that is present in the file is sometimes reported as not found (a different problem)