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

> The vanished write is not one bug. It is a class with four members: key mismatch, compaction drop, concurrent lost-update, and stale-read then write. Two you detect, two you prevent at the write.

Published: 2026-07-05 · Author: Vladyslav Parakhin · Canonical: https://agent-coherence.dev/blog/silent-data-loss-taxonomy/

---
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 four 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. You find these by looking at a trace. They are the application's job.

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. You stop these at write time.

Detect-side versus prevent-side. That is the spine. Two members you detect after the fact and fix in your own code. Two members you prevent at the moment of the write. Every "vanished learning" lands in one of the four, and knowing which one tells you where to look and whether the fix already exists.

## The four members

| Member | What you see | Cause | Prevented or detected |
|---|---|---|---|
| Key mismatch | Clean write to X, reader queries Y, fact never appears | App routing: writer and reader disagree on key/namespace | Detected (read/write trace: "wrote X, never read X") |
| Compaction drop | Fact goes in, then disappears at a summarization boundary | Lossy consolidation step overwrites or drops it | Detected (trace shows loss at the compaction tick) |
| Concurrent lost-update | Two writers, same key, same base version, one silently overwrites the other | Read-modify-write race on shared state | Prevented on one host by version-CAS (`write_cas`) |
| Stale-read then write | Peer commits a newer version, agent writes from its old view | Sequential: agent's view went stale before its write | Prevented on one host by MESI invalidation-deny plus reacquire |

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. What catches this is a trace that carries both the write key and every read key: you see "wrote `memory/session-42/lessons`, never read `memory/session-42/lessons`" and the mismatch is right there. The fix is key discipline in your own code. 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, which points you straight at the consolidation logic. That is where the bug lives and that is where you fix it. The coordinator never sees a compaction step eat a fact.

**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 four failure modes waiting. A shared plan, a task queue, a scratchpad two agents both edit, a langgraph store two nodes both touch. Members one and two are your code putting the write in the wrong place or a later step eating it, and you find them by tracing. Members three and four are two actors colliding on one key or one actor writing from a stale view, and you prevent them at write time.

The shipped surfaces that do the preventing are narrow and I want to keep them honest. 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. 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. Members one and two are never the coordinator's to fix, on any number of hosts. A trace can carry the read-basis version and show you member three too, but seeing a race is not stopping one. 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. Fix your key discipline. A read/write trace catches it.
2. Did a compaction run between the write and the missing read? Compaction drop. Fix your consolidation step. A trace catches it.
3. Were two sessions writing the same key at once? Concurrent lost-update. Prevent it with a version-CAS.
4. Did a peer commit a newer version between your read and your write? Stale-read then write. Prevent it with invalidation-deny plus reacquire.

Four questions, in that order, and you have named your bug and pointed at 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.

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 the learning your agent worked to distill actually survives to the next run.
