The Silent-Data-Loss Taxonomy: Five Ways Your Agent's Write Disappears

14 min read Original article ↗

You want a self-learning agent. It reads the shared plan, distills a lesson from what just happened, and writes the lesson back so the next run starts smarter. That loop is the whole promise of agent memory: the system that runs today should be a little better tomorrow because it wrote down what it learned.

The hard part showed up last time. In the previous post I walked through a single learning that vanished from exactly that loop. Two sessions shared one memory. Both read the plan, both distilled, both wrote back, and the store logged two clean successful writes. One learning was still gone. The agent had “forgotten” something it demonstrably wrote down, and the store had no error to show for it.

That post fixed one bug. This post is about the fact that the same symptom has five different causes, and they are not the same bug. “The write succeeded but the data is gone” is not a diagnosis. It is a category. If you cannot name which member of the category you hit, you cannot know whether it is prevented today or whether you are one compaction pass away from losing it again.

The line that organizes this

Here is the split. Sometimes the write hit the wrong target, or something dropped it after the fact. The coordinator never sees those. The bytes went somewhere, or a later step ate them, and no coherence protocol was ever in a position to object. Other times the write hit the right target and a coherence race corrupted the outcome anyway. Two actors on one key, or one actor writing from a view that went stale underneath it. These the coordinator can see, because they turn on version and ownership of a specific key.

The tempting way to draw that line is a clean detect-versus-prevent split. But detection is a floor, not a fate. The members on the detect side have prevention levers too. They are just not coordination levers, and pretending “prevent” means one thing flatters the part I ship.

So here is the sharper frame: one symptom, five members, three prevention levers, each owned by a different part of the stack.

  • A durability receipt. The write returns a receipt — key, version, content hash — and the run does not count as done until something re-reads the fact through the same path the reader will use. An acknowledged write is not the same thing as a write that is durable where readers actually look. This is the lever for key mismatch, and it lives in your pipeline.
  • A retention policy. Consolidation runs under explicit rules about what it may never drop: pinned keys, never-summarize tags, a fact-preservation check after the pass. A lesson survives compaction by policy, not by luck, and the same policy keeps a TTL from aging it out before a reader needs it. This is the lever for two members, compaction drop and expiry, and it lives in your store’s consolidation step and its retention window.
  • A coordination primitive. A version that rides on the write, and ownership that invalidates a stale view before it can act. This is the lever for both races, and it lives in the runtime under your store.

Tracing is still the diagnostic floor across all five. You need the trace to know which member you hit. But every member now has an owner and a lever, and “prevent” no longer quietly means “buy a coordinator.”

The five members

Member What you see Cause Prevention lever
Key mismatch Clean write to X, reader queries Y, fact never appears App routing: writer and reader disagree on key/namespace Durability receipt (write receipt + read-back through the reader’s path)
Compaction drop Fact goes in, then disappears at a summarization boundary Lossy consolidation step overwrites or drops it Retention policy (pin rules on the consolidation step)
Expiry Fact goes in, then ages out of a TTL or retention window before the read A clock, not a summarizer: a rolling TTL or keep-last-N reaps it Retention policy (pin or renew on access)
Concurrent lost-update Two writers, same key, same base version, one silently overwrites the other Read-modify-write race on shared state Coordination primitive: version-CAS (write_cas), on one host
Stale-read then write Peer commits a newer version, agent writes from its old view Sequential: agent’s view went stale before its write Coordination primitive: MESI invalidation-deny plus reacquire, on one host

Now walk the self-learning loop through each one.

Key mismatch. Your distiller writes the lesson under memory/session-42/lessons. Your reader, three commits of refactoring later, loads memory/lessons/session-42. The write is real. The store logged it. The bytes are sitting at the key you wrote. The reader is standing at a different key asking why the shelf is empty. No race happened here. The coordinator was never involved, because nothing about versions or ownership was violated. A trace that carries both the write key and every read key shows you “wrote memory/session-42/lessons, never read memory/session-42/lessons” — that is detection, and it works. The prevention lever is a durability receipt: the write hands back key, version, and content hash, and the loop’s definition of done includes reading the fact back through the reader’s own path. Do that and the mismatch can’t survive to production silently, because the read-back fails on the spot instead of three refactors later. This lever is yours to build. The coordinator does not fix this and does not pretend to.

Compaction drop. The lesson lands correctly. Then a memory-consolidation pass runs. It compresses older entries to keep the context window affordable, and while summarizing it drops the line your lesson lived on, or folds it into a summary that no longer contains the fact. The write succeeded. The consolidation lost it. Again there is no race and no coherence violation on any key. A trace shows the fact present after the write and absent after the compaction boundary — detection, again real. The prevention lever is a retention policy: the consolidation step takes explicit rules about what it may never eat — pinned keys, never-summarize tags, a post-pass check that the pinned facts still resolve. A lesson your agent worked to distill should survive compaction because a rule says it must, not because the summarizer happened to keep it. This lever lives wherever your consolidation runs. The coordinator never sees a compaction step eat a fact.

Expiry. The lesson lands correctly and stays correct for a while. Then a rolling TTL passes over it, or a keep-last-N policy pushes it out to make room, and the row is gone before the next read wants it. No race, no wrong key, no summarizer. A clock did it. This looks like the compaction drop and it lands on the same lever, but the trigger is different, and the difference changes how you catch it. A compaction drop leaves a place in the trace you can point at: the fact is present after the write and absent after the summarization boundary. A clock leaves no such boundary. The row can be reaped lazily, on whatever access next happens to touch it, or never, and plenty of stores emit nothing at all when a TTL crosses. So you do not catch expiry by waiting for the store to announce it. You catch it on the read: the reader validates what it loaded and treats a miss as the fault, instead of trusting the store to send an event that may be fire-and-forget or may never arrive. The prevention lever is a retention policy, the same one compaction needs but aimed at the clock: pin the fact, or renew it on access, so the window cannot age out something a reader still depends on. This lever is yours too. The coordinator does not run your TTL.

Concurrent lost-update. This is last time’s bug. Two sessions, one memory key, both read version 7, both distill, both write. The store applies them in some order and the second overwrites the first. Both writes return success. One lesson is gone, and nothing errored. Here the coordinator is in the loop, because the collision is on a real key with a real version. On a single host, version-CAS prevents it: each writer submits the base version it read, write_cas checks that the key is still at that version, and the loser gets a typed retryable conflict instead of a silent drop. The write that would have vanished becomes a conflict you can catch and retry. The data stops disappearing because the version rides on the write.

Stale-read then write. The agent reads the plan at version 7. Before it writes, a peer commits version 8, which changes the ground the agent was standing on. The agent, still holding its version-7 view, goes to write. This one is sequential: the peer’s commit completed first. On a single host, MESI invalidation handles it. When the peer commits, the agent’s cached view is marked INVALID. Its next write from that now-stale view is denied fail-closed rather than applied on top of a superseded read. Recovery is reacquire() plus a fresh read, so the agent rebuilds its view on version 8 and writes from current state. The distinction that matters: this is the sequential case. If the two writes truly overlap on the same key, that is member three’s OCC path, not this one.

Generalize it

None of this is specific to a memory loop. Any read-modify-write on shared state has these five failure modes waiting. A shared plan, a task queue, a scratchpad two agents both edit, a langgraph store two nodes both touch. The routing, compaction, and expiry members are your code putting the write in the wrong place, a summarizer eating it, or a clock aging it out — a durability receipt and a retention policy are their levers. Members three and four are two actors colliding on one key or one actor writing from a stale view — the coordination primitive is theirs.

There is also a fourth move worth naming: you can design the races away instead of coordinating them. Serialize writers per key so no two ever hold the same record at once, or keep the store append-only so a reader always sees a whole version and nothing is ever edited in place. If your architecture genuinely does one of those, members three and four don’t exist for you by construction, and you don’t need a coordination primitive for them. The levers you still need are the receipt and the retention policy, because wrong-key writes, lossy compaction, and TTL expiry survive any concurrency design. Avoid beats coordinate wherever the workflow allows it. The coordination primitive is for the shapes where it doesn’t — shared mutable state that multiple actors genuinely edit in place.

The shipped surfaces cover exactly one of the three levers, and I want to keep that honest: the coordination primitive. Version-CAS (commit_cas / write_cas) and the read-generation fence stop the concurrent lost-update. MESI invalidation-deny plus reacquire stops the sequential stale-read-then-write. These ship in the coordinator, in the CCSStore drop-in for langgraph.store, in CoherentVolume, and in the Claude Code plugin, and the shipped receipt covers what each one checks and which TLA+ invariant proves it. They also ride the store you already run, a Postgres row or an S3 object, rather than requiring a migration. All of it prevents on a single host. Cross-host fencing is not shipped, so if your two writers are on different machines, members three and four are still open and a trace is what you have. The durability receipt and the retention policy are not the coordinator’s to ship, on any number of hosts — they belong to your pipeline, your consolidation step, and your retention window. A trace can carry the read-basis version and show you member three too, but seeing a race is not stopping one. For the races, prevention is the version on the write, and only there.

The reader’s diagnostic

A learning vanished and every write logged success. Ask, in order:

  1. Was it read from a different key than it was written to? Key mismatch. A read/write trace catches it; a durability receipt with a read-back prevents the next one.
  2. Did a compaction run between the write and the missing read? Compaction drop. The trace shows the loss at the boundary; a retention policy on the consolidation step prevents the next one.
  3. Did a TTL or retention window pass between the write and the missing read? Expiry. A read-side check that treats a miss as a fault catches it; a retention policy that pins or renews on access prevents the next one.
  4. Were two sessions writing the same key at once? Concurrent lost-update. Prevent it with a version-CAS.
  5. Did a peer commit a newer version between your read and your write? Stale-read then write. Prevent it with invalidation-deny plus reacquire.

Five questions, in that order, and you have named your bug, its lever, and its owner.

Why it matters

Every one of these debugs, by default, as “the model forgot.” The learning is gone, the output is worse, and the reflex is to reach for the prompt. You add a line telling the agent to remember harder. You raise the temperature or lower it. You rewrite the memory instructions. None of it touches the write path, because the write path was never the suspect. The vanish gets blamed on the model’s attention and buried in prompt tweaks, and it comes back on the next run because the actual cause was a routing key, a compaction pass, or a race on a version.

Naming the member is how you stop reaching for the prompt. Once you can say “this is a concurrent lost-update, not forgetting,” you know the fix is a version on the write, not a sentence in the system prompt. Once you can say “this is a compaction drop,” you know it’s a retention rule, not a memory-instructions rewrite.

One concrete next step: if your agents share a key and both write it, put the version on the write. Read a base version, submit it with write_cas, and handle the typed conflict. The write that used to vanish becomes a conflict you can see. And if the write is fine and the fact still vanishes, the missing lever is a receipt or a retention rule — those are yours, and now they have names.

Frequently asked

My agent wrote a fact and it disappeared. Why?

There are five distinct causes: the write went to a different key than the reader queries, a compaction pass dropped it, a TTL aged it out, a concurrent writer overwrote it, or the agent wrote from a view that had already gone stale. They need different fixes, so naming the member first is what stops you from tweaking the prompt.

What is the difference between a concurrent lost-update and a stale-read-then-write?

A concurrent lost-update is two writers on the same key at the same time, and version-CAS resolves it to exactly one winner with a typed conflict for the loser. A stale-read-then-write is sequential: a peer already committed a newer version, and the agent writes from a view that went stale underneath it. Invalidation-deny plus reacquire handles that one.

Can a coherence layer prevent all silent data loss?

No, and claiming otherwise flatters the coordination lever. It covers the two races, on a single host. Wrong-key writes need a durability receipt in your pipeline, and compaction drops and expiry need a retention policy in your store.

How do I tell which failure I hit?

Ask in order whether the fact was read from a different key than it was written to, whether a compaction ran between the write and the read, whether a TTL passed, whether two sessions wrote the same key at once, and whether a peer committed between your read and your write. The first yes names the member, its lever, and its owner.

Can I design these races away instead of adding a coordinator?

Yes, for two of the five. Serialize writers per key so no two ever hold the same record at once, or keep the store append-only so nothing is edited in place, and the two races stop existing by construction. Wrong-key writes, lossy compaction, and TTL expiry survive any concurrency design, so the receipt and the retention policy are still yours to build.