In the first version of this product, a chat turn was welded to the HTTP connection that started it. The tab that hit Send opened the request, read the streamed reply into component state, and that state lived and died with the component. Three things broke as a direct result: the same thread open in a second window showed nothing while the first one streamed; a refresh mid-reply lost the answer; and a second tab had no idea a turn was even running. But the deeper wrongness was environmental. This is a phone-first product, and on a phone, a dropped connection is not an edge case — the lift, the tunnel, the Wi-Fi-to-cellular handoff are the normal operating conditions. An architecture in which the work's survival depends on the connection's survival is, on mobile, an architecture that loses work routinely.
The founding move of this phase, from which everything else follows: the turn outlives the connection. The gateway forwards a chat turn to the pod deliberately detached — the client's disconnect signal is not passed along — so the model runs to completion and the finished turn persists, whatever happened to the tab. Closing, refreshing, losing signal: none of it cancels anything (stopping is its own explicit act, as an earlier entry described, precisely so that leaving and objecting stay distinguishable). Once that holds, every recovery problem changes species: recovering a reply stops being about re-executing work and becomes about reading a result that already exists. The rest of this entry is what that reframing exposed — including one place we were paying for the guarantee and then throwing it away.
Reclaim, don't resend
The audit that kicked this phase off — triggered by a user on mobile repeatedly seeing "couldn't reach" errors that a single dropped fetch had escalated into — found an asymmetry invisible to users and expensive to us. Server-side, a turn interrupted mid-stream was fine: it completed, it persisted. Client-side, the dropped stream was treated as lost work, and the UI offered a resend — which re-ran, and re-billed, a turn that had already finished. We had bought durability and were spending it on double-billing.
The fix inverts the recovery order. On a mid-stream drop after output has begun, the client does not offer anything — it shows a quiet "reconnecting" and polls the session for the reply that is, in all likelihood, completing server-side right now. If an over-eager re-send does fire while the turn is still running, the server's duplicate guard rejects it safely; the next build routes that rejection into the same reclaim poll, because "your turn is still running" is the best possible news a recovering client can receive. Only when the bounded reclaim window expires with no persisted reply does the resend affordance appear — now meaning what it says. The success criterion was written to be measurable: a network blip during a turn ends with the reply appearing, and zero recovery paths re-run completed work.
The turn that never finished finishing
Durability has a failure mode of its own, and it found us. The persistence contract writes the user's message early (so nothing typed is ever lost) and the terminal assistant reply at finalize. But a finalize is code, running in a process — and when that process dies mid-turn (a relay dropping, a pod being killed), the terminal write never happens. What remains is a session whose last message is a dangling user message: structurally indistinguishable from a turn that is in flight right now. The client's resume machinery — which on reload sees a user-message tail and reasonably concludes a turn is running — showed "picking up where you left off…", blocked sending, waited its full window, found nothing, and then did it all again on the next reload. A dead turn, wearing a live turn's clothes, forever.
The fix is a pattern we now reach for by name: a read-path reaper. When a session is loaded and its tail is a user message older than a staleness threshold — set comfortably past the load balancer's own idle timeout, so no genuinely live turn can be falsely reaped — an error bubble is synthesized and appended, marking the turn as ended by disconnection. Idempotent by construction (the appended bubble makes the tail non-user, so the check never fires twice), best-effort persisted so the record heals for future reads, and invisible to the model (display-only messages never re-enter its context). The general lesson earned its place in more than one subsystem the same month: any detached process's terminal state needs to exist — written at finalize, or synthesized, age-keyed and idempotently, on read — because an in-flight guard cannot cover a finalize that never ran, and a record that cannot distinguish "dead" from "busy" will make its readers wait for the dead.
Two tabs are not one problem
The seamless dream — a thread live in two windows, a refresh resuming mid-token — decomposes, on inspection, into two problems that only look alike. Same-browser multi-tab is a client-side problem: same origin, same device; the tabs can simply tell each other what is happening over the browser's broadcast channel, with the streaming tab as the source of truth. True resume — refreshing the only tab, or opening the thread on a phone while a laptop drives it — is a different animal entirely: after a refresh there is no peer tab to copy from, so the survivor must be server state, and today only completed turns persist; there is no in-flight record to reattach to mid-token. Filing refresh under "multi-tab" is the tempting mistake, because it is secretly the hard problem wearing the easy one's clothes.
One architectural fact keeps even the hard version tractable, and it is a dividend from a decision made months earlier for other reasons: all of a user's connections route to their single pod. Fan-out to N tabs is an in-process concern on one machine — no shared bus, no pub/sub infrastructure, no cross-node coordination. And one design judgment keeps the hard version honest: with turns durable and reclaim in place, a full mid-token resume protocol buys seconds of continuity on top of a system that already never loses the result — so it is sequenced as polish, not foundation. At this entry's date, mid-token resume remains the open gap; the reply always arrives, and the last few seconds of watching it arrive can be lost. We can live with which half we built first.
The ladder of honesty
The last piece is a UX contract, applied uniformly, for the moments recovery is in progress — because "recovers by itself" still needs an answer to what the user sees meanwhile. It is a ladder: under about two seconds, silence — retry beneath the surface, never flicker the interface for a blip. Beyond that, a quiet "reconnecting…", inline where the action lives. The moment the device itself reports offline, say offline immediately — spinning at a network the OS knows is absent is theatre. And when retries are genuinely exhausted: a clear, actionable message that names what failed, keeps the user's input, and offers one tap to retry. Two absolutes bound the ladder at both ends: never an indefinite spinner, and never silent loss. Every state resolves — to content, to honest waiting, or to an honest ask.
What transfers
Detach the work from its delivery channel first — durability converts every recovery feature downstream from re-execution to reads, which is cheaper, safer, and un-double-billable. Then audit for places you offer re-execution of work that already completed; every one is the durability guarantee being thrown away at the last hop. Give every detached process a terminal state that cannot fail to exist, synthesizing it on read where finalize can die. Split multi-tab sync from true resume before estimating either. And write the feedback ladder down as policy, because without one, each surface improvises its own spinner — and a product's trustworthiness under bad networks is exactly the sum of what it does during the worst ten seconds.
Postscript, August 2026. The read-path reaper became a family — the same age-keyed, idempotent, synthesize-on-read shape now finalises orphaned scheduled runs and stuck background jobs, which is what a good pattern does when a failure class recurs: stops being a fix and becomes a vocabulary. The gaps from this entry's date keep their ranking: mid-token resume is still sequenced behind things that lose actual work rather than actual seconds, and the offline-capable app shell — a blank page on a cold offline load — sits in the same queue. The reclaim numbers have been quiet, which is the point: the measurable promise was zero recovery-induced double-billing, and the way you know a recovery system works is that the bill stops carrying its failures.