Back to blog

It Wasn't a Hallucination — My Agent Had a Cache-Coherence Bug

Sep 18, 2026
It Wasn't a Hallucination — My Agent Had a Cache-Coherence Bug

Tool-call #203: the agent opens appsettings.json and, before touching it, recites the current contents back to me — full JSON block, correct keys, correct nesting, a connection string I recognize from an environment we retired weeks ago. No "let me check," no hedge. Just confident recall. Problem is, tool-call #47 — ninety-some calls earlier in that same session — deleted that file as part of a cleanup step I'd approved myself. The agent is about to write forty lines into a file that, as far as the filesystem is concerned, no longer exists in this form. And it's quoting the deleted version like it just read it off disk.

This was a long-running session cleaning up a .NET service ahead of a config migration — the kind that racks up two, three hundred tool calls before you look up. Long enough that you stop reading every diff and start skimming for the ones that smell wrong. This one smelled wrong.

The reflex: call it a hallucination

My first move was the standard one. Model hallucinated a file's contents, re-prompt it to actually read the thing, move on. I've filed dozens of weird mid-session moments under that label without a second thought, because the label comes with a built-in exit: it's a model quirk, not a system I need to understand. Except this time the quoted content wasn't invented. It was real — word for word, matching a version of appsettings.json that genuinely existed at some point in this session. Just not the point the agent claimed.

What the transcript actually says

So I pulled the raw transcript instead of re-prompting. Tool-call #12: a grep across the repo for a config key, which happens to return the full contents of appsettings.json as match context. Tool-call #47: the cleanup step, deleting that file as dead weight from an old environment split. Tool-call #150: a mid-session summarization pass — the harness compacting older turns to keep the context window sane — which flattens the #12 grep result into plain prose: "appsettings.json contains {config}." No timestamp, no caveat, nothing marking this as observed-once-and-not-reverified. Tool-call #203 reads that summarized line, finds it sitting in context looking exactly as current as everything else, and acts on it.

Nothing in that chain is a lie. The grep was accurate at call #12. The summary is an honest compression of what the grep said. Every step, taken alone, is correct. The failure lives entirely in the gap between #47 and #150 — a write happened, and nothing downstream knew to care.

It's not a hallucination, it's a cache-coherence bug

Once I saw the chain laid out, the framing flipped on me. This isn't the model making things up — it's a cache with no invalidation path. The grep at #12 cached a fact. The delete at #47 was a write that should have invalidated it. It didn't propagate, because nothing in the session treats "a fact learned from tool output" as a cache entry with a shelf life. Then the summarization at #150 did something worse than simply failing to invalidate it — it laundered the staleness into confident prose, stripping out the one thing that might have saved me: the link back to call #12, the moment I could have paused and asked, wait, is that still true?

Why the transcript can't tell you

This is structurally invisible — not a one-off bug you can grep for. Every line in an agent transcript reads in the same eternal present tense. "The file contains X" looks identical whether X was observed ten seconds ago or ten minutes and ninety tool calls ago, through a state of the world that's since been overwritten. Nothing timestamps a fact. Nothing scopes a fact to a filesystem snapshot a later write has already replaced. A human skimming the transcript — or a model attending over its own context — has no signal to tell "true when observed" from "true now." And the detail that would let you catch it is exactly what compaction throws away first, because it reads like the least essential clause in the sentence.

The fix: re-read before every write

The fix that actually worked wasn't a bigger context window, and it wasn't a smarter model — I tried both, and the failure reproduced on Opus just as easily as on the smaller model, because this was never a reasoning failure. It's a missing mechanism. The fix was a hard rule, enforced independent of what the agent "remembers": any tool call that mutates a target — edit, write, delete, move — has to be preceded by a fresh read of that exact target, in the same turn. No exception for "I already know what's in this file." Recall doesn't get to substitute for a re-check on anything you're about to change.

text
Rule: mutating-call-requires-fresh-read

Before any tool call that writes, edits, deletes, or moves a file, the agent
MUST issue a read of that exact path in the same turn — even if its contents
were already established earlier in this session.

No exception for "I already read this." A cached fact is not a current fact.
If the read fails (file missing, permissions changed), that is itself
information — surface it, don't paper over it with the cached version.

It costs one extra tool call per mutation. On a two-hundred-call session, that's real overhead. It's also nothing next to quoting a deleted file back to me as ground truth.

Before and after

A week later, same class of session — another cleanup-then-migrate run, another few hundred calls deep. Same shape of trap: a file got moved early on, then referenced again much later, after a summarization pass had flattened the move into a flat "file X is at path Y." With the rule in place, the agent issued its mandatory pre-write read, hit a "path not found," and stopped to ask instead of writing. That's the entire difference. Before the rule, that session writes to a path that no longer means what the agent thinks it means. After, it surfaces as a one-line pause instead of a diff I'd have to unwind later. No new capability, no smarter model — the near-miss got caught because re-derivation was mandatory, not because anything got better at remembering.

Every framework I've used ships eviction. None of them ship invalidation.

Zoom out and this stops being a quirk of one session — it's a gap in every agent harness I've touched. They all ship eviction: compaction, summarization, sliding windows, some mechanism to keep context bounded as the call count climbs. None of them ship invalidation — a mechanism that says "this fact was derived from a state the world no longer has, discard it or re-derive it before you act on it." Eviction manages size. Invalidation manages truth. A framework can be excellent at the first and have zero mechanism for the second, and from the outside both failures look identical: the agent said something wrong.

It gets worse at multi-agent handoffs, and this is the part I can't shake given where my own research sits. When one agent hands a summary to another — a planner passing findings to an executor, a subagent reporting back to its parent — that summary is exactly the present-tense, unstamped artifact that caused the appsettings.json failure, except now it's crossing a trust boundary. The receiving agent has no way to know the fact was derived from a filesystem state that a third agent, running concurrently, may have already changed. A stale fact doesn't just persist — it gets re-rooted as someone else's ground truth, one hop further from the observation that ever made it true.

The bug looked like a hallucination because that's the label with the easy exit. It was actually a cache-coherence bug: a grep result cached early got echoed back by a summarization step as present-tense fact, and the transcript format has no way to mark the difference between "true when observed" and "true now." The fix wasn't a smarter model. It was refusing to let the agent trust its own memory on anything it was about to change.

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.

Ask me anything about “It Wasn't a Hallucination — My Agent Had a Cache-Coherence Bug” — trade-offs, decisions, or the story behind it.