Skip to content

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

Closed as not planned

Description

@harshil-mistry

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)

Activity

  1. neriousy commented on Oct 19, 2026

    @neriousy
    Member

    The fuzzy BlockAnchorReplacer and 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 with npm i -g @opencode/cli@latest. If you still see this on v2, please open a new issue with the details.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions