0010 — A finished session leaves, by its own exit path, never by a signal

What b88294643 actually said, and why it is being amended within a day

After the 2026-09-01 kernel panic (88 sessions, 114.8 GB on a 64 GB box) Gera was offered a cap, a reaper, or both. He chose the cap:

"Cap only: refuse to start new sessions beyond a concurrency ceiling (~35 on this hardware), never killing a running one"

and added, in his own note: "cap but also return a log saying we have hit the limit; let's not create more until they're done; and make sure we clean them when they are done."

session-cap (local-bin#64) shipped those four clauses. The fourth — CLEAN — was implemented as session-cap reap, whose predicate is tmux's pane_dead.

That clause is a no-op for memory, and the same day proved it. pane_dead means the process has ALREADY EXITED, so its resident pages are already gone; removing the tmux session returns a slot in the tmux sense only. Measured 2026-09-02: the cap REFUSED a start at 36/35 within hours of shipping, 14 finished sessions were holding 6.4 GB between them, and reap found nothing to remove — because every one of those processes was still running. Closing them by hand took free memory from 0.8 GB to 15.5 GB.

So the cap without a working fourth clause is the paralysis its own docstring warns about: a fleet that cannot start anything, on a box full of sessions that have nothing left to do. Something has to end a process. A dead pane is the aftermath, not the act.

The decision

  1. A finished session leaves through its own exit path. claude stop <id> for a background session; /exit typed into the pane for an interactive one. Both run the session's Stop hooks, flush its transcript, and leave the conversation resumable. There is no SIGTERM and no SIGKILL anywhere on this path, and none may be added.
  2. The Supervisor asks before it acts, and gives up before it escalates. exits.ts runs after releases.ts on the same terminal task: it sends one message, waits, requests the exit at most three times, and then raises it for a person. It never reaches for a stronger instrument.
  3. A schedule expires the sessions no task can reach. session-cap expire (every 30 min) covers the untasked majority, on a 48-hour window.
  4. A task that is not terminal protects its session, always — including awaiting-human. Those lanes are held open for Gera; ending one is a decision he has not been asked to make.

Why this is not the reaper he declined

The reaper was declined for a reason with a date. On 2026-08-08 this fleet's idleness detection misread a transient probe failure and retired 7 of 10 loops whose tasks were still active. That failure had three properties, and this design breaks all three:

2026-08-08 here
the trigger was a status word from a probe the triggers are timestamps; a status may only ever VETO an expiry, never cause one
a transient failure produced a plausible wrong reading ("gone") a failed read produces no reading; every unknown holds
it acted per row, in a burst, within seconds at most 3 per run, and if >25% of the fleet qualifies at once it acts on none and alarms

And the act itself is different in kind. Gera's objection was that a killed session's context is not recoverable. That is true of a signal against a mid-turn session; it is not true of a graceful exit. Verified live on 2026-09-02: a 355 MB background session was stopped through this path, its transcript grew on the flush (50,910 → 52,436 bytes), and claude --resume came back with full context. A wrongly-exited session costs one resume.

The asymmetry Gera weighed — "a refused start is reversible, a kill is not" — therefore lands differently once the act is reversible and the subject is a session that is terminal, released, idle, silent for fifteen minutes, unattached, with a drained inbox, and asked first. What is not reversible is the other side: the panic took all 88 sessions at once with nothing flushed.

releases.ts rule 3 is narrowed, not repealed

Release states:

IT RELEASES A LOOP, NEVER A SESSION. nudge --stop --sid kills the watcher process; it does not kill the Operator, which may legitimately still be writing its close-out.

That rule is right at the moment it applies. Release fires the instant a task turns terminal, and at that instant there is no evidence either way about whether the Operator is mid-close-out — so refusing to touch the session is the only honest move, and release keeps doing exactly that.

The exit phase runs later, on the same task, with evidence release never had: fifteen minutes of transcript silence, roster idle rather than busy or waiting, no attached client, a drained inbox, and a grace window since the release. Rule 3's objection is about the moment, not about the act — so the answer is a later rung, not a change to release.

What was measured, and what was not

Measured on the mini, 2026-09-02:

What is durable, and is not a post-reboot artifact: the 10 awaiting-human tasks have been waiting 46 to 392 hours, and their Operators are respawned as they die. Measured by process-tree RSS rather than extrapolated: 5.4 GB across the ten, 389–790 MB each — about 35% of the fleet's entire 15.3 GB, held by lanes waiting on an answer. The oldest, sess-agent-smith-d5, has waited 269 h (11 days).

That is the largest identified block of memory held by sessions with nothing to do, and this ADR deliberately does not touch it. Bounding a lane that is waiting on Gera is a decision for Gera — it belongs on the Decisions page, not in a daemon.

Consequences