Concurrency Control for Multi-Agent Shared Memory: Versioned Commits, Semantic Merging, and Conflict Replay
When multiple agents concurrently modify shared plans, preferences, or conclusions, simple last-write-wins handling loses facts. This article uses versioned commands, per-key ordering, and field-level merge policies to control concurrency.
Key Points
- Topic Focus: AI、AI Agent、DevOps
- Suitable for: indie hackers, full-stack engineers, AI application developers
- Core value: quickly understand background, practical paths, common pitfalls, and engineering advice
While a research agent writes a conclusion to a shared blackboard, a review agent may revoke the same conclusion; two execution agents may also make different plan changes based on version 7. If the storage layer merely runs UPDATE memory SET value = ?, whichever operation finishes later silently overwrites the earlier one. Multi-Agent Communication Design notes that shared state must be serialized; this article turns that requirement into a replayable memory commit protocol.
Clients Submit Commands, Not Entire Snapshots
A read response returns the memoryId, version, and field versions. A write must include an expectedVersion, a stable commandId, the version on which its patch was based, and the agent's identity. An HTTP interface can express the precondition with ETag/If-Match; an internal queue must preserve equivalent semantics rather than dropping the version merely because the transport layer changed.
{
"commandId": "run-82:step-4:memory-write",
"memoryId": "task-19",
"expectedVersion": 7,
"op": "replace",
"path": "/decision/deployRegion",
"value": "ap-southeast-1",
"evidenceIds": ["tool-551"],
"actor": "planner"
}
Retries reuse the same commandId. The server enforces uniqueness on (tenant_id, command_id): the same ID with the same hash returns the original result, while the same ID with a different payload is marked as a protocol conflict. Generating a new random ID for every retry bypasses deduplication; the step identity from Long-Running Agent Workflows must carry through to this layer.
Use CAS to Turn Lost Updates into Explicit Conflicts
The transaction first writes the command deduplication record, then updates the projection with a version condition. After success, it appends an immutable event and outbox entry. If the affected row count is zero, it returns the current version and conflicting paths instead of automatically reading again and overwriting the value.
UPDATE shared_memory
SET value_json = apply_checked_patch(value_json, $patch),
version = version + 1
WHERE tenant_id = $tenant
AND id = $id
AND version = $expected;
-- row_count = 0 => PRECONDITION_FAILED
Under PostgreSQL 18's default Read Committed isolation, an ordinary read followed by a write may still make decisions from a stale snapshot; the version predicate is the business precondition. When conflicts on hot keys are frequent, a single writer partitioned by (tenant,memoryId) should order commands instead of raising transaction isolation without limit and making every key wait on the others. The PostgreSQL 18 documentation used by this article was accessed on 2026-07-18; rerun the isolation tests if the target version changes.
Merge Rules Must Follow Field Semantics
After CAS fails, do not let a model freely "synthesize" two JSON documents. Different fields use explicit algebra: set-valued tags use union by element ID, and append-only evidence merges by unique ID. Single-valued decisions, permissions, state transitions, and user preferences cannot be merged automatically. Counters must first distinguish operations from state; ordinary addition cannot be mislabeled as an idempotent merge.
/evidence/* append_unique(evidenceId)
/labels/* set_union(labelId)
/progress/completed monotonic_true
/jobsStarted increment_once(commandId, delta)
/tokenCountByActor gcounter_join(max_per_actor)
/decision/* reject_concurrent
/permissions/* reject_concurrent
/status validate_state_transition
increment_once is an operation: the delta for the same commandId is applied only once, while two different commands are added together; addition itself does not satisfy merge(a,a)=a. When state-based merging is required, use a G-Counter: each actor increments only its own component, merging takes the max of each actor's component, and the components are summed only at the end. Only a state join must be commutative, associative, and idempotent; an operation instead requires a stable ID, deduplication, and one logical effect. Optimistic vs. Pessimistic Locking explains the database mechanisms; the key here is field semantics.
Validate with Concurrency Barriers and Event Replay
Have 100 workers write the same single-valued field based on version 7 after the same barrier, and assert exactly one commit, 99 explicit conflicts, and no lost events. Replaying the same increment command ten times must increment only once, while two different commands must increment twice. Repeatedly merging a G-Counter snapshot from the same actor takes the maximum rather than the sum, and different orderings produce the same state. Then inject a lost response after transaction commit, outbox redelivery, worker lease expiration, and process crashes, and assert that the command has only one logical effect.
Independently rebuild the projection from event zero, and compare the version, value hash, conflict set, and final sequence. Comparing only the final JSON is insufficient because an overwritten value that happens to be identical may still mean audit history was lost. Stress tests using the same memoryId across tenants must never expose data between tenants.
During rollout, first record potential conflicts while retaining the legacy single-writer path, and measure hot keys and the legitimate retry rate; then enable CAS by tenant. Monitor precondition failures, automatic merges, human-resolved conflicts, exhausted retries, and replay differences. Stop the rollout after even one incorrect automatic merge of a permission or decision field.
For rollback, switch the command entry point back to read-only mode or the legacy single writer, while retaining the new events and command deduplication table. Never disable the expectedVersion check to "restore the success rate." After fixing the issue, replay from the last trusted sequence. Event-Driven Architecture can carry delivery, but whether shared memory is correct ultimately depends on whether its version conditions and field semantics can be proven.
Further Reading
Related
Related Content
Article
What Is Vibe Coding? Will It Change How Programmers Work?
Technical trends, engineering practices, and an indie developer perspective on Vibe Coding, AI, and independent development.
Article
From Copilot to Codex: AI Coding Tools Enter the Agent Era
Technical trends, engineering practices, and indie-developer perspectives around AI, AI Agents, and Vibe Coding.
Article
Why Does MCP Matter? Understanding the AI Tool Protocol
Technical trends, engineering practices, and an independent developer perspective on MCP and AI Agents.
Article
Core Architecture of AI Agents: Planning, Memory, Tools, and Execution
Technical trends, engineering practices, and independent developer perspectives around AI Agents and large language models.