DEV Community

Cover image for Two AI agents, one Postgres row: the bug your version check won't catch
Vlad
Vlad

Posted on

Two AI agents, one Postgres row: the bug your version check won't catch

If you have two AI agents writing the same Postgres row, you have probably already added a conditional write:

UPDATE profiles SET value = $1, version = version + 1
WHERE id = $2 AND version = $3
Enter fullscreen mode Exit fullscreen mode

When a peer moved the version first, you get rowcount = 0 and reject the write. That is real, it works, and you should keep it. It closes half the problem.

This post is about the other half, which is quieter and shows up as a bug you will blame on the model.

The failure

Here is the timeline that bit me.

  1. Agent A reads the row at version 7 and starts a multi-step edit. Call it two minutes of reasoning.
  2. Agent B commits version 8.
  3. A finally writes. The conditional write comes back rowcount = 0 and A's write is rejected.

The row is fine. That is exactly what the CAS is for.

But look at what happened in step 2. For those two minutes, A was deriving work from a value that was already dead. The plan it wrote, the summary it saved, the tool call it fired. None of that went through the row's CAS, so none of it was rejected. It all landed normally.

The row survived. The work is contaminated.

A version check protects the write. It does not protect the reader.

This is specific to agents in a way it is not for ordinary CRUD. A normal service reads and writes in the same breath, so the window is milliseconds. An agent reads, reasons for a long time, then acts. The window is the whole reasoning pass, and everything it emits in that window escapes before the CAS ever fires.

Reproduce it

The demos are offline and need no credentials, so you can watch this happen in about a minute:

git clone https://github.com/Cohexa-ai/agent-coherence
cd agent-coherence
pip install -e ".[coherent-row]"

python -m examples.coherent_row.main --baseline
Enter fullscreen mode Exit fullscreen mode

The --baseline arm runs the un-coordinated agent first, so you see it act on its stale cache, and then runs the guarded version. The deny gets measured against its absence rather than just asserted at you. Exit code is 0 only if the contract held.

The fix, in code

The piece that was missing is telling A that its cached view died, before A acts. That is what the binding adds. Condensed from examples/coherent_row/main.py:

from ccs.adapters.coherent_row import CoherentRow
from ccs.adapters.substrate import CoordinatedSubstrate, SubstrateCoordinatorSession
from ccs.core.exceptions import StaleView

session = SubstrateCoordinatorSession(root, managed=("**",))
agent_a = CoordinatedSubstrate(CoherentRow("profiles", dsn=DSN), session)

value, token = agent_a.read("user-42")
# ... A reasons for a while. Meanwhile B commits a new version ...

try:
    agent_a.commit("user-42", expected_token=token, new_bytes=edited)
except StaleView:
    fresh, fresh_token = agent_a.reacquire("user-42")
    agent_a.commit("user-42", expected_token=fresh_token, new_bytes=redecide(fresh))
Enter fullscreen mode Exit fullscreen mode

The detail I care most about is what does not happen. When StaleView is raised, the write never reaches Postgres. The binding denies it at the coordinator before the substrate is touched, and the demo asserts exactly that by checking the CAS was never called. You are not catching a rejection after the fact. You are stopping the act.

Recovery is one verb, reacquire(), and it is the same verb whether the state is a row, an S3 object, or a file on disk. CoherentObject gives you the identical story against S3 with If-Match underneath.

Your bytes stay in your database

The thing that would make me suspicious of a library like this is whether it quietly becomes a second store. It does not, and the design makes that checkable.

The coordinator holds a monotonic version, per-agent state, and a fixed-width content hash. The binding declares SENDS_CONTENT_TO_COORDINATOR = False, the conformance kit asserts it, and composition refuses any binding that sets it True. The row body never leaves Postgres. Your database keeps its backups, permissions, and durability story, and stays the system of record. Remove the library tomorrow and your data is exactly where it always was.

Nobody should migrate production state to get a correctness guarantee.

When you do not need this

Worth saying plainly, because it is a real answer for some of you.

If you wrap every read and write in a pg_advisory_lock and never cache a read between them, the bare CAS is enough and you can stop here. The binding is for the agent that reads, reasons, then acts. If your window between read and write is short and you never derive anything inside it, you do not have this problem.

Honest scope

The limits matter more than the pitch, so here they are.

  • Single host only. Two writers on two machines are not covered by a shipped guarantee. The cross-host transport is a demo and is not production fencing.
  • The bindings are cooperative. A writer that goes straight to Postgres without the binding is not caught at write time. The coordinator notices the divergence on the next mediated read from the content hash, and detection after the fact is not prevention.
  • The demo models the row in memory so it runs offline. The real Postgres CAS is exercised against actual Postgres in the conformance kit's real_substrate arm. In production you point the same binding at a real DSN.
  • No read-generation fence in this binding. The v1 writers ride admit-on-absent plus the version CAS. The binding surfaces invalidation. The fence is a documented roadmap item, not shipped behaviour here.
  • It does not make a weak substrate strong. Each binding declares a capability tier derived from what the substrate can actually enforce, and the conformance kit checks the declared tier against observed behaviour.

Try it

pip install "agent-coherence[coherent-row]"    # Postgres
pip install "agent-coherence[coherent-object]" # S3
Enter fullscreen mode Exit fullscreen mode

Repo: https://github.com/Cohexa-ai/agent-coherence
Full write-up: https://agent-coherence.dev/blog/shipped-byo-substrate/

If you have hit the read-reason-act window in your own system, I would like to hear how it surfaced for you. It almost never gets reported as a concurrency bug.

Top comments (0)