WizusLabs Engineering · Studio

When two agents edit one file, the last write wins

There is a seductive idea in AI-native development: if one agent can do the work, ten agents can do ten times the work. Fan the tasks out and watch the throughput climb. It does climb — right up until two of them reach for the same file, and one of their afternoons quietly disappears with no error, no warning, and no way to tell whose it was.

By WizusLabs Engineering · 2026-07-25 · ~7 min read

Running two agents against the same file at the same time does not double the work. It starts a silent argument about whose edit survives, and the loser is never told. There is no merge conflict, no red build, no exception in a log — the second agent simply writes back the file it read, on top of a version the first agent already changed, and the first agent’s work is gone as if it were never done. We learned this the way these things are always learned: a change was made, the tests came back green, and the change was not actually in the file.

The failure with no error message

Start with the mechanism, because the whole problem hides in one word: shared. When several dispatches run in parallel against a single checkout, they are not working on private copies that get reconciled later. They are all editing the same files on the same disk. Agent A reads game_bloc.dart at its current state and starts working. Agent B reads the same file, at the same state, and starts working too. A writes its version back. Then B writes its version back — the one built from the file before A touched it — and A’s edit is overwritten in place. Not conflicted. Not queued. Overwritten.

The reason this is so quiet is that a merge conflict needs two committed histories to disagree about. This is one working tree with no commit in between, so there is nothing for the tools to flag — the second write is just a normal save. The tell is perverse: the test suite goes green over a file that lost half of what it was supposed to contain, because the half that survived still compiles. A handful of files attract this every time, the ones almost every task wants to touch — the central game logic, the app root, main.dart, pubspec.yaml, the changelog. We call them hot files, and they are hot precisely because they sit where everyone’s work crosses.

Why parallelism looked free and wasn’t

The appeal of fanning work out is real, and it is worth being fair to it: when two tasks touch genuinely different files, running them at once is a clean win, and most of a sprint is exactly that kind of work. The trap is that a task list looks like a set of independent tickets when it is nothing of the sort. Two tickets can read as unrelated — “add the retry banner,” “wire the new sound cue” — and both still land in the same bloc, the same router, the same one-line-per-platform version file. The moment two units of work share a surface, they are not parallel; they only look parallel. The shared file is the serial part of the job wearing the costume of two separate jobs, and no amount of extra agents makes a serial part run in parallel. Throw more of them at it and you do not go faster — you just raise the odds that one of them is writing over another.

Two agents editing one file, unguarded versus guarded Two scenarios side by side. On the left, unguarded: Agent A and Agent B both write to the same file, game_bloc.dart, in one shared working tree; because the two writes race, only one of the two edits survives — the last write wins and the other change is silently lost. On the right, guarded: a file-ownership matrix assigns Agent A to file X and Agent B to a different file Y, so each file has exactly one owner; a post-dispatch grep for a unique marker verifies each change actually landed, and both edits survive. parallel, unguarded parallel, guarded Agent A Agent B game_bloc.dart one shared working tree last write wins 1 of 2 edits survives — the other is silently lost Agent A Agent B file X one owner file Y one owner grep marker ≥ 1 — verified ownership matrix + grep guard: both edits survive
The same two agents, two outcomes. Unguarded, two writes to one shared file race and only the last one survives. Guarded, a file-ownership matrix gives every hot file a single owner and a post-dispatch grep confirms the change actually landed.

The rule that fixed most of it: one owner per file

The fix that removes the hazard rather than patching around it is embarrassingly simple to state and requires real discipline to keep: at most one agent may touch a given hot file in a given parallel batch. If two tasks both need the central bloc, they do not run at the same time — they are sequenced, one after the other, even when that stings. We plan this before dispatching, not after something breaks: every parallel batch ships with a file-ownership matrix that names exactly which agent owns which file, and two tasks that want the same one are a scheduling problem to be solved on paper, not a race to be discovered in the tree.

The honest cost is throughput we leave on the table. Work that could have run four-wide sometimes runs two-wide, or in sequence, because the pieces share a file and we refuse to gamble on the write order. We take that trade every time. A batch that finishes an hour later but contains all of the work it claims to is strictly better than one that finishes on time and quietly dropped a change nobody will notice until it ships. Coordination is not the tax you pay to avoid the real work; on shared state, coordination is the real work.

Trust, but grep

