skip to content
all writeups
4 min read

Atlas: implementing a spec I did not write

One week, 151 commits – building an agent chat client against the ACP specification and supervising a long-lived coding-agent subprocess.

otjcollegeaiwebinfra

The build week: 151 commits, Monday to Friday. Two things I had never done before, done at once – implementing a protocol to somebody else’s specification, and managing a long-lived subprocess. By the end of the first day the backend was proven end to end: a prompt sent from the PC spawned omp acp on the server, round-tripped a permission prompt, ran a tool, and wrote a rendered transcript.

Implementing against a spec you did not write removes the option of redefining the problem when the implementation gets awkward – and that turned out to be the week’s most useful property. omp scopes session resume to the working directory a session started in, and rejects a second resume with is already loaded for <cwd>. Nothing about that is mine to change: the arbitration – first resume wins, the second device binds to the run the first created – is mine, the behaviour is not. The discipline the spec forces cuts closer than that. Resume was quietly broken because acp_session_id was never written: the endpoint that stores it existed, guarded and unit-tested, with zero callers. My job was to carry omp’s session id faithfully, not invent one of my own, and because the id belongs to the protocol it was recoverable – every stored prompt frame carries params.sessionId, so the whole conversation history was backfilled with pure SQL.

A conformant client

The client is conformant by decision: Atlas framing is confined to an outer envelope, inner frames are untouched ACP JSON-RPC, and the relay routes on the envelope without ever parsing the body – the smoke test asserts frames arrive at the far end byte-identical. The payoff is everything inherited rather than built: permission prompts, interrupt, plan mode, model switches, slash commands, the session lifecycle. Every time the week got awkward, the awkwardness was arbitrated in Atlas, never patched into the protocol.

The subprocess is not the conversation

The exec host also runs a live Minecraft network, so one agent process per conversation anyone ever opened was never affordable. An idle reaper retires a child after fifteen minutes of silence. The safety property is the inverse: it must never reap a session that is working. A turn spends most of its life inside a long tool call or waiting on a permission prompt, emitting nothing at all – an activity clock alone kills exactly the sessions doing real work. A turnOpen flag exempts in-turn sessions, and a review session at the end of the week found two defects in the two lines that maintained it. The original:

atlas-channel/src/session-manager.ts
if (event.type === 'turn_start' || event.type === 'agent_start') session.turnOpen = true;
if (event.type === 'turn_end' || event.type === 'agent_end') session.turnOpen = false;

agent_end is not terminal – omp schedules automatic continuations, and its own type declares willContinue – so that second line cleared the exemption mid-retry, on a session genuinely working. And a turn that opened and never settled was immortal: crashes were covered by the restart path, exits by the watchdog, a hang by nothing. Both are fixed in one predicate now (agent_end clears the flag only when isTerminal !== false), plus an outer bound of four hours’ total silence inside one turn, closed loudly as a stall rather than a routine reap.

What makes reaping safe at all is a fact established by measurement, not assumption: a brand-new omp acp process resumed a session killed hours earlier and quoted the prior conversation back verbatim. The conversation outlives the process; reaping only costs the next prompt a handshake.

Last, the events the protocol drops had to be forwarded separately – the fidelity extension, loaded into each omp acp process, exists for exactly that. The interesting case: omp can walk its retry fallback chain and answer with a model the user never chose, and no event for that reaches the wire. The fact is still derivable – a model change while a retry is in flight, seen on before_provider_request – but the two obvious derivations were silent no-ops. Comparing against ctx.model always matches, because it moves with the fallback. Closing the retry window on auto_retry_end alone is wrong too: production D1 holds nine auto_retry_start against five auto_retry_end.

The week closed with an audit. Fifteen defects, and the four worst – including a transcript that rendered nothing and the system’s only genuine data loss – were found by driving a real browser against a real relay, not by reading code. Ordering and lifecycle faults read correctly line by line. Two firsts landed; the last lesson of the week was not to trust a code review over a real page load.

Projects

what this writeup is about