You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Proposal] str_replace_editor: tolerant old_str matching + closest-region hint for local models (cherry-pickable branch)
#4934
str_replace_editor locates old_str with a bare content.indexOf(old_str) and, on a miss, answers only "did not appear verbatim". Local models driven through dsh miss that literal contract constantly — not because of whitespace in the file, but because they re-type the target lines from memory. Each miss costs a grep -n round trip before the model can retry, which at local decode speeds is the dominant cost of an edit.
I have a small, cherry-pickable change on a fork that keeps literal matching first, applies a unique whitespace-only match, and on a real content miss shows the model the closest region with line numbers. Since the project does not take PRs, I'm posting it here as a proposal.
The model's old_str began at `services/api, … — it dropped the first line of the span and invented an opening backtick so its snippet looked well-formed. The tool replied:
Error: No replacement was performed, old_str ` `services/api, services/worker, … tools/release`.` did not appear verbatim in …/plan.md.
The model then ran grep -n "tools/ci, tools/docs" plan.md, viewed the lines, and retried. Both quantisations do this repeatedly; the file itself had no tabs, CRLF, trailing spaces, or non-ASCII in that region.
Two failure classes need different answers:
Whitespace-only misses — an indentation level dropped, trailing spaces added, CRLF — carry the model's intent intact; refusing them is pure friction.
Content misses — a fabricated character, a dropped line — must never be auto-applied, but the model needs to see what the file actually says at the spot it meant.
The change
Literal matching is unchanged and stays the first path. Only when it fails:
Tolerant fallback, applied only when unique. Compare line windows after trimming trailing whitespace (which also absorbs \r), and require every non-blank line to carry the same indentation shift — a prefix added or a fixed count stripped, never tabs against spaces. Exactly one such window is applied: new_str receives the same shift and the window's line ending, and the success message says old_str matched after normalising indentation and trailing whitespace. Two or more windows are refused as FS_AMBIGUOUS_EDIT with their line numbers.
Closest-region hint on a miss. Score every window by per-line similarity (exact after trim = 1, else common-prefix ratio) and append the best region to the FS_EDIT_NOT_FOUND message in view's numbered format, followed by one instruction: copy old_str exactly from a view of those lines, including leading whitespace and markup such as backticks. Nothing is appended when no window resembles old_str or the file is shorter than it.
Unchanged: the public old_str vocabulary, the error codes, the literal-match behaviour, and the default tool description. A model that copies correctly never enters the new paths.
For the backtick case above, the error now ends with:
The closest match is at lines 323-325:
323 services/api, services/worker, services/scheduler, tools/ci, tools/docs,
324 tools/release`.
Copy old_str exactly from a view of those lines: every line must match character for character, including leading whitespace and markup such as backticks.
Evidence
packages/fs/tool-str-replace-editor/tests/tools.spec.ts: 6 new cases — indentation shift applied and re-indented; surplus indentation stripped with CRLF preserved; whitespace-only window; ambiguous normalised match refused; closest-region hint (fabricated backtick, tabs vs spaces, uneven shift); no hint when nothing resembles old_str or the file is shorter. 20/20 pass; the 14 existing tests are untouched.
Coverage on the package: 100% statements / branches / functions / lines (v8, the repo's per-file threshold).
oxlint clean (the loops are written without non-null assertions), tsc clean, pnpm run doc-sync 32/32 — config catalog regenerated, README pair and Agent Note pair recorded with verify-translation-pairing.
Alternatives I rejected
Similarity-threshold auto-apply (Aider-style). Would also have "fixed" the backtick case — by editing lines the model never quoted. A content miss is exactly where the model's picture of the file is wrong; the tool must show, not guess.
A replace_lines(start, end) command. Sidesteps old_str entirely and suits weaker models, but adds a command to a vocabulary that deliberately mirrors the Claude Code editor, and does nothing for the whitespace misses that dominate. Possible follow-up.
Description-only guidance. A description override telling the model to view first and copy verbatim reduces misses (I run one — see below) but does not remove the round trip when a miss still happens. Complementary, not sufficient.
Workaround others can use today
The tool's description is configurable; this is the block I run in settings.yaml alongside the patch, so the model is told to copy from a view rather than from memory:
tool-str-replace-editor:
description: | <upstream default text> * Never type `old_str` from memory. Immediately before every `str_replace`, `view` the lines you intend to change (use `view_range`) and copy `old_str` character for character from that output, dropping only the line-number prefix. Keep every leading space and every backtick, bullet, or other markup exactly as shown. * Keep `old_str` as short as possible while still unique: one to three lines that occur once in the file. * If a `str_replace` fails, do not retry with a guessed `old_str`: `view` the region the error names and copy it exactly. * To add lines without changing existing ones, prefer `insert` with `insert_line`.
Related discussions (searched before posting)
I searched Discussions for str_replace, old_str, str_replace_editor, "did not appear verbatim", fuzzy/tolerant/whitespace matching and the Chinese equivalents; nothing covers this, but three are adjacent:
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Summary
str_replace_editorlocatesold_strwith a barecontent.indexOf(old_str)and, on a miss, answers only "did not appear verbatim". Local models driven through dsh miss that literal contract constantly — not because of whitespace in the file, but because they re-type the target lines from memory. Each miss costs agrep -nround trip before the model can retry, which at local decode speeds is the dominant cost of an edit.I have a small, cherry-pickable change on a fork that keeps literal matching first, applies a unique whitespace-only match, and on a real content miss shows the model the closest region with line numbers. Since the project does not take PRs, I'm posting it here as a proposal.
master: master...thinkun:deepseek-harness:fix/str-replace-tolerant-matchproposed/feature): https://github.com/thinkun/deepseek-harness/blob/fix/str-replace-tolerant-match/.agents/notes/proposed/feature/2026-08-29-str-replace-tolerant-match.mdEnvironment
0.1.1-rc.2(npm), CLI anddsh web, macOS (Apple silicon); patch based onmasteratdsh-v0.1.2-alpha.1(cd5ef81481)openai-completionsprovider (llm-pi-ai)@deepseek-ai/dsh-tool-str-replace-editor, default descriptionWhat happens
The file being edited contained a wrapped Markdown code span:
The model's
old_strbegan at`services/api, …— it dropped the first line of the span and invented an opening backtick so its snippet looked well-formed. The tool replied:The model then ran
grep -n "tools/ci, tools/docs" plan.md, viewed the lines, and retried. Both quantisations do this repeatedly; the file itself had no tabs, CRLF, trailing spaces, or non-ASCII in that region.Two failure classes need different answers:
The change
Literal matching is unchanged and stays the first path. Only when it fails:
\r), and require every non-blank line to carry the same indentation shift — a prefix added or a fixed count stripped, never tabs against spaces. Exactly one such window is applied:new_strreceives the same shift and the window's line ending, and the success message saysold_str matched after normalising indentation and trailing whitespace. Two or more windows are refused asFS_AMBIGUOUS_EDITwith their line numbers.FS_EDIT_NOT_FOUNDmessage inview's numbered format, followed by one instruction: copyold_strexactly from a view of those lines, including leading whitespace and markup such as backticks. Nothing is appended when no window resemblesold_stror the file is shorter than it.Unchanged: the public
old_strvocabulary, the error codes, the literal-match behaviour, and the default tool description. A model that copies correctly never enters the new paths.For the backtick case above, the error now ends with:
Evidence
packages/fs/tool-str-replace-editor/tests/tools.spec.ts: 6 new cases — indentation shift applied and re-indented; surplus indentation stripped with CRLF preserved; whitespace-only window; ambiguous normalised match refused; closest-region hint (fabricated backtick, tabs vs spaces, uneven shift); no hint when nothing resemblesold_stror the file is shorter. 20/20 pass; the 14 existing tests are untouched.oxlintclean (the loops are written without non-null assertions),tscclean,pnpm run doc-sync32/32 — config catalog regenerated, README pair and Agent Note pair recorded withverify-translation-pairing.Alternatives I rejected
replace_lines(start, end)command. Sidestepsold_strentirely and suits weaker models, but adds a command to a vocabulary that deliberately mirrors the Claude Code editor, and does nothing for the whitespace misses that dominate. Possible follow-up.descriptionoverride telling the model toviewfirst and copy verbatim reduces misses (I run one — see below) but does not remove the round trip when a miss still happens. Complementary, not sufficient.Workaround others can use today
The tool's
descriptionis configurable; this is the block I run insettings.yamlalongside the patch, so the model is told to copy from aviewrather than from memory:Related discussions (searched before posting)
I searched Discussions for
str_replace,old_str,str_replace_editor, "did not appear verbatim", fuzzy/tolerant/whitespace matching and the Chinese equivalents; nothing covers this, but three are adjacent:insertwrites LF separators into a CRLF file. Same package; this proposal does not touchinsert, and its tolerantstr_replacepath re-applies the matched window's line ending so it does not add a second mixed-EOL source.edit/writeresults give the model no visibility into what changed. This proposal is the failure-side counterpart: on a miss, show the model what the file actually says where it meant to edit.FS_NOT_OBSERVEDon first edits costs ~2 wasted steps per rejection. The round-trip cost argument is the same one made here forFS_EDIT_NOT_FOUND.Questions for the maintainers
proposed/featurethe right home for the Agent Note, and is the section shape what you expect?format-patchattachment here?Happy to adjust the branch to whatever shape helps.
All reactions