Repository navigation
Description
Feature hasn't been suggested before.
- I have verified this feature I'm about to request hasn't been suggested before.
Describe the enhancement you want to request
Summary
The edit tool's fuzzy replacer chain is an intentional design to tolerate LLM whitespace/indentation drift — this request does not ask to change or remove it. The gap is observability: when oldString matches only via a fuzzy fallback, the tool reports the same plain "Edit applied successfully." as an exact byte match. Callers (human or spec-driven automation) get zero signal that their oldString did not literally exist in the file, so a wrong oldString (e.g. drifted indentation) is silently absorbed and only surfaces later via manual diff review.
Companion bug (filed as #44996): the not-found error text falsely claims exact whitespace matching.
Why this is not a duplicate (fuzzy-chain family)
| Issue | Where the defect is | Nature |
|---|---|---|
| #41872 (open) | Multiple equivalent candidates; tool silently commits the first | Fuzzy picks the wrong target (ambiguity bypass) |
| #32559 (closed) | Stale/incorrect oldString still matches | False match applied |
| #31785 (closed) | Single-line oldString bypasses a guard | Guard bypass hole |
| #42908 (open) | No match; error gives nothing to correct against | Failure path unhelpful |
| This request | A single candidate; fuzzy matches it and applies | Success path opaque — fuzzy vs exact success indistinguishable |
Those pursue "make fuzzy more conservative". This asks instead: make a fuzzy hit recognizable, without changing the tolerance.
Reproduction
File:
alpha
beta_line
foo bar
<TAB>tab_indented
gamma
| # | File content | oldString | Result | Matched via |
|---|---|---|---|---|
| 1 leading indent | beta_line (3sp) |
beta_line (4sp) |
applied | LineTrimmedReplacer |
| 2 control | — | nonexistent_text_xyz |
rejected | — |
| 3 internal space | foo bar (2sp) |
foo bar (1sp) |
applied | WhitespaceNormalizedReplacer |
| 4 tab vs spaces | <TAB>tab_indented |
tab_indented (4sp) |
applied | LineTrimmedReplacer |
Real-world impact: in a spec-driven refactor, a spec's oldString had 4-space indentation while the file had 3 spaces. The edit silently succeeded and rewrote the line with the spec's wrong indentation; the corruption was only caught later by manual base-branch diff review.
Source analysis (packages/opencode/src/tool/edit.ts)
replace() walks nine ordered replacers — SimpleReplacer (byte-exact) first, then eight tolerant fallbacks. Two causes of the opacity:
replace()returns only the new content string; which replacer matched is known inside the loop but discarded atreturn.- The tool's output is the constant
"Edit applied successfully.";ctx.metadatacarries onlydiff/filediff/diagnostics.
Proposal
The tool already returns { metadata, title, output }, so this enriches existing fields — no return-contract change.
Recommended: detect at the tool layer (zero change to replace())
Keep replace(): string as-is. In execute, after a successful replacement:
const wasExact = contentOld.includes(old) // `old` already line-ending-normalized
const strategy = wasExact ? "exact" : "fuzzy"Sound because SimpleReplacer is first and yields oldString verbatim, so includes ⇔ exact-byte match winning; ambiguous multi-match inputs throw before returning. Then:
let output = "Edit applied successfully."
if (strategy !== "exact")
output += `\nNote: oldString did not match byte-for-byte; applied via fuzzy fallback. Verify the region if unintended.`
// + add matchStrategy: strategy to ctx.metadataNon-breaking; the note is informational, placed after the success line. Caveat: a rare edge (exact ambiguous, fuzzy unique) may report exact — acceptable for an observability signal.
Reference alternative: return { content, strategy } from replace()
For the exact replacer name, tag each replacer in the chain and return it. Listed for reference only — replace is exported and used by edit.test.ts, so this changes its signature and call sites (larger blast radius).
Optional: strict mode
An exact: true parameter restricting the chain to SimpleReplacer, for automation that needs byte-exact semantics.
Related
- Companion bug:
edit: not-found error claims exact whitespace matching, but matching normalizes whitespace #44996 (not-found error text contradicts fuzzy behavior). - Fuzzy-chain family: edit fuzzy replacers can bypass multiple-match ambiguity when equivalent candidates differ in whitespace #41872, edit tool: fuzzy replacer chain silently overwrites previous edits in same file #32559, [BUG] V1 edit.ts isDisproportionateMatch bypasses all safety checks for single-line oldString #31785,
edittool: failed oldString match returns no context — models abandon the edit instead of retrying #42908.
Environment
- opencode: 1.18.21
- OS: Windows (win32)
- Tool: built-in
edit - Confirmed via controlled probes.
Activity
github-actions commentedon Aug 25, 2026
This issue might be a duplicate of existing issues. Please check:
polkamu-ops commentedon Aug 25, 2026
duplicate of #34424, findings contributed there