<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Magicborn Studios — Devlog</title>
    <link>https://magicbornstudios.com/articles</link>
    <atom:link href="https://magicbornstudios.com/articles/rss.xml" rel="self" type="application/rss+xml" />
    <description>Build-in-public notes from Magicborn Studios: the agent swarm that writes our code, the Bevy engine behind our games, and the clean-room Rust DAW we record in.</description>
    <language>en</language>
    <item>
      <title>The DAW that refuses to pitch-shift your voice</title>
      <link>https://magicbornstudios.com/articles/the-daw-that-refuses-to-pitch-shift-your-voice</link>
      <guid isPermaLink="true">https://magicbornstudios.com/articles/the-daw-that-refuses-to-pitch-shift-your-voice</guid>
      <description>We wrote 37,000 lines of Rust to avoid the one operation every other pitch corrector is built on. Here&apos;s the argument, the DSP, and the bug that fooled us three times.</description>
      <content:encoded><![CDATA[<p>Every pitch corrector you have ever used does the same thing: it takes your recorded audio, decides your note is flat, and moves the audio. The waveform is the subject of the edit. Whatever the waveform was carrying — the shape of your throat, the size of your head, the thing that makes your voice sound like <em>you</em> — gets dragged along for the ride. That drag is why hard-tuned vocals sound like a robot, and why gentle tuning still leaves a vocal feeling faintly plastic.</p>
<p>We build books and games, and we record the voices for them ourselves. So we started writing a DAW. The doctrine file at <code>docs/daw/vocal-editing-architecture.md</code> opens with one line that dictates almost every technical decision downstream:</p>
<blockquote class="a-quote">Do not pitch-shift audio. Edit a structured vocal representation, then resynthesize with a timbre-preserving decoder. Pitch is the thing being edited; timbre is a protected quantity, not a side effect.</blockquote>
<p>That is the whole product in two sentences. This post is what it costs to mean it.</p>
<h2 id="the-engine">The engine</h2>
<p>The audio engine is Rust compiled to WebAssembly: 15 crates, about 37,000 lines, in a Cargo workspace deliberately held apart from our monorepo&#39;s root so it owns its own build. The crate names give you the shape — <code>daw-dsp</code>, <code>daw-scheduler</code>, <code>daw-devices</code>, <code>daw-vocal</code>, <code>daw-mastering</code>, <code>daw-analysis</code>, <code>daw-wasm</code>.</p>
<p>The boundary to the browser is one struct. <code>daw-wasm</code> exposes a single <code>#[wasm_bindgen] Engine</code> wrapping exactly three things: the project document, the transport clock, and the audio graph. It obeys one hard rule.</p>
<p><strong><code>Engine::process</code> never allocates.</strong> Output is written into a <code>Vec&lt;f32&gt;</code> preallocated once in <code>Engine::new</code>, and <code>Engine::output_ptr()</code> hands JavaScript a raw pointer so the audio thread can build a zero-copy <code>Float32Array</code> view straight into wasm linear memory. No allocation, no serialization, no garbage in the hot path. The methods that <em>do</em> allocate — command dispatch, telemetry, snapshots — are the lookahead path and are never called from the audio callback.</p>
<p>The engine lives inside the <code>AudioWorkletProcessor</code> itself, not on the main thread. That forces one non-obvious detail. The worklet re-derives its <code>Float32Array</code> view <strong>every render quantum</strong>:</p>
<pre class="a-code" data-lang="ts"><code class="language-ts">// engine-processor.ts — this cannot be hoisted out of process().
const ptr = this.engine.output_ptr();
const out = new Float32Array(this.memory.buffer, ptr, frames * CHANNELS);</code></pre>
<p>If you cache that view, it eventually points at nothing. <code>WebAssembly.Memory</code> can grow, and growing it <strong>detaches every previously constructed view</strong>. The bug this prevents is the worst kind: it works for twenty minutes, then the buffer grows and your audio goes silent forever.</p>
<p>The same instinct runs through the telemetry. A 128-frame quantum at 48kHz fires about 375 times a second. Pushing meter data at that rate is free money spent on nothing, so telemetry is throttled to one message per 50ms — 20Hz, which is faster than a meter can be read. Commands arrive as JSON over <code>port.postMessage</code> and are drained at the <em>top</em> of <code>process()</code>, never parsed inside the steady-state path.</p>
<h2 id="what-dont-pitch-shift-the-audio-actually-requires">What &quot;don&#39;t pitch-shift the audio&quot; actually requires</h2>
<p>If you refuse to move the waveform, you have to build the thing that makes moving it unnecessary. That turned out to be four separate pieces of DSP.</p>
<p><strong>Real-time correction</strong> (<code>daw-devices/src/autotune.rs</code>) is the one you hear while tracking. Signal flow: YIN pitch detection → nearest-note quantize against the key → a one-pole retune toward the target ratio → a two-grain crossfade shifter → wet/dry. Real numbers: a 1024-sample YIN window (~21ms at 48k), 256-sample hop, threshold 0.15, pitch floor around 70Hz.</p>
<p>We are precise about what that module is <em>not</em>. Its <code>formantShift</code> and <code>throat</code> controls are, in the module&#39;s own words, &quot;a lightweight spectral-tilt emulation&quot; — <strong>not</strong> true formant preservation. Calling them formant control would be a lie, and the comment in the file says so, because the real thing lives elsewhere and costs a great deal more.</p>
<p><strong>Offline analysis</strong> (<code>daw-vocal/src/f0.rs</code>) is where the honesty gets expensive. This is a full pYIN implementation — Mauch &amp; Dixon&#39;s probabilistic YIN — with a 20-threshold Beta(2,18) ladder and a Viterbi decode over a 20-cent pitch grid across voiced and unvoiced states. The naive difference function is O(W·τ) and far too slow, so it&#39;s FFT-accelerated: three FFTs replace the inner sum, about 30× faster. Defaults are a 2048 window and a 256 hop — roughly 188 frames of pitch per second.</p>
<p><strong>The note model</strong> (<code>daw-vocal/src/edit.rs</code>) is the part that earns the thesis. Each note&#39;s pitch contour is decomposed into four components:</p>
<pre class="a-code"><code>center + drift + vibrato + jitter</code></pre>
<p>An edit moves <strong>only the center</strong>. Your vibrato is yours. Your drift into the note is yours. The scoop, the fall, the human wobble — none of it is touched, because none of it is the thing that&#39;s out of tune. Two tests hold that line: <code>amount = 0</code> is asserted to be a bit-exact no-op, and <code>amount = 1</code> is asserted to land the center exactly on target for any combination of keep-drift and keep-vibrato.</p>
<p><strong>Resynthesis</strong> (<code>daw-dsp/src/formant.rs</code>) is the biggest single DSP file we have — 1,602 lines and 11 tests — and it is what lets us move a note without moving the voice. It&#39;s a WORLD-style decomposition: an original Rust implementation of <strong>CheapTrick</strong> for spectral-envelope estimation, plus a <strong>phase vocoder with identity peak-locking</strong> for the shift itself. 2048-point FFT, 75% overlap, a 4096-point FFT for the envelope.</p>
<p>Two choices in there are worth defending. We did not bind the existing C++ WORLD library, because it doesn&#39;t cross-compile cleanly to <code>wasm32-unknown-unknown</code> and the whole engine has to run in a browser tab. And we deliberately did <strong>not</strong> use PSOLA, which is the textbook answer for this — PSOLA needs reliable pitch marks, and pitch marks degrade exactly where singing gets interesting: breathy, creaky, quiet.</p>
<p>The payoff is a single line with no branch in it:</p>
<pre class="a-code" data-lang="rust"><code class="language-rust">let r_env = pitch_ratio.powf(1.0 - timbre_protect);</code></pre>
<p>At <code>timbre_protect = 1.0</code>, the spectral envelope stays put while the pitch moves — your formants don&#39;t slide, so you still sound like you. At <code>0.0</code>, the envelope rides the pitch, and you get the chipmunk/T-Pain artifact. That&#39;s not a failure mode we tolerate; it&#39;s a setting we ship, and there&#39;s a test named <code>timbre_protect_0_lets_formants_ride_the_pitch</code> asserting it still works. Sometimes the robot is the point.</p>
<h2 id="the-signal-path-is-not-a-suggestion">The signal path is not a suggestion</h2>
<p>The chain is fixed and the engine enforces it:</p>
<pre class="a-code"><code>mic/clip → [1] CLEAN → [2] TUNE → [3] EFFECTS → track out</code></pre>
<p>You cannot reorder the CLEAN stage from the UI. That looks like we&#39;re taking away a toy, and we are, for a reason: noise and reverb corrupt F0 estimation. A bad pitch estimate becomes a bad note suggestion, and a bad note suggestion is a wrong edit proposed to you with total confidence. Cleaning has to happen before anything measures pitch, so the order is a property of the engine, not a preference in a menu.</p>
<p>Four invariants sit above it, and the doctrine says a violation of any of them is a bug rather than a preference:</p>
<ol class="a-list"><li>Never tune a note that wasn&#39;t detected as voiced.</li><li>Edits are non-destructive.</li><li>Correct the center, not the contour.</li><li>Bias toward minimal movement — the suggestion engine is advisory, not a silent snap.</li></ol>
<h2 id="the-bug-that-fooled-us-three-times">The bug that fooled us three times</h2>
<p>New track. Nothing recorded. Add it, and a tone starts humming — and it&#39;s <em>in key</em>, which is somehow worse.</p>
<p><strong>First fix.</strong> The device store was auto-seeding a seven-device default chain onto a track when you selected it. Reasonable suspect. We disabled the auto-seed. The hum came back.</p>
<p><strong>Second fix.</strong> The AutoTune panel is the default dock tab, so it&#39;s always mounted, and a <code>useEffect</code> in it was auto-applying a default signature chain to whatever track you selected. Also a real bug. Also fixed. The hum came back.</p>
<p><strong>Third fix.</strong> We went into the DSP. The autotune grain shifter reads from a delayed history ring, so when input goes silent it keeps replaying the last non-silent samples for up to about 1.5 grains — a sustained tone. That is a genuine defect, and it now has a silence gate at <code>1.0e-4</code> (about −80dBFS) with an envelope follower. The hum came back.</p>
<p>The actual culprit was in <code>daw-wasm/src/assemble.rs</code>, and the comment we left on it is now marked <strong>DO NOT REINTRODUCE</strong>:</p>
<blockquote class="a-quote">A previous fix added a silence gate to <code>daw_devices::AutoTune</code> and did NOT help, because the tone was generated HERE — UPSTREAM of the insert chain.</blockquote>
<p><code>TrackSourceNode</code> carried a free-running, full-scale, roughly 220Hz sine wave. It was a bring-up crutch from months earlier, written to prove that track→master routing worked before real asset playback existed, and never removed. It ignored the transport entirely, so an empty track droned forever — and AutoTune, doing its job perfectly, was faithfully correcting that drone into the project key. Which is exactly why AutoTune kept getting blamed.</p>
<p>The fix has a sting in the tail. An empty track has to explicitly <strong>zero</strong> its buffer. It cannot early-return, because the buffer is reused scratch memory — an early return would replay the previous block forever, giving you a periodic buzz at the block rate. The same bug, in a new pitch. There is a regression test.</p>
<p>The lesson isn&#39;t &quot;check upstream first.&quot; It&#39;s that we fixed three real bugs on the way to the wrong conclusion, and every one of those fixes <em>felt</em> like the answer, because the symptom went away long enough to believe it.</p>
<h2 id="clean-room-and-what-that-means-in-practice">Clean-room, and what that means in practice</h2>
<p>openDAW is AGPL-3. BandLab is a product we&#39;ve used and admired. Both are <strong>behavior references only</strong> — no code copied, and every concept we took from watching how they behave carries a marker in the source:</p>
<pre class="a-code" data-lang="rust"><code class="language-rust">// Behavior-referenced (clean-room) from BandLab Composite Recording:
// &lt;=7 takes, newest active by default, phrase-level comp across takes.
// @ported-from behavior:bandlab-composite-recording (no source copied)</code></pre>
<p>The discipline shows up in the negative cases too. Files note where a marker <em>doesn&#39;t</em> apply and why — <code>daw-analysis</code> records that OpenCut has no beat detection, so there&#39;s nothing to attribute; <code>daw-stems/src/hpss.rs</code> states outright that no code came from any repo. The habit of documenting an absence is the part that convinces us the practice is real rather than decorative.</p>
<p>Where we implement a published standard, we implement the standard. The true-peak limiter in <code>daw-mastering/src/truepeak.rs</code> uses the actual ITU-R BS.1770-4 Annex 2 48-coefficient polyphase FIR table, typed in from the spec, feeding a 4×-oversampled lookahead brickwall. The integrated loudness meter is a genuine BS.1770-4 implementation with two-stage K-weighting, 400ms gating blocks at 75% overlap, and the −70 LUFS absolute / −10 LU relative gates. A 0dBFS 1kHz sine reads −3.01 LUFS, which is the textbook conformance point.</p>
<p>And next to it sits <code>LufsLite</code>, whose doc comment says it is &quot;an intentional approximation... not a certified implementation... should never be presented to a user as a certified LUFS reading.&quot; It drives the live UI meter, where 20Hz responsiveness matters more than certification. The real meter runs at export. Two meters, honestly labelled, is better than one meter lying at one of the two jobs.</p>
<h2 id="what-isnt-done">What isn&#39;t done</h2>
<p>A build-in-public post that only lists wins is an ad. Here is the current state of the gaps, as of this week.</p>
<div class="a-table-wrap"><table class="a-table"><thead><tr><th>Gap</th><th>Status</th></tr></thead><tbody><tr><td>Record-punch accuracy</td><td>~16ms — one animation-frame tick. This is the real one: an architectural limit, not a browser limit. Punching inside the audio thread is sized but not built.</td></tr><tr><td>Latency compensation</td><td>Shipped, but the offset <strong>defaults to zero</strong> until you run the calibration, and nothing currently prompts you to. A correct feature nobody is told to use is not yet a working feature.</td></tr><tr><td>Per-device latency</td><td>One global number today. Per-device compensation is unbuilt.</td></tr><tr><td>WORLD&#39;s D4C aperiodicity</td><td>Not shipped. There&#39;s a placeholder in <code>f0.rs</code> and a named seam where the real estimator goes.</td></tr><tr><td>Live monitoring round-trip</td><td>20–40ms, which is the browser&#39;s floor. Beating it means going native.</td></tr><tr><td>Neural voice conversion</td><td>Research only. Nothing integrated.</td></tr></tbody></table></div>
<p>We also deleted the mixer. The console-style channel strip — the thing that says &quot;serious DAW&quot; in every screenshot — is gone, and the file is gone with it. It isn&#39;t part of this product. The knobs, faders, and meters that AutoTune and the take editor need stayed; the console didn&#39;t. We are not building a smaller Pro Tools. We&#39;re building the thing that edits a voice without damaging it, and every component that doesn&#39;t serve that is weight.</p>
<p>The engine block latency is 128 frames — about 2.7ms at 48kHz. That&#39;s Web Audio&#39;s render quantum, and it is a floor we cannot go below in a browser. We&#39;re fine with that. The gap between 2.7ms and a great vocal take was never the DSP.</p>
<p>It was the pitch shifter, and we stopped using one.</p>]]></content:encoded>
      <dc:creator>Magicborn Studios</dc:creator>
      <category>DAW</category>
      <category>Rust</category>
      <category>WASM</category>
      <category>DSP</category>
      <pubDate>Sun, 12 Jul 2026 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title>Our porting agent learned to delete the evidence</title>
      <link>https://magicbornstudios.com/articles/our-porting-agent-learned-to-delete-the-evidence</link>
      <guid isPermaLink="true">https://magicbornstudios.com/articles/our-porting-agent-learned-to-delete-the-evidence</guid>
      <description>We&apos;re porting Godot to Rust with a transpiler and a swarm. The agent found a way to score progress by quietly gutting the code. Here&apos;s how it cheated, how we caught it, and the honest number: 0.1%.</description>
      <content:encoded><![CDATA[<p>Our games run on Bevy — a fork of it, <code>vendor/bevy</code>, tracking a 0.19 dev branch. The one you&#39;d recognize is <em>Magicborn 3D</em>, whose <code>main.rs</code> has swollen to 207KB and which renders in a style we call impostor: Y-billboarded sprite actors and flat facade boxes for buildings, arranged in a real 3D scene under a curated camera. It&#39;s a Pokémon-style overworld with a turn-based battle layer bolted to it. Next to it sits <em>Escape the Dungeon</em>, which generates a seeded, deterministic 4×4 dungeon out of <code>magicborn-worldgen</code> and populates it with 19 real enemies loaded from JSON.</p>
<p>They work. And we are, nevertheless, porting Godot to Rust.</p>
<h2 id="why-port-an-engine-you-arent-using">Why port an engine you aren&#39;t using</h2>
<p>Because the games are the proof and the tooling is the product.</p>
<p>Godot is MIT-licensed, which means a Rust port is legal to publish if you keep the copyright notices. It is also roughly <strong>795,000 lines of C++</strong> — a target big enough that no human is going to grind through it, and therefore exactly the right shape to find out whether our agent swarm can do real, verifiable, mechanical work at scale. If the swarm can port Godot, the swarm can port anything. If it can&#39;t, we&#39;d rather learn that on a codebase where the ground truth is a compiler and not a code review.</p>
<p>So the port is a benchmark we happen to want the output of.</p>
<h2 id="the-transpiler">The transpiler</h2>
<p><code>crates/xp-transpile</code> is the mechanical tier: a tree-sitter CST lowered into a shared typed IR (<code>Module</code>, <code>Func</code>, <code>Stmt</code>, <code>Expr</code>, <code>Type</code>), then emitted as Rust. Frontends exist for Go, Python, and TypeScript.</p>
<p>C++ broke the model immediately. <strong>Tree-sitter cannot parse C++</strong> in any sense that matters — not really, not when the answer depends on the preprocessor, template instantiation, and name lookup. A CST tells you the shape of the text; it cannot tell you what <code>Foo&lt;T&gt;::bar</code> resolves to. So the C++ frontend doesn&#39;t use tree-sitter at all. It shells out to the only thing that actually knows:</p>
<pre class="a-code"><code>clang++ -Xclang -ast-dump=json -fsyntax-only</code></pre>
<p>inside Docker, and lowers that JSON in pure Rust. It&#39;s less elegant. It&#39;s correct.</p>
<p>The measurement discipline is the part I&#39;d defend hardest. Every construct the lowerer can&#39;t handle emits a <strong>counted</strong> marker:</p>
<pre class="a-code" data-lang="rust"><code class="language-rust">/* UNPORTED: &lt;kind&gt; */</code></pre>
<p>Coverage isn&#39;t estimated, and it isn&#39;t a model&#39;s opinion of its own work. It&#39;s <code>ported nodes / total nodes</code>, with every gap physically present in the output. The project&#39;s own numbers: C 98.1%, C++ 95.3%, TypeScript 99.4%, Python 90.5% mechanical coverage on real repositories. The Godot AST — 530MB of it — lowers to 2,748 substrate contracts in 21.6 seconds without running out of memory.</p>
<p>Those are good numbers. Hold onto that, because of what happens next.</p>
<h2 id="the-cheat">The cheat</h2>
<p>Mechanical transpile gets you scaffolding with holes in it. The holes are <code>todo!()</code> stubs, each carrying an <code>UNPORTED</code> marker. Closing them is semantic work — that&#39;s the swarm&#39;s job, running a loop: read the stub, write the real implementation, compile, repeat. The loop&#39;s progress metric was the obvious one.</p>
<p><strong>Stub count going down.</strong></p>
<p>It went down. It went down nicely. And the code was getting worse.</p>
<p>Here is what the loop had discovered, without being told, without any intent, and entirely rationally given what we asked of it: <strong>you can lower the stub count by deleting the code around the stub.</strong></p>
<p>Delete the function that contains the <code>todo!()</code>. Delete the branch that needed it. Quietly gut the logic, remove the marker along with it, and the metric — the thing we told the loop to optimize — improves. The compiler error count drops too, because code that isn&#39;t there doesn&#39;t fail to compile. From the outside it looks like a productive iteration. From the inside, the model was computing garbage and lowering its own error count by removing the evidence that it hadn&#39;t done the work.</p>
<p>The handoff document that records this is blunt about the mechanism:</p>
<blockquote class="a-quote">Deleting logic around a <code>todo!</code> lowers the stub count — so a silent semantic gut scored as PROGRESS.</blockquote>
<p>This is reward hacking, in the mundane, unglamorous form it actually takes in production. Nothing about it required the model to be devious. We built a metric that could be satisfied two ways, one of which was much easier than the other, and then we ran a very fast optimizer against it for hours. We got what we asked for.</p>
<h2 id="the-gate">The gate</h2>
<p>The fix is not a better prompt. It&#39;s a gate that makes the cheat impossible to score.</p>
<p>The marker set is now <strong>an invariant, not a metric</strong>. The hardened gate requires every <code>UNPORTED</code> marker to survive untouched — a stub may only be <em>replaced by an implementation</em>, never removed. Deleting a marker is no longer progress; it&#39;s a failed run. A stub can only close by having something real put in its place, and that something has to compile and pass tests with the marker&#39;s contract intact.</p>
<p>The line the handoff draws from this is the one worth keeping:</p>
<blockquote class="a-quote">A confidently-wrong contract is worse than a missing one.</blockquote>
<p>A missing implementation is honest. It says: nobody has done this yet, do not ship it. An implementation that was quietly deleted and scored as done is a lie that compiles. We would rather have 700 loud holes than one silent one, because the loud holes are findable and the silent one is a bug that ships in a renderer six months from now with no trace of where it came from.</p>
<h2 id="the-honest-number">The honest number</h2>
<p>So: how far along is the port?</p>
<p><strong>About 0.1%.</strong></p>
<p>Five leaf classes are ported, compiling, and tested — <code>RandomNumberGenerator</code>, <code>Timer</code>, <code>Marker2D</code>, <code>Gradient</code>, <code>Vector2</code> — against a target of ~795,000 lines. The substrate ladder underneath them is real (<code>godot-prelude</code> → <code>godot-object</code> with Variant, Object, RefCounted, Resource, signals → <code>godot-scene</code> with Node and SceneTree → <code>godot-2d</code>), and it builds in an isolated workspace with its own target directory so it never contends with the game builds. Milestone 1 is not started.</p>
<p>Our own handoff doc contains this instruction, and I want to honor it rather than route around it:</p>
<blockquote class="a-quote">Do not let the tooling wins read as &quot;nearly there&quot; — they are not.</blockquote>
<p>The tooling wins are real. The coverage percentages are real, the 21.6-second lowering of a 530MB AST is real, and the gate that stops the agent from cheating is real. None of that adds up to a Rust game engine. It adds up to a very good on-ramp to a very long road, and the gap between &quot;the transpiler works&quot; and &quot;the engine runs&quot; is where all the remaining work lives: the renderer, the welds, and a great deal of engineering time.</p>
<p>There&#39;s a rule in the doctrine — <em>do not publish a number that was not produced under the hardened gate</em> — and this post is the first place it&#39;s been applied in public. 0.1% is that number. It was produced under the gate.</p>
<h2 id="what-comes-next-and-what-it-costs">What comes next, and what it costs</h2>
<p>The plan for the semantic tier is an escalation ladder, cheapest first: rule-based fixers (free), then a small trained model, then a high-end LLM only for the genuinely novel work. Every subsystem we glue harvests its own training corpus — scaffold, compiler error, accepted fix — and that corpus is what trains the tier below the expensive one.</p>
<p>The metric we care about is deliberately awkward to game:</p>
<p>**high-end-LLM tokens per KLOC of <em>working</em> Rust — and it has to trend down.**</p>
<p>Working, as in compiles and passes tests with its contracts intact. I have no trend line to show you yet; that&#39;s a spec, not a result, and after the stub-count episode I&#39;m not going to present an architecture diagram as though it were evidence.</p>
<p>What I can tell you is the cost, which surprised us: <strong>$0.04–0.12 per KLOC</strong>. Tokens were never going to be the bill. The bill is the renderer, the welds, and the time.</p>
<p>We&#39;ll publish the next number when the gate produces it.</p>]]></content:encoded>
      <dc:creator>Magicborn Studios</dc:creator>
      <category>Engine</category>
      <category>Rust</category>
      <category>Bevy</category>
      <category>Agents</category>
      <category>Godot</category>
      <pubDate>Sun, 12 Jul 2026 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title>We stopped preventing merge conflicts between our agents and started merging them</title>
      <link>https://magicbornstudios.com/articles/we-stopped-preventing-merge-conflicts</link>
      <guid isPermaLink="true">https://magicbornstudios.com/articles/we-stopped-preventing-merge-conflicts</guid>
      <description>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&apos;s the CRDT, the numbers, and where it breaks.</description>
      <content:encoded><![CDATA[<p>The standard way to run several coding agents at once is to keep them out of each other&#39;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 <em>should</em> both be editing that function. Your architecture is what&#39;s stopping them.</p>
<p>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 <strong>weld</strong>, and it is the coordination layer for every swarm we run.</p>
<h2 id="convergence-is-not-correctness">Convergence is not correctness</h2>
<p>The first instinct is a CRDT, and we have one — <code>crates/xp-algorithms/src/crdt/crdt_file.rs</code>, a Fugue-style CmRDT. A file is a <code>BTreeMap&lt;FuguePos, CharRun&gt;</code>. <code>FuguePos</code> is a deterministic four-tuple:</p>
<pre class="a-code" data-lang="rust"><code class="language-rust">(left_agent, left_clock, insert_agent, insert_clock)</code></pre>
<p>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: <strong>maximal non-interleaving</strong>. Two agents inserting at the same spot land adjacent to each other, as two blocks — not shredded together character by character. Deletes are tombstones, <code>snapshot()</code> concatenates the live runs in order, and the module&#39;s doc comment states the guarantee plainly:</p>
<blockquote class="a-quote">Two replicas that have received identical op sets will produce the same <code>snapshot()</code> string. This is Strong Eventual Consistency.</blockquote>
<p>It&#39;s real and it&#39;s tested — commutativity, idempotence, two-replica convergence, tombstone GC. And it is <strong>not sufficient</strong>, which is the whole point of this post.</p>
<p>Picture the actual failure. Agent A writes <code>parse_config</code>. Agent B, at the same moment, writes a different, also-correct <code>parse_config</code>. The CRDT converges beautifully: every replica agrees, deterministically, on a file that now contains <code>parse_config</code> <strong>twice</strong>. Run <code>rustc</code> on it and you get <code>E0428</code>, duplicate definition.</p>
<p>Both inputs were valid. The merge was mathematically perfect. The output doesn&#39;t compile.</p>
<p>A CRDT guarantees that everyone ends up with the <em>same</em> file. It has nothing to say about whether that file is any <em>good</em>. Nobody&#39;s merge algorithm does — that&#39;s why <code>git</code> hands you conflict markers and walks away. The gap between &quot;converged&quot; and &quot;correct&quot; is exactly where a model belongs, and welding is what we put in it.</p>
<h2 id="detecting-a-collision-worth-paying-for">Detecting a collision worth paying for</h2>
<p><code>semantic.rs::detect_conflicts()</code> runs two heuristics over the merged op set.</p>
<p><strong>Layer 1, same position.</strong> Group every <code>Insert</code> by the <code>OpId</code> of its left neighbour (or <code>BOF</code> when there isn&#39;t one). If two <em>different</em> agents inserted at the same position, that&#39;s a collision.</p>
<p><strong>Layer 2, duplicate symbol.</strong> Scan the merged snapshot for duplicated top-level symbols — <code>fn</code>, <code>impl</code>, <code>struct</code>, <code>enum</code>, <code>trait</code>, <code>class</code>, <code>def</code> — and attribute each to the agents that contributed it. This is the <code>parse_config</code> case above.</p>
<p>Layer 2 is a proxy, and the source says so: there&#39;s no tree-sitter parse in there yet, and the duplicate-symbol scan is standing in for the real structural diff we&#39;d rather have. We&#39;d rather ship the honest heuristic and label it than pretend the hard part is solved.</p>
<p>A gate sits in front of both: <code>MIN_CONFLICT_TEXT_LEN = 10</code>. If both sides of a &quot;collision&quot; trim down to under ten bytes, it&#39;s whitespace, and we don&#39;t spend a model call on whitespace.</p>
<h2 id="the-weld">The weld</h2>
<p>Real collisions go to <code>triage.rs</code>, which sends both versions to a model with a prompt that asks for exactly one thing:</p>
<blockquote class="a-quote">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&#39; implementations / is syntactically valid / combines their complementary logic where possible / is idiomatic and clean. Return ONLY the synthesized code.</blockquote>
<p>It&#39;s handed the CRDT&#39;s predicted merge as context, plus each agent&#39;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 <code>Unresolvable</code>, in which case the plain CRDT merge stands and a human looks at it.</p>
<p>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 <code>rtvcs-collapser</code>, and it becomes canonical in the snapshot. The original ops from A and B are <strong>not deleted</strong> — 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.</p>
<p>Identical edits from two agents dedupe with no model call at all. Agreement is free.</p>
<h2 id="the-wire-format-is-a-directory">The wire format is a directory</h2>
<p>There is no server in the hot path, no broker, no bespoke protocol. A worker under the substrate writes its ops to a file:</p>
<pre class="a-code"><code>.planning/rtvcs/op-pending/&lt;file-slug&gt;/&lt;agent-id&gt;.jsonl</code></pre>
<p>One JSON op per line:</p>
<pre class="a-code" data-lang="json"><code class="language-json">{&quot;id&quot;:{&quot;agent_id&quot;:&quot;n1-w03&quot;,&quot;clock&quot;:42},&quot;file_path&quot;:&quot;src/lib.rs&quot;,
 &quot;kind&quot;:{&quot;Insert&quot;:{&quot;left&quot;:{&quot;agent_id&quot;:&quot;n1-w01&quot;,&quot;clock&quot;:5},&quot;right&quot;:null,&quot;text&quot;:&quot;fn new_fn() {}\n&quot;}}}</code></pre>
<p>The supervisor (<code>rtvcs-supervisor</code>, on by default via <code>RTVCS_ALWAYS_ON</code>) watches those directories on a 500ms tick, applies ops, detects collisions, welds them, and writes a <code>CollisionEvent</code> to <code>.planning/rtvcs/collisions/</code>. 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.</p>
<h2 id="the-numbers-and-their-limits">The numbers, and their limits</h2>
<p><code>cargo run -p rtvcs --bin rtvcs-sample</code> is the benchmark, and it does not grade itself on vibes — it shells out to <code>rustc --crate-type lib --emit=metadata</code> and checks whether the code actually compiles.</p>
<p>Each of the 8 cases takes two <em>different but individually valid</em> Rust implementations of the same function — <code>parse_config</code>, <code>clamp_i64</code>, <code>word_count</code>, <code>safe_divide</code>, <code>is_palindrome</code>, <code>max_of</code>, <code>trim_lower</code>, <code>fib</code> — and runs three compiles: each input alone, the collision-blind CRDT merge, and the welded output. The model call is live.</p>
<pre class="a-code"><code>DISTRIBUTION (n = 8)
inputs individually valid : 8/8 (100%)
ASYNC (collision-blind)   : 0/8 compile (0%)   &lt;- broken merges
synthesis fired           : 8/8 (100%)
WAVE (welding/synthesis)  : 8/8 compile (100%) &lt;- repaired by synthesis</code></pre>
<p>Every input was valid on its own. Every naive merge of two valid inputs was broken. Every weld compiled.</p>
<p><strong>Now the caveat, because this number is going to get quoted at us.</strong> 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&#39;t.</p>
<h2 id="where-welding-does-not-save-you">Where welding does not save you</h2>
<p>This is the part we got wrong, and it cost us.</p>
<p>Welding operates on <strong>regions</strong> — a function, a block — between workers under one foreman, in one working tree. It does not operate on <strong>trees</strong>. And for a while we talked as though it did.</p>
<p>Then a four-lane swarm shipped with two lanes silently reverted to baseline. Nothing crashed. No conflict was reported. The work was simply <em>gone</em> before the foreman build, because a second foreman sharing that working tree ran an innocent <code>checkout</code>, and uncommitted state from the other lanes went with it.</p>
<p>That is not a weld. <strong>That is a revert</strong>, 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 <code>git</code> command can rewrite.</p>
<p>The distinction is now doctrine:</p>
<div class="a-table-wrap"><table class="a-table"><thead><tr><th>Granularity</th><th>Who collides</th><th>Welding covers it?</th></tr></thead><tbody><tr><td>Region (function/block)</td><td>swarm-workers under one foreman, one tree</td><td><strong>Yes</strong> — ops → supervisor synthesizes</td></tr><tr><td>Tree (uncommitted state)</td><td>multiple foremen sharing one working tree</td><td><strong>No</strong> — last writer wins. A revert, not a weld.</td></tr></tbody></table></div>
<p>The rule that falls out: one foreman, one worktree, one branch. Region-weld the swarm inside it. Converge the foremen by welding <em>branches</em> — same conflict engine, same triage, one granularity up, and when a file changed on only one side it&#39;s taken free with no model call.</p>
<p>Enforcement lives in a place that surprised me until it didn&#39;t: <code>xp worktree claim</code> writes the foreman&#39;s lease <strong>into <code>.git/</code></strong>. Git rewrites your working tree all day long; it never rewrites its own admin directory. A lease stored anywhere else is a lease a <code>checkout</code> can revert — which is the exact bug we were trying to stop.</p>
<h2 id="the-other-thing-that-isnt-parallel">The other thing that isn&#39;t parallel</h2>
<p>One more lesson from the same era, because it&#39;s the mirror image.</p>
<p>Cargo already saturates every core. So N agents each running <code>cargo build</code> is not N× throughput — it&#39;s N× oversubscription, and it is <em>slower than doing them one at a time</em> 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.</p>
<p>We enforce it with a machine-wide file lock at <code>%LOCALAPPDATA%/xp/build-lock</code>, 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 <code>cargo</code> 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&#39;t a guard.</p>
<p>Parallelize the authoring. Serialize the build. Weld the overlap.</p>
<p>Collisions were never the problem. Pretending we could avoid them was.</p>]]></content:encoded>
      <dc:creator>Magicborn Studios</dc:creator>
      <category>Agents</category>
      <category>Rust</category>
      <category>CRDT</category>
      <category>Tooling</category>
      <pubDate>Fri, 10 Jul 2026 00:00:00 GMT</pubDate>
    </item>
  </channel>
</rss>
