September 8, 2026 · 7 min read
What Happens When Two AI Agents Change the Same MongoDB Document?
A Python walkthrough of database branches, merge conflicts, and undo, using one order and two competing price changes.
Two agents start with a $49 order. The planner writes $44. The executor writes $1. Both writes are valid MongoDB updates. The planner's change is merged first, so the executor's proposal must be checked against a main branch that now contains $44.
This is the example we use to test Argon's review workflow. The agent actions are scripted, so you can reproduce it without a model or an API key. The database work is real: PyMongo writes to separate MongoDB databases, and Argon captures their history. The script issues the writes in sequence; it is not a concurrency benchmark.
The complete Python example includes setup, assertions, and cleanup. The excerpts below follow its execution order.
Start both runs from the same order
The script creates a project, inserts one document into a seed branch, and merges it into main:
{"_id": "order-1", "price": 49, "status": "pending"}It then pins that state as baseline and creates two sandboxes from it. A pin names a fixed point in the project's history. Both runs get the same input, even if main changes later.
pin = argon.create_pin(
project, "baseline",
note="Identical input for both agent runs",
)
planner = argon.sandbox_from_pin(
project, "baseline", name="planner", actor="agent:planner",
)
executor = argon.sandbox_from_pin(
project, "baseline", name="executor", actor="agent:executor",
)Each sandbox checks out as a physical MongoDB database. Creating the branch metadata is a small operation; preparing a database that a driver can query also takes copying time and storage. That cost grows with the data being materialized.
The two database handles below are ordinary PyMongo objects. Each update changes only its branch. After these calls, main still has 49.
a = planner.pymongo_database()
b = executor.pymongo_database()
a.orders.update_one(
{"_id": "order-1"}, {"$set": {"price": 44}},
)
b.orders.update_one(
{"_id": "order-1"}, {"$set": {"price": 1}},
)Accept the planner's change
We choose to accept 44 in this example. A merge preview produces a plan for bringing the planner's changes into main. Because main still matches the starting state, this plan has no conflicts.
approved = argon.merge_preview(project, planner.branch)
assert not approved["conflicts"]
argon.merge_apply(approved["id"])Applying that plan changes main to 44. Argon checks that the source and target still match the state used for the preview. If either has changed in the meantime, the caller needs a fresh preview before applying the plan.
The executor still has 1
Merging the planner does not rewrite the executor's sandbox. Its document still says 1, based on the original 49. When it asks to merge, Argon now has three states to compare:
| Common starting point | Main now | Executor proposal |
|---|---|---|
| 49 | 44 | 1 |
Main and the executor have both changed the same document since that starting point. The preview reports one conflict.
rejected = argon.merge_preview(project, executor.branch)
assert len(rejected["conflicts"]) == 1The script stops short of applying this plan. Main stays at 44. A reviewer could resolve the conflict, ask for another attempt, or discard the branch. Here, we use it to test undo.
The conflict is about the competing edits. Argon has no pricing rule that rejects a $1 order. If main had stayed at 49, the executor's change would not conflict with it. An application still needs its own checks for acceptable prices before approving a merge.
Undo the attempt, then undo the accepted merge
First, we undo the executor's write inside its own branch. The captured entry has the actor label agent:executor and a log sequence number, or LSN. These identify the history to undo.
executor_writes = [
e["lsn"] for e in argon.entries(project, executor.branch)
if e.get("actor") == "agent:executor"
and e["operation"] == "put"
]
argon.undo(
project, executor.branch,
from_lsn=min(executor_writes),
actor="agent:executor",
)
assert b.orders.find_one()["price"] == 49The executor is back at 49. Main remains at 44. The actor label belongs to this branch's run; MongoDB change streams do not tell Argon which individual application user made every write. Separate runs need separate branches if you want to review them independently.
The script discards the executor branch and checks that a fresh sandbox from main reads 44. It then tests a different operation: undoing the accepted merge on main itself. That merge wrote its own history entries under merge:planner.
merge_writes = [
e["lsn"] for e in argon.entries(project, "main")
if e.get("actor") == "merge:planner"
and e["operation"] == "put"
]
argon.undo(
project, "main",
from_lsn=min(merge_writes),
to_lsn=max(merge_writes),
)A new sandbox from main now reads 49. These were two separate undo operations against two different histories. Reverting the executor did not cancel the planner's merge.
Undo needs the complete, retained history and the document images for the selected writes. Missing images or conflicting later edits can prevent it from proceeding. In this example, those conditions are controlled and the script checks the data after each operation. For an existing deployment, the capture and retention requirements are part of setting this up.
What changed in Argon 2.1
Branches, merge previews, and undo were already in Argon. The recent work tightened the behavior this example depends on: preserving BSON document identities, capturing exact before-and-after images, and applying merge and undo changes transactionally. Stale plans and incomplete undo histories must fail before they can partially change the data.
Capture now has an explicit readiness check and reports degraded history when it cannot record an update correctly. Native-driver tests exercise PyMongo and Mongoose writes through capture and undo. The 2.1 release notes link each change to its regression coverage. Version 2.1.1 also fixes a monitor shutdown deadlock found during the release checks.
We ran the full example with the published Argon 2.1.1 CLI, SDK 0.2.0, and a fresh MongoDB 7.0.25 replica set. It accepted 44, reported one conflict, and restored 49.
Run the same example
The local Quickstart installs the CLI, starts a MongoDB replica set, and runs argon doctor to check capture readiness. Keep argon console --no-browser running while the example runs; that process manages capture for the API sandboxes. The guide also checks out SDK tag v0.2.0 and installs it in a Python virtual environment.
From that SDK checkout, with the virtual environment active and the local console listening on port 1818, run:
export ARGON_API_URL='http://127.0.0.1:1818'
python examples/two_agent_review.pyThe output includes these fields. The generated project name and pin LSN vary between runs.
{
"reviewed_price": 44,
"conflicts": 1,
"restored_price": 49
}The source includes the assertions behind those numbers. You can also inspect proposals in the live demo, which uses temporary sample data. Use the local setup for this Python example; the anonymous demo does not issue native MongoDB credentials.
Argon is open source and MIT-licensed.