Repository navigation
Description
Description
BlockAnchorReplacer in packages/opencode/src/tool/edit.ts scores a candidate block by averaging per-line similarity over the middle lines. Blank middle lines are skipped by a continue, but they are still counted in the denominator — so a pair of blank lines, which is an exact match, contributes 0 to the average instead of 1.
Single-candidate branch:
const linesToCheck = Math.min(searchBlockSize - 2, actualBlockSize - 2)
for (let j = 1; j < searchBlockSize - 1 && j < actualBlockSize - 1; j++) {
const originalLine = originalLines[startLine + j].trim()
const searchLine = searchLines[j].trim()
const maxLen = Math.max(originalLine.length, searchLine.length)
if (maxLen === 0) {
continue // <-- contributes nothing
}
const distance = levenshtein(originalLine, searchLine)
similarity += (1 - distance / maxLen) / linesToCheck
}maxLen === 0 means both lines are empty after trim() — they match perfectly. The loop runs exactly linesToCheck times, so the denominator is fixed up front and every skipped pair is a straight subtraction of 1 / linesToCheck from a score that should have been full marks.
The multiple-candidate branch has the same continue, with similarity /= linesToCheck afterwards.
Running the scoring loop verbatim on a 7-line block with 2 blank middle lines, where the model's oldString has drifted by one character:
block lines: 7 middle lines: 5 blank middle lines: 2
current similarity = 0.583 -> below the 0.65 threshold, no match
correct similarity = 0.983 -> match
The block is a 98% match and gets scored at 58%.
Why it matters
BlockAnchorReplacer is third in the replacer chain, after SimpleReplacer and LineTrimmedReplacer. It only runs when the exact and line-trimmed matches have already failed — i.e. precisely when the model's oldString has drifted from the file and the fuzzy fallback is the last thing standing before the edit fails. Blank lines inside a function body are completely ordinary, so the fallback is weakest on exactly the blocks it is most likely to be handed.
The more blank lines a block has, the harder it is to match: 2 blanks out of 5 middle lines caps the achievable score at 0.6, below the 0.65 threshold, so such a block can never match no matter how similar the rest of it is.
Steps to reproduce
- Have a file with a function containing blank lines between statements.
- Call
editwith anoldStringfor that block that differs slightly from the file (a changed character, soSimpleReplacerandLineTrimmedReplacerboth miss). - The edit fails as not found, even though the block is nearly identical.
Operating System
Windows 11 (platform independent — pure arithmetic)
Activity
neriousy commentedon Oct 19, 2026
The fuzzy
BlockAnchorReplacerand its similarity scoring no longer exist in v2, so this particular scoring bug no longer applies. The replacement edit tool uses exact matching with limited punctuation and whitespace normalization, rather than scoring near-identical blocks. Upgrade withnpm i -g @opencode/cli@latest. If you still see this on v2, please open a new issue with the details.