Ownership prevents the collision; it does not prove the edit landed. So the second rule is verification, and it is as blunt as the first. Every dispatch that changes a file introduces a unique marker as part of its change — a new identifier, a distinctive string, the name of the function it added — and then greps the file for that marker and asserts at least one hit before it reports success. No marker, no landing: if the string the agent claims to have written is not in the file, the edit did not take, and it is re-applied and re-checked rather than declared done. It is the software equivalent of reading back the order.

At the end of a multi-dispatch sprint we run the whole set of those checks in one pass — a change-survival sweep — before anything reaches review. A missing marker at that stage is not a QA finding; it is a re-dispatch, done before the gate, not surfaced by it. This matters because of something we wrote about in an AI QA gate for games: a review gate can only judge the code it is given, and it cannot know that the file it is reading is missing a change someone believes they made. A green gate over the wrong file is worse than a red one, because it launders a lost edit into an approval. The sweep exists so the gate is always looking at the code that was actually written.

The recovery that makes it worse

There is a third way to lose work, and it is the cruelest, because it wears the face of a fix. When a dispatch hits a compile error or runs out of budget mid-task, the reflex — a very human reflex, and one an agent will happily imitate — is to “get back to clean” with git stash or git reset --hard. In a private checkout that is harmless. In a shared working tree it is a loaded gun, because those commands do not operate on your changes; they operate on the whole tree. Reset it and you throw away every uncommitted edit sitting there, including the work three sibling dispatches have not committed yet, and including whatever the human happened to have open. The blind cleanup is just a second overwrite, dressed as good hygiene.

So our rule is absolute and it points the other way: on any failure, do not tidy — report and stop, leaving the tree exactly as it is. “STOP: turn budget exceeded.” “BLOCKED: compile error at file:line.” Both of those leave every other dispatch’s work untouched, which is the only property that matters when you cannot see what else is in flight. An agent that stops with a mess intact is worth far more than one that hands you a clean tree with a hole in it, because you can always finish an honest job and you can never recover a silent one. Recovery that destroys is not recovery; it is the same failure with better manners.

The real fix was fewer shared trees

Every rule above is a guardrail on a hazard, and the best thing you can do with a hazard is delete it where you can. For us that meant a structural change, not a procedural one: we split what had been a single repository into independent repos, one per app, so that work on two different games now happens in two entirely separate working trees with separate histories. Last-write-wins cannot cross a boundary that does not share a file, so the whole class of cross-project collisions simply stopped being possible — no matrix required, because there is nothing to contend over. It is the same instinct as giving each agent its own checkout, or reaching for git’s own worktrees: the cheapest race to win is the one you make impossible to start.

Inside a single repo the contention is still real, and there the matrix and the grep guard earn their keep. But the arc is the same one that runs through most of how this studio works, which we sketched in building games with an AI agent crew: agents multiply output beautifully across work that does not touch, and catastrophically across work that does. So we spend our effort mapping the “does not touch” before we fan anything out, because parallelism is a coordination problem wearing a throughput costume. Two agents in one file is not twice the work — it is a coin flip about whose work survives, and the loser is never told. The rest of how we try to keep our releases honest lives on the WizusLabs Engineering blog.

Notes

This is a first-hand account of one studio’s conventions for running parallel AI-agent dispatches against a shared repository, not a universal standard. It contains no metrics — no count of lost edits, no failure rate, no time saved or spent — because we did not measure those things and inventing them would defeat the point of the piece. The underlying behaviours are general properties of the tools: that concurrent writes to one working tree resolve as last-write-wins, and that git stash and git reset --hard act on the entire working tree and can discard uncommitted work, are documented by Git and cited below; tooling behaviour can change, so verify against the current documentation before relying on specifics. The specific rules — one owner per hot file per batch, a post-dispatch grep for a unique marker, a change-survival sweep before the review gate, a no-stash recovery policy, and splitting into independent repos to isolate working trees — are our own working conventions, recorded in our engineering notes, not a feature of any tool.

Sources

The Git behaviours this post relies on are documented by the tool’s maintainers (living documents; accessed 2026-07-25):

  • Git — git-stash: that stashing records the state of the working directory and the index and then reverts the working directory to match HEAD, moving your local modifications onto a stack where a careless drop or clear loses them.
  • Git — git-reset: that --hard resets the index and the working tree so that any changes to tracked files in the working tree since the target commit are discarded.
  • Git — git-worktree: that a repository can have multiple working trees attached to it, allowing more than one branch to be checked out at once in isolated directories — the tooling analogue of giving each unit of work its own tree.

Keep reading: all posts on the WizusLabs Engineering blog.

← Back to the Blog