apply_patch verification fails on canonically-equivalent Unicode — seekSequence needs an NFC pass

Open Beginner friendly
#31,651 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
78/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Quiet
Tech stack
typescript
Domain
tooling

Research direction

Start at packages/opencode/src/patch/index.ts:460, tracing seekSequence and the deriveNewContentsFromChunks entry point. Run the deterministic NFD/NFC reproduction from the issue, then add focused coverage for canonically equivalent lines alongside the existing patch tests. Done means an NFC context matches NFD file content without changing non-equivalent matching behavior.

Written by the indexing model from the issue text.

Description

Description

apply_patch fails with Failed to find expected lines in <file> when the file on disk and the model's context lines are canonically-equivalent Unicode but different byte sequences (NFC vs NFD). None of the 4 passes in seekSequence (packages/opencode/src/patch/index.ts:460) handle this:

  1. exact
  2. rstrip
  3. trim
  4. normalizeUnicode + trim — but normalizeUnicode (index.ts:418) only maps smart quotes, dashes, ellipsis and NBSP; it never touches combining characters.

So a file containing decomposed й (и U+0438 + U+0306) can never be matched by a patch containing precomposed й (U+0439), even though the strings are canonically equivalent and render identically. Models essentially always emit NFC, so any NFD content on disk makes the file permanently unpatchable via apply_patch — the model re-reads, retries, fails again, and burns tokens in a loop.

We hit this in production on Cyrillic markdown files (tables with rows like | Район | Астана | ... |). Related-but-different reports: #2904 (auto-closed stale), #31422 (formatter drift, closed not-planned), #27282/#11687 (Windows). None cover canonical equivalence.

Proposed fix

A 5th pass in seekSequence, after the current pass 4:

// Pass 5: NFC-normalized match (canonical equivalence — no false positives)
const nfc = tryMatch(
  lines, pattern, startIndex,
  (a, b) => a.normalize("NFC").trim() === b.normalize("NFC").trim(),
  eof,
)
return nfc

This is safe: NFC normalization followed by exact equality only matches strings Unicode defines as the same text. Happy to send a PR with this + tests if you'd take it.

Plugins

None

OpenCode version

1.15.4 (gap still present at current HEAD of packages/opencode/src/patch/index.ts)

Steps to reproduce

Engine-level (deterministic, no model needed):

import { Patch } from "./packages/opencode/src/patch" // deriveNewContentsFromChunks

const fileContent = "# Сводка\nРайон: Астана\n".normalize("NFD") // й stored decomposed
const chunk = {
  old_lines: ["Район: Астана".normalize("NFC")], // models emit NFC
  new_lines: ["Район: Алматы"],
  is_end_of_file: false,
}
// -> throws: Failed to find expected lines

End-to-end:

  1. node -e 'require("fs").writeFileSync("test.md", "# Сводка\nРайон: Астана\n".normalize("NFD"))'
  2. Ask any model to change Астана to Алматы in test.md using apply_patch.
  3. apply_patch verification failed: Failed to find expected lines in test.md — and every retry fails the same way, since re-reading doesn't change the canonical-equivalence mismatch.
Operating System

Linux (Debian-based container)

Terminal

headless (server)

Dominant language
TypeScript
Stars
209k
Forks
27.5k
Avg merge
7h 20m
Merged PRs (30d)
358

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.

More from anomalyco/opencode

All issues in anomalyco/opencode

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.