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 five members across three prevention levers: a durability receipt for the wrong-key write, a retention policy for the compaction drop and the TTL expiry, and a coordination primitive for the two coherence races. The first two levers live in your pipeline. The third belongs to the runtime.
The runtime’s lever 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 coordination lever 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 this lever’s to stop and never will be. Wrote X, read Y wants a durability receipt in your pipeline. A summarizer eating a fact wants a retention policy on the consolidation step. A trace catches both so you can see them, but those two levers are yours to build, not the runtime’s to ship. 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 runtime’s lever of the three, 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.
If the state your agents fight over lives in Postgres or S3 rather than in this library’s own surfaces, the same guarantees now ride the store you already run.
Frequently asked
How do I stop two agents overwriting the same key?
Submit the base version you read along with the write. `write_cas` commits only if the key is still at that version, and the loser gets a typed retryable conflict rather than silently overwriting. On a single host this is prevented, not merely detected.
Why doesn't a version check catch a late write from a crashed agent?
If nothing else moved the value while the writer was stalled, the version still matches and the check passes. The read-generation fence also asks whether the grant the write was made under is still valid, and rejects it with a typed `stale_read_generation` when it is not.
Does this work across multiple machines?
No. Production cross-host fencing has not shipped. There is a demo-grade, default-off transport that shows the shape of it, but two writers on different machines are not covered by a shipped guarantee.
What does TLA+ actually prove here?
Five invariants run under the TLC model checker on every push and pull request, and each ships with a documented mutant, a deliberately broken spec the checker is confirmed to reject. It checks the model of the protocol under bounded exploration, not the whole running system.