Magicborn Studios
← Devlog

We stopped preventing merge conflicts between our agents and started merging them

When two coding agents write the same function, the usual fix is to keep them apart. We let them collide, then synthesize one version from both. Here's the CRDT, the numbers, and where it breaks.

  • Agents
  • Rust
  • CRDT
  • Tooling

The standard way to run several coding agents at once is to keep them out of each other's way. You partition the work by file, hand each agent a lane, and pray nobody needs to touch a shared module. It works, in the sense that a small enough box always works. Then the swarm grows, the lanes get thinner, and you spend more time partitioning than building — because the honest truth is that real features are not file-shaped. Two agents should both be editing that function. Your architecture is what's stopping them.

So we stopped preventing the collisions. Two agents editing the same region is now the expected case, and when it happens a conductor merges both versions into one better version by calling a model to synthesize them. The operator calls this a weld, and it is the coordination layer for every swarm we run.

Convergence is not correctness

The first instinct is a CRDT, and we have one — crates/xp-algorithms/src/crdt/crdt_file.rs, a Fugue-style CmRDT. A file is a BTreeMap<FuguePos, CharRun>. FuguePos is a deterministic four-tuple:

(left_agent, left_clock, insert_agent, insert_clock)

That gives every character run a total order that any replica can compute independently, which buys the property you actually want from a text CRDT: maximal non-interleaving. Two agents inserting at the same spot land adjacent to each other, as two blocks — not shredded together character by character. Deletes are tombstones, snapshot() concatenates the live runs in order, and the module's doc comment states the guarantee plainly:

Two replicas that have received identical op sets will produce the same snapshot() string. This is Strong Eventual Consistency.

It's real and it's tested — commutativity, idempotence, two-replica convergence, tombstone GC. And it is not sufficient, which is the whole point of this post.

Picture the actual failure. Agent A writes parse_config. Agent B, at the same moment, writes a different, also-correct parse_config. The CRDT converges beautifully: every replica agrees, deterministically, on a file that now contains parse_config twice. Run rustc on it and you get E0428, duplicate definition.

Both inputs were valid. The merge was mathematically perfect. The output doesn't compile.

A CRDT guarantees that everyone ends up with the same file. It has nothing to say about whether that file is any good. Nobody's merge algorithm does — that's why git hands you conflict markers and walks away. The gap between "converged" and "correct" is exactly where a model belongs, and welding is what we put in it.

Detecting a collision worth paying for

semantic.rs::detect_conflicts() runs two heuristics over the merged op set.

