Resumability: the run outlives the connection
Lesson 3 of 5 in Sessions and Streaming: The Conversation Plumbing.
A serious agent run takes minutes. A laptop lid closes in milliseconds. Between those two numbers lives an entire class of bug.
The design principle is one sentence: the run is server-side state; the connection is a subscription to it. Kill the connection and the run keeps going — the model keeps reasoning, the tools keep firing, the refund still gets issued. What you lost is only your view of it. Every reconnection strategy is an answer to “how do I rebuild my view of a run that never stopped?”
There are two honest ways to build that, and one dishonest one. The dishonest one is treating the stream as the run: when the socket dies, cancel, retry from the top, and hope nobody notices the duplicate email. Protocols are converging on the honest answers — the MCP spec change quoted below is a good illustration of how far a serious protocol is willing to push state out of the transport.
| Strategy | What the server keeps | What reconnect costs | When it is the right call |
|---|---|---|---|
Restart the run (the dishonest one) | Nothing beyond the transcript. | Full re-execution: re-reasoning you pay for twice, and every side effect fires again unless the tools themselves are idempotent. | Only for short, read-only runs where a duplicate execution is genuinely harmless. |
Poll authoritative state | Run status and committed messages, readable by id at any time. | One or two requests. You lose the intermediate deltas and jump to “where is it now”. | The default. Simple, robust, and always available because the run already has an id and a status. |
Replay from a durable event log | Every event, with a monotonic sequence number per run. | One request carrying your last-seen sequence number; the server replays the gap, then resumes live. | When the intermediate activity matters — long runs, multi-viewer surfaces, audit trails, anything you want to render after the fact. |
Most teams need the middle row and think they need the bottom one. Polling authoritative state is unglamorous and nearly always sufficient: reconnect, ask the run for its status, re-read committed messages, resubscribe for what happens next. You give up the pretty progressive rendering of the seconds you missed; you gain a code path with almost nothing in it to get wrong.
Replay is worth the engineering when the intermediate activity is a product feature or a compliance artefact. And note what building it actually requires — not a fancier transport, but a sequence number per run and a place to keep events. Once you have those, resumption is a query.
Reconnect without breaking anything
- Stream drops mid-run
Wi-Fi, a proxy idle timeout, a deploy, a closed laptop. Treat this as routine traffic, not an exception.
- Do you have the run id?
If the run id only ever existed in the memory of the dead connection, you cannot reconnect to anything — you can only guess. Persist ids client-side the moment they are issued.
- Orphaned run: cannot address it
The run keeps executing and billing with no observer. This is why the id is written down before the first frame is rendered.
- Fetch run status by id
Authoritative. Cheap. Available whether or not you built an event log.
- Terminal status?
completed / failed / cancelled. If terminal, there is nothing to resume — read the result.
- Read committed messages, render result
The run finished while you were away. Common, and completely fine.
- Have a last-seen sequence number?
Only if the server keeps a durable, ordered event log per run.
- Replay events after cursor, dedupe by id
Replay is at-least-once by nature. Dedupe on event id and apply each event idempotently.
- Resubscribe for live frames
And keep polling status on a slow timer anyway — notifications are best-effort even in well-designed systems.
- View rebuilt, run continues
Every path through that diagram lands on the same requirement: idempotent event handling. Once a stream can be replayed, or a notification can be delivered twice, or a client can reconnect twice in a second, your handler will see the same event more than once. Delivery in distributed systems is at-least-once; exactly-once is a property you build in the consumer, not one the wire gives you.
Three cheap mechanisms carry almost all of it. Give every event a stable id and keep a small set of processed ids per run, so a repeat is a no-op. Prefer absolute state over increments in event payloads — “status is now in_progress” survives replay, “increment the step counter” does not. And put an idempotency key on anything with a side effect, so that if the same tool call reaches your payment API twice you charge once. That last one is the difference between an annoying UI bug and a customer refunded twice.
Your connection just dropped 90 seconds into a run
Interactive decision tree — outcomes:
- Poll authoritative state — and stop there
Reconnect, fetch run status by id, re-read committed messages, resubscribe. You skip the missed deltas and rejoin live. This handles the large majority of real cases with the least code, and it is the strategy the MCP docs point at when they say clients should still poll because notification delivery is not guaranteed across reconnects.
- Replay from your cursor, then dedupe
Send your last-seen sequence number, apply the gap, resubscribe. Replay is at-least-once, so dedupe on event id and keep event payloads absolute rather than incremental — otherwise your step counter double-counts every reconnect. Keep a slow status poll running as a safety net.
- Do not restart the run — fix the tools first
A run that has already sent mail or moved money cannot be safely re-executed, and a dropped socket is a routine event you will see thousands of times. Poll authoritative state to find out what actually completed, and make idempotency keys on side-effecting tools the next thing you build. This is where sessions-and-streaming plumbing turns into a real incident.
- You are asking for replay without the thing that makes replay possible
Resumability is not a transport feature you can enable; it is a durable, ordered event log with a sequence number per run. Until that exists, be honest in the UI — reconnect, show current state, and label the gap — rather than reconstructing history from whatever fragments the last socket happened to deliver.
Interactive checkpoint quiz (2 questions) — open this page in a browser to take it.