anomalyco / anomalyco/opencode

edit: blank lines inside a block make BlockAnchorReplacer reject near-identical matches

Open
#45,199 0 comments 0 reactions 1 assignee View on GitHub

@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
  1. Have a file with a function containing blank lines between statements.
  2. Call edit with an oldString for that block that differs slightly from the file (a changed character, so SimpleReplacer and LineTrimmedReplacer both miss).
  3. The edit fails as not found, even though the block is nearly identical.
Operating System

Windows 11 (platform independent — pure arithmetic)

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.