[← Blog]

The Second Binary

Two builds of the same tool sat on one machine. They were weeks of work apart — one predated a daemon fix, a socket-discovery overhaul, and a change to how projects are laid out on disk. Both reported v0.3.0. Neither was lying. That is the worst kind of bug report: every instrument reads nominal, and the machine is wrong anyway.

The tool was folddb-dev — our development node. An agent had spent a day building a prototype against it and kept tripping over behavior the documentation said was fixed. It was fixed. Just not in any binary the agent could get its hands on: the released build predated the endpoint it needed, and the from-source build it fell back to came from a checkout frozen at an archived commit. Three layers of stale, and a version string cheerfully vouching for all of them.

The investigation that started as “is the dev node up to date?” ended somewhere better: the dev node should not exist.

THE CODEBASE every fix lands here today · says v0.3.0 THE DEV BINARY its own release line last release · says v0.3.0 THE DRIFT — INVISIBLE TO --version
Fig. 1 — two release lines, one version string

Why there were two

The second binary had honest origins. It was an ephemeral playground: throwaway data directory, mock identity, torn down on exit — a node you could not hurt anything with, by construction. It carried heavyweight machinery the real node didn’t want, chiefly a server-side Rust→WASM compiler for building transforms. And it kept a promiscuous authoring surface — register anything, write anything, advance the clock — out of the product people actually run. Every one of those reasons was good. Every one of them expired, and nobody sent a notice.

A second binary is a standing invitation to drift

The dev node re-implemented the real node’s wiring — the daemon lifecycle, the socket discovery, the boot sequence. So every fix to that wiring had to land twice, and the second landing is exactly the kind of chore that quietly doesn’t happen. It shipped on its own release line, so it could go stale independently of the thing it was supposed to mirror. And because its version was pinned to its own line, --version could not distinguish a current build from a fossil. None of this was a flaw in the code. It is the physics of maintaining two of something.

Auditing the reasons

Deleting a component is only safe if you can account for why it existed. We went down the list.

The compiler never belonged on the server. Compiling a developer’s Rust in-process meant a throwaway build directory — every compile a cold compile, some of them minutes long, behind a timeout budget nobody enjoyed tuning. But the developer already has a build system, sitting next to the source, with a warm cache: cargo. So the compile moves to where the code lives, and the node’s registration surface accepts compiled WASM bytes — the same binary-only contract our schema registry already adopted. Incremental builds arrive for free, and an entire class of timeout machinery is deleted rather than tuned.

The runtime always belonged in the product. Transforms are not a development toy; they are a feature of the database. A default build of the real node that cannot execute them is undershipped. So the WASM runtime graduates from a dev-node dependency to a default one. What remains for a “dev node” to uniquely carry? A runtime the product now includes, and a compiler nobody should host.

BEFORE YOUR PROJECT rust source source DEV BINARY compiles cold, then executes AFTER YOUR PROJECT cargo · incremental wasm bytes THE NODE executes · by default COMPILE WHERE THE CODE LIVES · EXECUTE WHERE THE DATA LIVES
Fig. 2 — the compiler leaves the server; the runtime enters the product

And the scope had quietly narrowed. Building an application on LastDB happens against the real node’s app surface — namespaced, capability-gated, over the local socket. What the dev node was genuinely for, by the end, was developing transforms: register a schema, register a view, write a row, watch it fire. A whole second server is a lot of architecture for that loop.

One binary, with a dev wing

So: delete the dev node, and bolt its one good loop onto the real node. folddb dev boots an ephemeral session — same throwaway data directory, same teardown — inside the one binary that gets every fix the moment it lands. There is no second release line to fall behind, because there is no second release.

Safety by construction, not by flag

The separate binary had one property worth grieving: it could not touch production data, the way a building without a door cannot be walked into. A merged binary must not demote that to an if statement. Two rules, both enforced where the compiler can see them: a dev session’s data root is pinned to the throwaway directory by type — there is no code path that opens the real store; and the authoring surface is compiled out of shipped builds — verified by a build test, so the door the production binary doesn’t have is a door it cannot grow back.

NO DOOR — BY CONSTRUCTION FOLDDB — ONE BINARY YOUR DATA production DEV WING folddb dev ephemeral · /dev/* ALWAYS SHIPPED DEV BUILDS ONLY
Fig. 3 — one house; the dev wing has no door, and shipped builds don't build it

The deletion is under way, cut into slices that each land whole: the runtime becomes a default, the sessions move in, the transform loop moves in, and then the second binary — its release pipeline, its formula, its whole parallel existence — is retired. The last slice is a proof, not a merge: a fresh install running the entire documented loop, plus a test that the shipped build serves none of the dev surface.

The takeaway

If your dev tool re-implements your product’s wiring, you do not have a dev tool; you have a fork with a friendlier name, and it is drifting while you read this. The fix is not more diligent double-fixing, and it is certainly not a version bump — our version strings were accurate the whole time. The fix is architectural: one binary, with the development surface as a compile-time wing — present when you’re building, absent when you’ve shipped, and never, structurally, a second thing to keep current.

The stale binary was found the way most of our bugs are found — by an agent doing real work inside our autonomous build loop, on the same stack that runs Brain and Kanban.

[← Blog]