anomalyco / anomalyco/opencode
edit: blank lines inside a block make BlockAnchorReplacer reject near-identical matches
@jlongster is already working on this.
Since Aug 26, 2026.
- Dominant language
- TypeScript
- Stars
- 209k
- Forks
- 27.5k
- PR merge metrics
- PR metrics pending
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)
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.