Delivering background job results to the turn that owns them
Binding background jobs to their owning turn and sandbox so that completions survive restarts, arrive at least once and report a result someone can vouch for.

A coding agent spends a good part of its working life waiting for things that have nothing to do with models: compilers, test suites, package installs. Run such a command in the foreground and the turn is pinned to one tool call, exposed to anything that can break that call. Run it in the background and the turn can carry on, but now someone has to find out when it finishes, and the simplest candidate is the model. It asks whether the job is done, hears that it is not, and asks again. Every one of those questions is an inference call, paid for in money and context, in exchange for a sentence containing almost no information.
Polling does have one property that is easy to overlook, which is that the answer always goes to whoever asked. Once a completion becomes an event it has to find its reader, and the reader may have moved. The turn that started the job may be in the middle of a model response, or waiting, or gone. The agent process may have restarted. The sandbox may have been replaced, in which case a report about a finished build describes a machine that no longer exists. When I connected finite background jobs to their owning turns in August 2026, the interesting part turned out to be that question of identity rather than the notification itself.
Every detached job got a receipt for each transition, started, completed, failed or cancelled, bound to a tenant, a thread, a turn and a sandbox generation. The runtime supplied that scope and the model never did, which let the host refuse to deliver a job to a turn that did not own it and cancel exactly the right job when a turn was stopped. Stopping meant more than sending a signal: closing a turn required confirmation that the process had actually stopped, and a completion that arrived after a cancel was dropped. Receipts lived in durable session state outside the model’s context. One queue owned the whole lifecycle, whichever command channel had launched the job, because the alternative was two partial systems that each knew half the story.
Delivery was deliberately dull. The host kept only the events its turn owned, flushed state, rechecked that the event was still pending, handed it to the session, acknowledged it and flushed again. If the session was mid-run the notice was steered into the active run, and otherwise it arrived as a prompt. Steering acknowledged acceptance into the session’s queue; prompting waited for processing. Meanwhile the host withheld turn completion, waiting on a change to the queue’s revision rather than on a timer. A model could still ask for status, and one that had already read the terminal result that way was not told again. Background launch required notifications to be enabled, so every admitted job had a delivery path back to its turn.
Delivery was at-least-once. An event stayed pending until acknowledged, acknowledging twice did nothing, and each event carried a stable ID built from the job and the transition. A crash after a notice went out and before its acknowledgement was flushed meant that the notice went out again. Consumers could use the stable ID to recognise a repeat; the delivery protocol itself could resend it. The turn’s ownership had been persisted before dispatch, so a restart could tell what was still owed and to whom. Recovery reattached to the existing job and replayed an unacknowledged notice rather than running the build again.
A restart put the ownership to work. The host read the saved owner back and, for each recorded sandbox generation, attached to the existing command channel through a read-only path that could not provision anything. Provisioning a new sandbox would abandon whatever was running on the old one. If the same owner and generation could not be recovered, the host stopped with an error saying that the outstanding owner was retained and no new work had been started. A saved owner with no proven generation was treated the same way, since the evidence could not say whether the crash came before or after dispatch. Persisted state that failed to parse was refused rather than discarded. When recovery did succeed, a job that had finished while the process was down produced one terminal event as its running receipt was re-armed, and receipts from other scopes were dropped on reload, so a job from an earlier sandbox could not come back dressed as current work.
A completion also has to be true. A command like cargo test | tail -n 20 ordinarily exits with tail’s status, so a successful summary can hide a failed test suite. When the launcher captured the per-stage statuses, the queue used them to report the failing test stage instead of tail’s 0; without that capture, it still had only the shell’s reported status to go on. What the model read alongside that status had to be safe as well as accurate: the notice carried a bounded tail of output and a preview of the command, both scrubbed, because the way commands reached the sandbox could otherwise have put exported credentials into its context.
In a July fix to the sandbox layer, I kept completed sessions available for a 20-second grace period and retained captured output in the SDK. If cleanup still won the race with result polling, the SDK returned that output with exit code -1: the bytes were known, the process’s exit status was not. Empty output still produced an error.
A command-channel failure had to stay separate from a sandbox failure. The optional E2B adapter attached to the sandbox the platform already owned; after three infrastructure failures by default, it fell back to the platform’s command path on that same sandbox. Its timeouts could extend the sandbox’s lease but never shorten it, and retirement stayed with the platform. Replacing the machine because one route into it was failing would abandon the jobs this recovery design was supposed to find.
Most of this code exists for the moments when the system does not know something: which turn owns a result, which sandbox ran it, how a process ended. A background job that reports to the wrong turn, or reports a success nobody can vouch for, does its damage later, when nobody is looking at it. Stopping with an error at the time is cheap by comparison.
Chris Chabot, November 2026