Why three live sessions stopped working, and the three holes that let them.
Three separate mechanisms sit behind that one sentence, and each one had its own hole. The sessions were not ignoring the keep-alive system. They were leaving through doors it never covered.
Session db06650b ran 5 assistant turns, then ended on an AskUserQuestion call: "What do you want done with these three Zappa documents?" Brandon had dropped in three file paths.
Why every gate missed it. All 15 keep-alive gates are Stop hooks. A turn that ends by asking a question never reaches Stop, it parks waiting on a human. The PreToolUse matchers covered only Agent|Task, Bash, and Write|Edit|NotebookEdit. A grep for AskUserQuestion across the whole hooks directory returned nothing. This was the cheapest way out of the building and it was unlocked.
Session fbd241ec ran 1,407 assistant turns and closed with "Next session picks up the sweep" and "Paste it into a fresh session and rename that session to the filename verbatim."
Why every gate missed it. The existing gates look for a completion claim: done, shipped, built, that's it. A punt never claims completion, so it does not match, so it passes. The same session also used the concurrency warning as a reason to stop: "Before I touch anything on Songsterr, that session needs to be the one that stops."
The one registered keep-alive, no_idle_stop_gate.py, returns early when the shift queue has open items, on the theory that another gate handles that case. The gate that handles it, no-quit-shift/stop_gate.py, is not registered in settings.json. Its own docstring describes refusing a stop when ready work is queued, and it never runs.
Reading the code suggests the bug. Running it settles the matter. One fixture transcript, unambiguously completion-shaped, after a real Write, with a long user message and no stop signal. The gate should block it.
### TEST A: real queue (713 queued) + completion-shaped reply -> stdout EMPTY, the gate ALLOWED the stop ### TEST B: identical reply, queue file empty {"decision": "block", "reason": "no-idle-stop gate: response is completion-shaped after substantive work, shift queue is empty [cut]"} -> the gate BLOCKED
Nothing changed between the two runs except the queue file. With queued=713 and in_progress=12 on the live queue, the open-items list was never empty, so the gate returned zero on every stop it ever saw. The keep-alive was switched off by the backlog it was built to protect.
When it did fire, its refusal text told the model how to get out: "add the phrase 'session truly complete' to the response." A gate that prints its own bypass inside the message it shows the model has no lock left. This is the "manipulate hook" half of the report.
ask_question_gate.py, PreToolUse on AskUserQuestionDenies the call by default. Two allowances stay open, both deliberate: a question about an irreversible or outward-facing act (send, publish, delete, pay, deploy) should still be confirmed, and a question is fine when Brandon's own message asked for a choice. Anything else gets a denial telling the model to pick the reading a careful colleague would pick, state the assumption in one line, and finish.
ok allow=False want=False | 'What do you want done with these three Zappa docum' ok allow=False want=False | 'Should I keep going with the sweep?' ok allow=False want=False | 'Want me to start the next chapter?' ok allow=False want=False | 'How would you like me to proceed?' ok allow=True want=True | 'Send this reply to the buyer?' ok allow=True want=True | 'Deploy this to Vercel now?' ok allow=True want=True | 'Which of these two mixes do you prefer?' SELFTEST PASS
Replayed against the real db06650b transcript, the actual bail question comes back permissionDecision: deny.
no_punt_gate.py, Stop hookTen patterns for handing work to a future session, a parallel session, or back to Brandon. It carries no escape phrase at all. The only soft exit is a real write to the shift queue inside 45 minutes, which turns the honesty spec's "no silent drop" clause from a request into a condition.
ok punt=True want=True | 'Next session picks up the sweep [cut]' ok punt=True want=True | 'that session needs to be the one that stops' ok punt=True want=True | 'What remains is running the corrected gate [cut]' ok punt=True want=True | "You'll want to run the sweep yourself [cut]" ok punt=True want=True | '### One ask' ok punt=False want=False | 'I fixed the tempo bug, re-ran the gate, 16103 [cut]' ok punt=False want=False | 'still running; 12 of 54 tabs done, continuing' ok punt=False want=False | 'could not reach Songsterr: 403 after three [cut]' SELFTEST PASS
The last three cases matter as much as the first five. An honest in-progress report and an honest blocked report both pass clean, so the gate does not punish a session for telling the truth about a wall.
no_idle_stop_gate.py, armed and de-keyedTwo changes. The open-items check now counts only queue entries touched in the last 45 minutes, so a months-old backlog stops disarming it. The phrase exit is gone, replaced by an evidence check: a URL, an http status, a real file path, or a command-output block.
### RE-TEST A: same fixture, real 713-item queue (was: allowed) {"decision": "block", "reason": "[cut]"} now BLOCKS ### RE-TEST B: same claim, with a real path as proof (empty) allowed on evidence ### RE-TEST C: the old magic phrase alone, no proof {"decision": "block", "reason": "[cut]"} phrase no longer works
| Hole | Observed in | Gate | Self-test | Registered |
|---|---|---|---|---|
| AskUserQuestion skips every Stop hook | db06650b, 5 turns | ask_question_gate.py | 7 of 7 | PreToolUse, matcher AskUserQuestion |
| Punt to a future or parallel session | fbd241ec, 1407 turns | no_punt_gate.py | 8 of 8 | Stop, hook 16 |
| Keep-alive disarmed by its own queue | every stop since the backlog grew | no_idle_stop_gate.py | 3 of 3 re-tests | already registered, now armed |
| Gate printed its own bypass phrase | no_idle_stop_gate.py reason text | replaced by evidence check | re-test C | same file |
Self-tests prove a gate refuses a fixture. The question that matters is whether a real parked session goes back to work. Session db06650b, the one that exited on the Zappa question, was sent an unpark that quoted its own dropped item back to it and told it to leave the contested Songsterr tabs alone.
The session went from 5 assistant turns to 86 and kept executing. Its own closing line: "Both hooks are real and registered. Queue write landed (q-2026-08-30-d4bad9). Now the leftovers I can actually finish here, none of which touch Songsterr."
That queue entry is the "no silent drop" clause firing for real rather than being asked for. Written 2026-08-30T18:42:30, status queued: publish the drum-notation fix on Brandon's own tab s6685613, because the two community tabs are correct and submitted and sit pending moderation, which is a wall the session cannot climb by working harder.
Both Songsterr revisions verified live, read-only, nothing submitted: Montana r8787106 and Zomby Woof r8787022. Note counts preserved exactly, 7,966 and 7,479, with the quiet hits moved from parenthesised noteheads to staccato dots. The billing document was rewritten at 18:37 with a backup kept, and its stale "item 7 was not delivered" line now reads as history rather than as a current failure.
None of that needed Brandon. Before the gates, it needed him twice: once to answer a question that was never a fork, and once to open the fresh session the handoff asked for.
ok: true immediately for any session not working on guitar tabs. Every non-guitar session gets none of it. Widening that scope is a judgment call about token spend on every stop, so it is left to Brandon.no-quit-shift/stop_gate.py stays unregistered. It refuses a stop when any item is queued. With 713 queued and 327 blocked, registering it as written would force every session to pull six-month-old work before it could ever stop. The recent-window approach in Fix 3 gets the intent without that.q-2026-08-30-d4bad9 rather than executed, because the two community revisions sit pending moderation.