Lesson 7 stopped at a boundary
The kill switch in lesson 7 ends a local tool loop at its next boundary: the head of a round, or just before a tool call. That is right for the model request in flight, which is never cut. It left two things uncovered. A tool can itself run for minutes, and a stop that waits for it is not a stop. And the server can be restarted while turns are running, which is a stop nobody pressed. October 2026 closed both.
A Bash command ends with its call
The six local bridges used to run a Bash tool call as subprocess.run(command, shell=True, timeout=...). At a timeout that kills the shell and nothing the shell started. The brain read "timed out" while the command ran on: once a retry started a second copy beside the first, once the orphan wrote over a report the brain had since finished. Dad's ruling on 2026-10-04 was to stop it.
Now variants/bash_commands.run_bash_command starts each command in its own session, which makes the shell the leader of its own process group, and the worker thread that started it waits in 0.2-second slices. It ends the command on the first of three reasons: the call's timeout passed, the operator's kill switch was raised for that conversation, or the backend is shutting down. Ending means the whole process group: SIGTERM, a grace (3 s, or 0.3 s at shutdown) in which git removes its lock files and a shell runs its traps, then SIGKILL. The result says which of the three it was and that the group was killed, followed by the output up to the stop.
Four details carry the weight:
- One thread signals. Only the thread that started a command signals it, and only before it has reaped the shell, because an unreaped leader keeps its group id reserved, so a signal can never land on a group that reused the number. The kill switch and the shutdown only raise a flag.
- Every stop result starts with
Error:, which keeps it out of the stateful checkpoints: a stopped command is not a finished step. - The call never waits on what will not die. If the output stays open after SIGKILL, the call returns and says something may still be running.
- A job backgrounded on purpose outlives the call, and now outlives a server restart too. It used to sit in the server's process group, and launchd killed it at every restart without a word.
Claude is not on this path: its Bash tool is Claude Code's own. That was checked live the same day rather than assumed: a 2-second timeout on a 31.7-second sleep came back as a timeout with the sleep gone, and the kill switch ended a running sleep 0.3 s after the request.
A restart owes running turns their grace
The backend has always advertised a 30-second grace at shutdown for the detached chat pipelines (lesson 5). On 2026-10-08 the cold review measured what actually happened. launchctl kickstart -k sent SIGTERM to the launcher; with no exit timeout set, launchd killed it 5 seconds later and left its children running as orphans; and the launcher's own 3-second cut ended every running turn about a second into the backend's grace, so no pipeline's cleanup ran. The advertised grace had never been delivered once.
Now the launcher's shutdown deadline waits through the turn grace and uvicorn's window, never past the job's own exit timeout minus two seconds, and it warns at startup when the plist would cut the grace; office's plist got an exit timeout of 45 seconds. Shutdown first withdraws turns still queued for the Master Chief conversation, as interrupted, so none starts a model call the grace will then cut. And turns the engine never took come back on every client: Telegram sends tg-<chat>-<message> as its turn id and asks turn status once the engine answers, resending under the same id only if the turn is missing; the WebUI sends webui-<uuid>, and after a lost link before the start, a turn the engine never took returns its words to the queue, held. A Chief turn still waiting now reads as pending rather than 404, because a 404 there used to invite a resend that ran the turn twice.
The same door, on every device
Lesson 7's controls began on the WebUI. On 2026-10-07 the native app got them for real: Stop persists the server's stop intent, so a disconnected or relaunched phone never resends a turn Dad explicitly stopped; queues are per conversation and editable; a send can steer or queue, settling before any fallback send; and an uncertain steer is reconciled against fresh server history before it is released. The composer shows the current tool, the count, the elapsed time and the tail of the text, including for a turn running on another device when the conversation is reopened. A private conversation's controls stay in memory, and its Stop is honored even after Dad has left the conversation.