Shipped: a version check and a read fence for agents that share state
The first post in this series told a small story with a bad ending. A self-learning agent read its memory, distilled a new lesson, and wrote it back. A second session did the same thing at the same time, over the same window. One of those two writes landed. The other one vanished. No error, no log line, no torn file. The agent just kept going as if the lesson had never happened. We named that ending the silent data loss, and then the taxonomy post split it into four members: two that the app has to detect, and two that are coherence races the runtime can prevent.
The prevent side is now live. Members three and four have shipped. This post is the receipt.
The two members that just shipped
Go back to the scenario. Two sessions share one memory. Both read version 7. Both distill. Both write. In the first post, the loser of that race disappeared without a trace. That was member three, the concurrent lost update. Member four is its quieter cousin: one session reads version 7, a peer commits version 8, and then the first session writes on top of its now-stale view. It never saw the newer value. It overwrote it.
Both of those endings are gone on a single host. Here is what replaced them.
Version-CAS stops the concurrent lost update. When a writer commits, it hands back the base version it read. The commit lands only if the key is still at that version. If a peer got there first, the loser does not overwrite anything. It gets a typed, retryable conflict back, version_mismatch or other_holder, and it can re-read and try again. The commit_cas and write_cas calls carry this. The write that used to vanish now bounces, visibly, with a reason attached.
MESI invalidation-deny stops the sequential stale-read-then-write. When a peer commits a key, every session holding a SHARED cached view of it has that view marked INVALID. The next write from that stale view is denied, fail-closed, before it can land. Recovery is explicit: call reacquire(), take a fresh read, then write against what is actually there. No write built on a value that has already moved out from under you.
There is a third mechanism underneath both of those, and it earns its own line because a plain version check cannot see the case it covers.
The read-generation fence closes the reclaim-zombie window. Picture an owner that stalls. The coordinator reclaims its grant and hands the key to someone else. Then the stalled owner wakes up and finishes its write. The version number alone might still look plausible to it, but the grant it was writing under is dead. The fence catches exactly that: a write carrying a stale generation is rejected at commit with a typed stale_read_generation. That is the difference between a version check and a fence. The version check asks “is the value still the one I read.” The fence also asks “is the grant I read it under still mine.”
What each guarantee actually covers
Here is the whole thing on one screen. Each shipped guarantee, the member of the silent-data-loss class it closes, and the machine-checked invariant that proves it holds.
| Shipped guarantee | Stops | Machine-checked by |
|---|---|---|
Version-CAS (commit_cas / write_cas) |
Member 3: concurrent lost update | NoLostUpdate |
| Read-generation fence | Reclaim-zombie late write | NoStaleApply |
MESI invalidation-deny + reacquire() |
Member 4: sequential stale-read-then-write | SingleWriter |
Durable version retention + read_at_version |
Losing the version an agent actually read | NoCollectedRead |
| Monotonic versioning | Versions going backward | MonotonicVersion |
Those five invariants are not aspirations in a design doc. They live in TLA+, and make tla-check runs them under the TLC model checker on every push and every pull request. Each one also ships with a documented mutant recipe: a deliberately broken version of the spec that the checker is confirmed to reject with a counterexample trace. If a mechanism stops holding, the model check goes red. That is the standard the prevent side is held to.
Two more pieces round out the surface. Detection under the hood does not trust file mtime. The coordinator tracks a version, a SHA-256 content hash, and per-session MESI state for every managed file, so an out-of-band edit to a tracked file is caught by a content-hash mismatch on the next read, not by a timestamp you can forge or lose. And version retention is opt-in and bounded: you keep a window of committed history, and you can ask what version a given agent read, which is what makes an after-the-fact “why did this write bounce” answerable.
The scope, stated plainly
A shipped post is exactly the place where cross-host overclaim gets tempting, so here is the fence around the fence.
Everything above is single-host. Members three and four are prevented on one host. Member four is the sequential case only. The concurrent same-key race is what version-CAS handles. If your two writers sit on the same machine, sharing one coordinator, you are covered.
If they sit on two machines, they are not. Production cross-host fencing has not shipped. There is a demo-grade, default-off single-coordinator transport you can point across hosts to see the shape of it, but it is a demo, not a production guarantee. Two writers on different machines are back to open members three and four, and what you have is a trace, not a prevented conflict. That is the honest line.
Members one and two, the key mismatch and the compaction drop from the taxonomy post, are not prevented here and never will be by the coordinator. Wrote X, read Y is an application key-discipline problem. A summarizer eating a fact is a consolidation problem. A trace catches both so you can see them, but preventing them is your job, not the runtime’s. And a few things people ask about are simply not in this release: no snapshot sessions, no multi-artifact transactions, no claims index or semantic-constraint enforcement, no advisory-versus-enforce toggle, no source-watcher. When they ship, they will get their own receipt.
Why it matters
Go back one last time to the write that vanished in the first post. Same two sessions, same version 7, same collision. Today that collision produces a typed conflict with a reason on it. The loser gets version_mismatch back and re-reads. The stale writer gets denied and calls reacquire(). The zombie gets fenced out at commit. The lesson that used to disappear now either lands cleanly or comes back as something you can see, catch, and retry. A silent drop became a loud, typed, recoverable event. That is the entire point of the prevent side, and it is real now.
Get it running
The guarantees ship everywhere the coordinator does: the CCSStore drop-in for langgraph.store, CoherentVolume for plain files on disk, the Claude Code plugin’s PreToolUse and PostToolUse hooks, and the stale-write-guard-fs MCP server.
pip install agent-coherence
Drop CCSStore in where your LangGraph store goes and your concurrent writes start bouncing instead of vanishing. Or run the two-act demo to watch a lost update turn into a typed conflict and a stale read get denied, side by side, in one script. Either way, start on one host. That is where the fix is real.