# You Don't Need a New Store. Put the Version on the Write.

> When an update vanishes the reflex is a store migration. The store was never the problem. The fix is the version on the write, over the store you already run.

Published: 2026-07-27 · Canonical: https://agent-coherence.dev/blog/put-the-version-on-the-write/

---
Losing an agent's write makes teams want a better store. The store is almost never what failed.

Here is the meeting where the decision usually happens. Last week an enrichment agent and a triage agent touched the same account record in overlapping windows, [the scene from the first post in this series](/blog/write-side-half/). The triage agent wrote a churn flag from a cancellation email. The enrichment agent finished its pass and wrote back the full record it had derived ten minutes earlier, and the churn flag was gone. Two successful writes in the log, one update missing, a duplicate outreach already queued to a customer who canceled. The postmortem lands one action item: evaluate a new memory store.

I want to talk you out of that action item, or at least make it much smaller.

## Would a better store have saved the update?

Walk the failure again from the store's side. The triage agent's write was correct for the version it read. So was the enrichment agent's. The store received two well-formed writes and applied both, durably and in order. A store with stronger consistency guarantees would have applied the second write just as faithfully, because nothing in any storage contract was violated. The loss lives in the interleaving, and no store call can see the interleaving. That is why swapping the store does not fix it, and why [the taxonomy in this series](/blog/silent-data-loss-taxonomy/) files this failure under coordination, not storage.

Here is what changes: put the version on the write.

The writer reads a base version, does its work, and submits that version with its write. The commit lands only if the key is still at that version. The loser does not overwrite anything. It gets a typed, retryable conflict, re-reads, and re-derives from current state. Both updates survive. In a coding fleet the same check is what stops an agent writing back over a plan a second agent already moved, where last write wins and the run looks green.

One precision for the distributed-systems reader, because it bounds every claim in this post: a compare-and-set needs one linearizable point where the compare happens, and that is why the shipped guarantee is scoped to a single host with a single coordinator.

## Your store already does half of this

Postgres ships `UPDATE ... WHERE version = ?`. S3 ships `If-Match`. DynamoDB ships condition expressions. Used well, the store's own conditional write already rejects a lost update at the moment you write, on the key you write. If you are using it today, keep using it. The [substrate bindings](/blog/shipped-byo-substrate/) in this project ride those native checks rather than replacing them, and the coordinator holds only a version, ownership state, and a content hash, never your bytes. That part of the problem was solved before agents arrived, by the store you already run.

## What can't a conditional write do?

It cannot say anything to a reader.

A conditional write tells a writer it lost, at commit, on the key it writes. The common worst case in an agent fleet involves no second write to that key at all. An agent reads the plan, then spends minutes producing other artifacts informed by it, a summary, a config change, a tool call. It never writes the plan. No version check fires anywhere, and the stale read contaminates every downstream artifact it produced, fluently, with no error to point at.

That is the read-side half, and it takes a different mechanism: invalidation. When a peer commits, every cached holder of the old version is marked stale, and the stale agent fails fast at its next touch, denied with a typed reason, instead of finishing confidently on the wrong version. Recovery is explicit, reacquire and take a fresh read. With agent writers there is an economic edge too: every retry is a re-generation, minutes of latency and real tokens, so learning you lost ten seconds in beats discovering it after the full pass.

The split, side by side:

| The store's job, and it does this well | The coordination layer's job |
|---|---|
| apply every write durably, in order | refuse a write built on a stale read |
| serve consistent reads of what was stored | tell a cached reader its view died before it acts on it |
| reject a conditional write on the key you write | resolve concurrent writers to one winner, typed conflict for the loser |
| stay the system of record and keep the bytes | hold only version, ownership, and a content hash |

They stack. Neither replaces the other, and the [shipped receipt](/blog/shipped-write-side/) covers what each mechanism on the right checks and which TLA+ invariant proves it holds.

## When you should not add any of this

If two agents share a key by accident, partition it. Single-writer-by-design is the first thing to recommend, it costs nothing, and the check above is idle on partitioned keys. It is also worth naming what partitioning is: a coherence protocol, the degenerate one where ownership never moves. The first handoff breaks it. A reviewer takes over the plan, or agent B resumes agent A's work, and the team starts hand-rolling invalidate-on-transfer in retry loops and prompt instructions. The realistic alternative to a named protocol was never no protocol. It was an unnamed one, smeared across the codebase, and naming it is what makes it small enough to check. The whole machinery here is a version, four ownership states, and one local coordinator process, with the model checking in CI rather than in the request path.

Cognee made a version of this argument for the storage layer itself in "Just Postgres: Drop the Graph Database. Keep the Graph.", and it is the right instinct: keep the store you already run and add the capability, instead of migrating to new infrastructure. This post is the same move one layer up. The fix for the vanished update is not a migration. It is a version on the write, over whatever you already run, a Postgres row, an S3 object, a file on disk, a `langgraph.store` key.

## Scope, stated plainly

Single host, one coordinator, writers that go through it. Concurrent same-key writers on one host are covered. Production cross-host fencing is not shipped, and across hosts the honest recommendation today is the one your store already gives you, its own conditional write. One boundary no layer can cross: a writer that ignores the bytes its read returned defeats any version check, so the contract is write from what `read()` gave you. And one edge left genuinely open for the distributed-systems readers: the cross-agent invalidation half across hosts is unsolved in the agent tooling layer as far as I have seen. If you are handling it in production, I would like to hear how.

## The afternoon versus the quarter

A store migration is a quarter: schema mapping, dual writes, backfill, a long tail of readers to move. The version on the write is an afternoon, and it removes the failure the migration was reaching for. If two of your agents write the same key, the smallest useful move is: read a base version, submit it with `write_cas`, and handle the typed conflict you get back. If the update still vanishes after that, the cause is on the detect side of [the taxonomy](/blog/silent-data-loss-taxonomy/), a routing key, a compaction pass, a TTL, and those levers are yours, with names.

The repo, with deterministic offline reproductions of both halves, is at [github.com/Cohexa-ai/agent-coherence](https://github.com/Cohexa-ai/agent-coherence). And if you evaluated a store migration for exactly this reason and it did or did not fix it, that is the postmortem I want to compare notes on.
