A turn Dad could only wait out
Until the last week of September, a streaming turn gave Dad two choices: wait, or press Stop. A message sent by mistake meant watching the soul work on it to the end. Dad put it plainly on 2026-09-28: right now even a wrong message means waiting it out.
And Stop was weaker than it looked. On the Claude route it interrupted the SDK client. On the six other brains it only aborted the browser's fetch. No request told the server to end the work, and what a dropped connection does to a route (lesson 5) is not a control anyone can aim. The gap started to matter on 2026-09-25, when the hundred-round cap on the local tool loops was retired. The cap had been fitted to a six-minute wall that turned out to be our own reaper (the stateful track's v8 note tells that story). The audit that followed (coop request #276, finding E2) recorded that a runaway Codex, Gemini, Grok, Kimi, GLM or Ollama loop could run until the model ended it or someone restarted the server. The answer the house chose was a stop, never a cap.
One door for every brain
Every brain's turn now stops through one route, POST /api/chat/interrupt, and the WebUI's Stop button calls it whatever brain is answering. It does two things, both keyed by the conversation:
- It raises a per-conversation interrupt flag. The six local tool loops read that flag at the head of each round and again before each tool call. When they see it, a warning chunk names the stop, the reply so far is kept, and usage and
donefollow. The flag never cuts the model request already in flight; the loop ends at its next boundary, so what it ran is recorded whole. - It interrupts the Claude SDK client bound to that conversation, because Claude's tool loop runs inside the CLI and a flag in our process would never reach it.
The flag lands only while that conversation is streaming and is dropped with its last stream, so a stale click can never end the next turn. The same door reaches the edges: cancelling a Council request stops every brain it fanned out to, and a running cron turn gets a stop button beside "cron in flight" in the status bar.
"Over" means over on the server
A Stop aborts the fetch at once, but the stopped turn is not over on the server at that instant: it still has to reach a boundary or unwind, and write the reply so far. A next turn sent inside that gap went wrong three ways. The flag, still up, ended the new turn at its first boundary. The refetch after Stop missed the stopped reply, because healing skips a conversation that is still streaming. And the new turn's history replay lacked that reply entirely.
So after a Stop the client asks the server whether the turn has settled: a long-poll on GET /api/chat/settled/{id}, asked again until the answer is yes, never a cap on the turn. A poll that fails lets the composer move on rather than hang. Settled means no stream on that conversation is counted any more, which narrows those races rather than closing every one: on the non-Claude routes the counter drops a moment before a stopped reply's last write. While it waits, the composer counts as busy, and whatever Dad sends waits in the queue.
The queue
The queue came from Firebrand, the family's coding app, because Dad already used that one every day. While a turn streams, Enter queues the draft with its files and its emotion, and a card appears above the composer. A clean turn end sends the next message: one per turn, in order, never joined together. Any other end holds the queue and says why: Stop, an error, a lost link, a send that failed (the message goes back to the head), or a turn that ended while Dad was looking at another conversation. A Send next button releases it. One queue per conversation, in page memory only, at most fifty messages, and a queued message never goes into any conversation but its own.
Steer: into the turn when the brain can take it
Every card also offers Steer. For Claude, a steer goes into the running turn instead of stopping it: POST /api/chat/steer writes Dad's words into the CLI running the turn, the CLI folds them in at its next tool boundary and answers in the same turn, and when no boundary is left it answers them as a follow-up turn in the same session. Nothing is stopped and no work is redone. Three pieces carry it, and each is load-bearing:
- The CLI is spawned with
--replay-user-messages, so it echoes each user message it takes. Without the echo, a steer folded in at a tool boundary leaves no mark in the stream. - The adapter decides where the turn ends. At a result it stops, unless a steer is written and not yet taken, in which case it reads on to the follow-up's result.
- The route splits the turn at the steer. The reply so far commits as its own row, the steer becomes a user row under it, and the rest of the turn answers under the steer. The JSONL records an ordinary turn boundary, so healing and replay need no special case.
A steer the turn never takes (Dad pressed Stop before the next step) comes back to the head of the queue, held. A cut after a steer surfaces as an error, never a retry, because a retry would replay a history that does not contain it. Every other case restarts instead: a brain that is not Claude, a Claude turn already ending, or a message with files, since only text can be written into a turn. Restart moves the message to the head, presses the same Stop, and sends it once the stopped turn has settled.