The Agent Wasn't Lying About 'Done' — Its Memory Was
"Done, all three files updated." The agent says it, and it isn't lying — not the way you'd catch a person lying. Push back and it'll walk you through the diff, calm and specific. From where it's standing, inside a context that just got compacted, the sentence is true. Somewhere upstream, a summary told it the rename was finished, and it took that summary the way you'd take a teammate's handoff note: at face value. It's reporting the world as it was described to it. Faithfully. That's the whole problem.
Here's how it actually plays out. You watch the diff scroll by — three files touched, tests green on each, CI green on the PR. You merge. Three days later a teammate is staring at a NameError in prod: `computeInvoiceTotal` is not defined, called from a file none of you had thought about twice, because you all watched that function get renamed in the same session. Same agent, same run, same string of green checkmarks. Something in the middle went quiet, and nobody clocked it until a real call path hit the gap.
Rewinding the transcript
I went back and pulled the transcript afterward, mostly to settle whether this was bad luck or something that repeats. The task was mechanical — the kind you hand to an agent precisely because it's tedious and low-risk: rename `computeInvoiceTotal` to `calculateInvoiceTotal` across `billing.py`, `invoice_service.py`, and `reports.py`, then chase down every call site. The session ran long. A couple of failed test iterations, some back-and-forth over an unrelated import error. Around turn 34, context compaction fired.
At that exact moment, here's what the real files looked like. `billing.py`: definition renamed, its one internal call site fixed. `invoice_service.py`: call site updated. `reports.py`: the rename had landed on a local helper — but the call site three lines below it still read `computeInvoiceTotal(...)`, untouched, next in the queue. That's the actual state of the repo. Here's what the auto-generated summary said instead.
Progress summary: Renamed computeInvoiceTotal to calculateInvoiceTotal.
Updated call sites in billing.py, invoice_service.py, and reports.py.
All references now use the new name. Tests passing for billing and
invoice modules. Remaining: verify reports.py test suite.Read that last line again. "Verify reports.py test suite" — not "finish updating reports.py." The summary had already checked the call-site fix off. It wasn't done. And the agent, past that point, had no way left to know the difference — because the thing it was about to trust wasn't the file. It was that paragraph.
It's not random. That's what should worry you
What happened next isn't noise — it's the mechanism doing exactly what it's built to do. The agent's post-compaction moves tracked the summary's account of reality, not the file's actual contents. It ran the reports.py test suite, which passed, because the test mocked the function at the module boundary and never touched the unrenamed call site — then reported the whole rename complete. It didn't re-derive "what's left" from a grep or a diff against the plan. It pulled it from the one thing that survived compaction: prose. Diffs don't survive compaction. Working state doesn't. The summary does, by design — that's the entire point of it. And prose is a lossy compression of intent, not a ledger of fact. The part that's easy to miss: the summary wasn't wrong on purpose, and it wasn't hallucinated from nothing. It was written mid-task, extrapolating a near-finished state forward to "finished" — the exact move you make when you tell a colleague "yeah, I've basically renamed it everywhere" while you're one file short. The difference is you know you're one file short. The next version of the agent, reading only your sentence, doesn't.
Why refactors take the hit hardest
This shape of task — rename, then chase every call site — carries an implicit ordering: you can't honestly claim a call site is fixed until you've actually visited it. The agent was tracking that queue in working memory, not in any file. Nothing in the repo says "reports.py call site: not yet touched." That fact lived only in the agent's live reasoning about what it had and hadn't gotten to. Compaction keeps the what — renamed X to Y, touched these three files — because that's the part that compresses cleanly into a sentence. It drops the what's-still-pending, because sequencing state doesn't survive being turned into prose without becoming exactly the kind of granular claim a summary exists to smooth over. So the agent isn't repeating work at random, and it isn't skipping work at random either. It redoes the step the summary flagged as ambiguous ("verify reports.py") and treats as closed the step the summary described as certain ("updated call sites in ... reports.py"). You end up with three files that are each internally coherent and collectively wrong — and the wrongness sits exactly where the summary papered over the seam.
Why review didn't catch it
This is the part that actually stings, because review did its job. Each file's diff was small and legible. Each file's tests passed on their own merits. CI was green on the aggregate. Nothing in a per-file view is wrong — `reports.py`'s diff just happens to be incomplete, and an incomplete diff looks identical to "this file needed a smaller change" unless you're specifically checking coverage across files instead of within them. The inconsistency only exists in the gap between files, and nobody was diffing the agent's own claim — "all call sites updated" — against what the files actually held at that point in the run. We had a green pipeline and a confident closing message. Both were true statements. About the wrong thing.
The checklist
- ▹Before trusting a multi-file agent PR, find the compaction boundary in the transcript — most harnesses log it explicitly, or you'll see a summary paragraph suddenly stand in for what had been tool calls and diffs.
- ▹Check the summary's claims against actual file contents at that point in the session, not just against the final diff. If the summary says a file is done, open that file as it stood at that turn.
- ▹For rename/call-site refactors specifically, don't let per-file tests stand in for coverage — after the agent claims completion, run a cross-file reference check: grep for the old symbol repo-wide, or use your language server's find-references.
- ▹Right after compaction, make the agent restate what it thinks is left before it keeps going, then check that restatement against the files yourself. It's the cheapest point to catch the drift — before more work gets stacked on top of a wrong premise.
None of this is a case for pulling agents off refactors. Mechanical, high-volume renames are exactly where they save you the most time, and this failure mode is narrow and well understood once you've watched it happen once. It's a case for one extra, boundary-aware step in review, layered on top of the diff you were already reading. The fix costs you a grep and a cross-reference check. The miss costs you a production NameError and a teammate's afternoon. You only need to pay that cost once before the checklist turns into reflex.
Discussion
Chat with Chi Cong (AI) about this article. Your conversation is private to you — you can publish a summary for others when you're done.