On this page

← Blog
Engineering6 min read

We made the model stop echoing your code back

Our fix-apply used to ask the model to repeat the exact code it was patching, then match against that repeat. Any typo in the echo broke the match. Here's the hallucination class that created, and why we stopped asking the model to remember what it can already read.

ai-debuggingmcpedit-contractreliability

The old apply path had a quiet assumption baked into it: if the model wrote the fix, it could also be trusted to repeat the exact lines it was fixing. Both halves came from the same response, so a mismatch felt like it shouldn't happen. It happened. A model-echoed line came back with a tab where the file had two spaces, or a trailing carriage return the model's tokenizer didn't preserve, and the patch that depended on matching that line byte for byte just failed to apply. We tracked it internally as fix/applyfix-indent-crlf. Small bug, wrong root cause: we were asking the model to remember something we already had in hand.

What was actually being asked

To build a patch, an applier needs an exact old_string to find and an exact new_string to replace it with. The straightforward way to get that is to ask the model for both. That means every patch carries a second hallucination surface beyond "is the fix right": is the model's memory of the code it just read also character-exact. Most of the time it is. The failures cluster in whitespace, line endings, and long snippets, which is exactly the kind of thing a language model has no strong reason to reproduce perfectly, because none of it changes what the code means.

Note: the fix itself being correct and the echoed old code being byte-exact are two different claims. Grading only the first one and assuming the second made every apply-path bug look like a fix-quality bug, when it was actually a transcription bug.

What we changed

The engine already knows the user's submitted code, verbatim, because the user sent it. It also already enforces a contract on every fix: line_hint names the exact lines being replaced, code is the full replacement for exactly those lines. That means old_string was never actually unknown. It's just the named lines of the snippet the request came in with. So we stopped asking the model for it and started deriving it mechanically, in the engine, from the request that's already sitting there.

python
# apps/engine/app/services/edit_contract.py
# old_string comes from the user's OWN submitted snippet, not a model echo.
# Expanded with context lines until it matches exactly once, or omitted.

If the named lines occur more than once in the snippet, the engine expands the match outward with surrounding context lines until it's unique. If it's still ambiguous after that, edits gets left off the response entirely. No fallback guess, no "close enough" match. The rest of the response still ships, just without a field we can't back mechanically.

Fix: old_string is sliced directly from the request payload, never regenerated by the model. Byte-exact by construction, because it was never re-typed by anything.

The CRLF part

Windows files are CRLF, most of our internal comparisons are LF, and the two used to meet exactly at the point where an echoed string got matched against a file on disk. old_string now arrives LF-normalized from the engine. The applier matches in LF space, then restores whatever line ending the actual file was using before writing anything back.

typescript
// apps/extension/src/fixApply.ts
const eol = originalContent.includes('\r\n') ? '\r\n' : '\n';
let content = originalContent.split(/\r\n/).join('\n');
// ... match and replace happens in LF space ...
return eol === '\n' ? content : content.split('\n').join(eol);

A CRLF file now stays all-CRLF after a patch instead of coming back mixed, which is its own quiet source of "why does git diff show every line changed."

Fail-closed, not best-effort

The applier that runs this, applyEditsToString, has one job and one exit condition: find old_string exactly once, replace it, or return null. Missing entirely, present twice, doesn't matter which, same result.

typescript
const first = content.indexOf(oldStr);
if (first === -1) { return null; }
if (content.indexOf(oldStr, first + 1) !== -1) { return null; } // ambiguous

null isn't an error state, it's a routing signal. The extension tries the structured edit first, and on any miss falls back to the older line-hint apply path unchanged, the same one that's been running the whole time. Nothing gets forced through on a partial match. A patch that can't apply cleanly doesn't apply at all.

Note: unified_diff still ships alongside edits, but its line numbers are relative to the snippet the client sent, not the whole file, whenever that snippet was a fragment. Context-matching tools tolerate that offset. We say so in the schema instead of presenting it as file-absolute and letting someone find out the hard way.

What this doesn't cover

verify_with in this pass is a syntax check only, python -m py_compile or node --check, run when the file path is known. Asking the model to propose an actual test command is a real feature and a real prompt change, which means it goes through the benchmark gate before it ships, not before. No sandbox execution either. This phase closes one specific hallucination surface, the echoed patch, not every way a fix can be wrong.

Every fix that carries a parseable line hint and real replacement code now gets a mechanically derived edit. Every fix that doesn't, a shell-command suggestion, an out-of-range hint, an ambiguous match, carries no edits key at all rather than a guessed one. The apply button either has ground truth under it or it doesn't, and now it knows which.

FAQ

Q: Does this mean the fix itself is guaranteed correct now?

A: No. This closes one specific gap, whether the patch applies cleanly to the code that's actually there. Whether the fix is the right fix is a separate question, covered by the tri-state verification described in our confidence calibration writeup.

Q: Why not just ask the model to be more careful with whitespace?

A: Because the model doesn't need to reproduce anything if it never has to. The engine already holds the exact bytes. Deriving old_string mechanically removes the failure mode instead of asking the model to avoid it more reliably.


DebugAI reads your codebase, not just your stack trace, and now hands you a patch built from code it already had, not code it tried to remember.

Debug faster starting today.

Free VS Code extension · 10 sessions/day · no credit card

Install free →

Related posts

Engineering

We built a test harness for our own AI debugger. The harness had more bugs than the debugger.

6 min read

Engineering

Our AI debugger's confidence numbers were inverted. Here's the graph.

7 min read

← All posts