A run outlives the container through a leased, fenced ledger with a write-ahead step record
Context
Under the two-lives design (0006) a run in flight during a restart was gone: its card froze, its transcript vanished and the caller waited forever. The release pipeline compensated with a gate that refused to roll the bot over while any run was live. The gate cost users nothing and cost releases a wait that stretched to hours of self-redispatch on a busy bot, and it was the only thing standing between a rollout and a killed run.
Decision
A run outlives the bot container. A rollout or a hard kill neither loses a run nor freezes its card: the next container resumes the run from a durable ledger, or closes it as interrupted with its full transcript. The run index, run pages, stop requests and thread admission all read that one ledger across container generations.
Mechanism:
- A lease with a fencing token. The container's generation (
startedAtplus a random suffix) is sent with every owner write; the ledger refuses a write from a generation that no longer holds the lease, so a zombie old container can never overwrite a run the new one resumed. Lease thirty seconds, heartbeat ten. - A write-ahead step record. Every tool call is named durably before it runs, so a resume knows what may have had effects.
- An append-only transcript in a per-run Durable Object, flushed every half second or thirty-two events, so transcript bytes never queue behind an admission decision in the index object.
- Compare-and-swap phases
live → finishing → finishedandlive → handoff. Live runs never enter the finished table, whose columns are non-null for finished facts and whosefinished_atdrives retention. A run's finish writes the finished record and deletes the lease in one transaction, which is why there is one index object and not two. - Admission becomes
thread_key UNIQUEon the live-runs table (0011).
Guardrails the design refuses to cross: no secret moves out of the process that holds it; no resume re-issues a command whose effects are unknown (a git push in flight at the kill must never run twice); at most one awaited round trip per step on the hot path; no edit to a transcript the model has already read.
A resume is a valid continuation, not a byte-identical request across a code deploy. A transcript that cannot be stored closes the run interrupted at reclaim rather than being trimmed.
With this in place the release pipeline's bot gate became a warning (0015).
Consequences
- Releases no longer wait for a quiet bot, and a kill mid-run produces a resumed run or an honest
interruptedrecord, never a frozen card. - Every owner write carries the generation, and every step pays one durable write before it runs. The budget is one awaited round trip per step.
- The two-lives shape is unchanged: the ledger is the live store, and the finished record is still written once at the end.
- Idempotent replay of a tool result after a kill is not built. A step whose effects are unknown at resume is reported, not re-run.
Alternatives rejected
- A Durable Object per run. Deferred with the full argument in 0016.
- Quiescing the bot before a rollout. Makes users wait for the deploy.
- Reconstructing the transcript from the event stream. Events omit tool inputs and cap outputs.
- Replacing the whole transcript array per step. Re-uploads the transcript every step.
Pattern
Lease with a fencing token (Kleppmann); write-ahead step record; Memento for the resumable state; compare-and-swap phases; Null Object for the off-state.