Durable sessions and the interruption contract

Stop, timeout, and kill are states of a turn. They are not “the agent crashed, start over.”

Blazorly stores each session as an append-only log. Line 1 is a header; every following line is an event with a contiguous sequence number. Turns end with an explicit reason from a closed set: completed, aborted (user / parent / disposed), interrupted, error, blocked, max-tokens. Cancelled tools write a result. If a cancelled stream already produced text, that assistant message is committed with interrupted: true. A process killed mid-turn leaves a log that reloads, repairs the open turn in memory, and accepts the next user message. Measured on the project’s own evals: median 0.3 ms to abort mid-stream (in-process adapter), 1.6 ms mid-SSE on the HTTP adapter, 148 ms median to abort inside a bash process-tree kill. A 132,010-event, 24 MB production session cold-replays in 1.29 s; its full-text index rebuilds in 42 ms.

The invariants

  1. Every turn ends, exactly once, with an explicit reason. After a crash, reload synthesizes the missing end in memory; the disk keeps the honest record that the turn never ended.
  2. Pending tool calls close. Bash kills its process tree and reports aborted: true. A killed process leaves dangling calls; repair synthesizes TOOL_OUTCOME_UNKNOWN so the next model request is balanced.
  3. Partial assistant work is preserved. You keep what you read.
  4. Queued inbox items survive or are spliced out with a canceled outcome, on purpose.
  5. The log reloads and resumes. A follow-up on a killed session is an ordinary turn.
  6. Exit codes tell the truth for headless: 0 completed, 2 error/blocked, 3 aborted/interrupted.

Why this is a product feature

Interactive coding agents run long turns. Users hit Stop when the model goes the wrong way. Laptops sleep. CI watchdog timers fire. If the harness throws that work away, you pay for tokens twice and you lose the trail of what already happened. Treating interruption as an error also poisons the next prompt: the model sees a truncated tool call with no result, or it sees nothing, and either way it guesses.

The HTTP adapter has a known asymmetry in OpenAI-compatible clients: cancelling mid-SSE-read often surfaces as a stream error rather than an abort. Blazorly still closes the turn; the paper in the repo spells out why that distinction matters.

How to see it

The scored tasks are eval/tasks/interrupt-cancel, interrupt-timeout, and interrupt-restart. They run against a fake OpenAI-compatible server so the bash window is deterministic.

Related