Layer 1, same position. Group every Insert by the OpId of its left neighbour (or BOF when there isn't one). If two different agents inserted at the same position, that's a collision.

Layer 2, duplicate symbol. Scan the merged snapshot for duplicated top-level symbols — fn, impl, struct, enum, trait, class, def — and attribute each to the agents that contributed it. This is the parse_config case above.

Layer 2 is a proxy, and the source says so: there's no tree-sitter parse in there yet, and the duplicate-symbol scan is standing in for the real structural diff we'd rather have. We'd rather ship the honest heuristic and label it than pretend the hard part is solved.

A gate sits in front of both: MIN_CONFLICT_TEXT_LEN = 10. If both sides of a "collision" trim down to under ten bytes, it's whitespace, and we don't spend a model call on whitespace.

The weld

Real collisions go to triage.rs, which sends both versions to a model with a prompt that asks for exactly one thing:

You are a code synthesis engine. Two agents independently wrote the following versions of the same code region. Synthesize a single best version that: preserves the correct behaviour of both agents' implementations / is syntactically valid / combines their complementary logic where possible / is idiomatic and clean. Return ONLY the synthesized code.

It's handed the CRDT's predicted merge as context, plus each agent's version tagged by agent id. Routing goes to OpenRouter if a key is present, OpenAI if not, and falls back to shelling out to our own CLI. A 30-second timeout resolves to Unresolvable, in which case the plain CRDT merge stands and a human looks at it.

The merge itself is the part I like. The synthesized text is injected back as a fresh synthetic op, authored by an agent literally named rtvcs-collapser, and it becomes canonical in the snapshot. The original ops from A and B are not deleted — the log is append-only, so the record of who wrote what, and what the machine did about it, survives. You can always ask what the weld replaced.

Identical edits from two agents dedupe with no model call at all. Agreement is free.

The wire format is a directory

There is no server in the hot path, no broker, no bespoke protocol. A worker under the substrate writes its ops to a file:

.planning/rtvcs/op-pending/<file-slug>/<agent-id>.jsonl

One JSON op per line:

{"id":{"agent_id":"n1-w03","clock":42},"file_path":"src/lib.rs",
 "kind":{"Insert":{"left":{"agent_id":"n1-w01","clock":5},"right":null,"text":"fn new_fn() {}\n"}}}

The supervisor (rtvcs-supervisor, on by default via RTVCS_ALWAYS_ON) watches those directories on a 500ms tick, applies ops, detects collisions, welds them, and writes a CollisionEvent to .planning/rtvcs/collisions/. Per-agent files mean writers never contend; append-only JSONL means the whole thing is greppable and diffable, which we chose on purpose over a compressed format.

The numbers, and their limits

cargo run -p rtvcs --bin rtvcs-sample is the benchmark, and it does not grade itself on vibes — it shells out to rustc --crate-type lib --emit=metadata and checks whether the code actually compiles.

Each of the 8 cases takes two different but individually valid Rust implementations of the same function — parse_config, clamp_i64, word_count, safe_divide, is_palindrome, max_of, trim_lower, fib — and runs three compiles: each input alone, the collision-blind CRDT merge, and the welded output. The model call is live.

DISTRIBUTION (n = 8)
inputs individually valid : 8/8 (100%)
ASYNC (collision-blind)   : 0/8 compile (0%)   <- broken merges
synthesis fired           : 8/8 (100%)
WAVE (welding/synthesis)  : 8/8 compile (100%) <- repaired by synthesis

Every input was valid on its own. Every naive merge of two valid inputs was broken. Every weld compiled.

Now the caveat, because this number is going to get quoted at us. Those eight cases are small, hand-authored functions, chosen so that both versions compile standalone. It is one run, on one day, unreplicated. It is a controlled proof of concept, not a study — it establishes that the mechanism does the thing, and it says nothing rigorous about how welding behaves on a 900-line file with three-way overlap and a trait bound in the middle. We believe the effect is real because we run on it daily. We are not going to dress up n=8 as evidence it isn't.

Where welding does not save you

This is the part we got wrong, and it cost us.

Welding operates on regions — a function, a block — between workers under one foreman, in one working tree. It does not operate on trees. And for a while we talked as though it did.

Then a four-lane swarm shipped with two lanes silently reverted to baseline. Nothing crashed. No conflict was reported. The work was simply gone before the foreman build, because a second foreman sharing that working tree ran an innocent checkout, and uncommitted state from the other lanes went with it.

That is not a weld. That is a revert, and no amount of CRDT machinery helps, because the content never reached an op log — it was sitting in the working tree, which is a shared mutable global that any git command can rewrite.

The distinction is now doctrine:

GranularityWho collidesWelding covers it?
Region (function/block)swarm-workers under one foreman, one treeYes — ops → supervisor synthesizes
Tree (uncommitted state)multiple foremen sharing one working treeNo — last writer wins. A revert, not a weld.

The rule that falls out: one foreman, one worktree, one branch. Region-weld the swarm inside it. Converge the foremen by welding branches — same conflict engine, same triage, one granularity up, and when a file changed on only one side it's taken free with no model call.

Enforcement lives in a place that surprised me until it didn't: xp worktree claim writes the foreman's lease into .git/. Git rewrites your working tree all day long; it never rewrites its own admin directory. A lease stored anywhere else is a lease a checkout can revert — which is the exact bug we were trying to stop.

The other thing that isn't parallel

One more lesson from the same era, because it's the mirror image.

Cargo already saturates every core. So N agents each running cargo build is not N× throughput — it's N× oversubscription, and it is slower than doing them one at a time while pegging the machine. Subagents in our system therefore do not build. They do not run, commit, or push. They write code and stage paths; the foreman builds once, after integration.

We enforce it with a machine-wide file lock at %LOCALAPPDATA%/xp/build-lock, so every build across every worktree and every session serializes on one lock. The first attempt at this was a hook that watched for the string cargo in shell commands, and it was useless — it never saw the builds our own tooling spawned in-process, which were most of them. A guard that only catches the honest path isn't a guard.

Parallelize the authoring. Serialize the build. Weld the overlap.

Collisions were never the problem. Pretending we could avoid them was.