Skip to content

Hooks: add ContinuationGate to auto-continue unfinished work - #2133

Open
rathko wants to merge 2 commits into
danielmiessler:mainfrom
rathko:continuation-gate
Open

rathko wants to merge 2 commits into
danielmiessler:mainfrom
rathko:continuation-gate

Conversation

@rathko

@rathko rathko commented Sep 15, 2026

Copy link
Copy Markdown

Hooks: add ContinuationGate to auto-continue unfinished work

What this is

Claude Code ends every turn whether or not the work is done. Measured on my install: across 1,605 human turns over 30 days, 12.1% were a bare "go" — 91% of them after a reply that had asked me nothing. Median dead time before each: 357s. LifeOS already knows what "done" means (ISC criteria); this adds the converse: when the declared work is demonstrably not done, hand the run one more turn instead of handing it back.

ContinuationGate is a Stop gate, registered last in StopGates — every stop-reason outranks the one continue-reason. It continues only when ALL hold, and any failure lands on hand-back (today's behavior):

  1. Armed — ships in shadow (cap 0): verdicts logged to MEMORY/OBSERVABILITY/continuation-gate.jsonl, never acted on, and no model is ever called while unarmed. Three arming surfaces: say "auto-continue for 2 hours" in a prompt (deterministic UserPromptSubmit parser → session-scoped expiring grant, 12h/50-continue clamps, "auto-continue off" revokes; mention or deliberation never arms), bun LIFEOS/TOOLS/ContinuationDoctor.ts --arm N for a standing cap, or per-ISA autocontinue: N frontmatter. Separate keys (isa / all / grant) so nothing arms by accident through one knob. A grant buys unattended TIME, never unattended judgement — questions, tool errors, and no-work turns still hand back.
  2. The reply asked the principal nothing — no AskUserQuestion (a truncated transcript scan that can't find the turn boundary reads as "asked"), no question in prose (conservative regex; a false positive costs one hand-back).
  3. Clean tool evidence this turn — every TxEvent except user-text is tool-derived; any isError hands back.
  4. Work is unfinished — ISA path: ≥1 open ISC criterion, and the continuation names the next one. No-ISA path: a haiku-tier judge via the existing TOOLS/Inference.ts; only an explicit, well-formed {"finished": false} continues — crash, timeout, prose, hedge, wrong shape, extra keys, or an ambiguous double-verdict all hand back.
  5. Budget — per-run consecutive-continue counter (reset when the principal speaks), hard cap 8, 45-min wall-clock ceiling, kill switch CONTINUATIONGATE_OFF=1.

Loop safety (the part that matters)

  • The gate deliberately does not short-circuit on stop_hook_active — the counter is the loop breaker instead. The cap is enforced inside an exclusive-create lockfile critical section (commitContinue: re-read, re-check freshness + cap, write, read back). A contended lock, an unpersisted write, or a present-but-corrupt state file each refuse the continue.
  • Lock release is owner-token verified; a stale lock (crashed holder, >30s) is age-broken once then re-contended through O_EXCL.
  • The judge child gets a SIGKILL backstop that fires before the outer race returns, so a hung Inference process is reaped while the hook is still alive.
  • Every failure direction was chosen so that the worst outcome of any bug is the status quo (one manual "go"), never an unbounded run.

Includes: gate-chain arbitration fix

StopGates' old inline reducer kept the FIRST object any gate returned, so a passive systemMessage from an early gate silently swallowed a decision:"block" from any gate below it. Extracted to lib/gate-chain.ts (tested): first block wins and short-circuits; a block from any gate outranks earlier non-block output. Compatibility: existing gates emit blocks or nothing on the block path, so the only behavior change is the previously-dropped case.

Deliberate tradeoffs (from three adversarial cross-model review passes)

  • Judge context is minimal by design — final message + tool names, never tool output or files. Expanding context would grow the injection surface and cost more than it improves the verdict; a steered finished:false buys at most cap-bounded extra turns of the same session with no new privileges. An echoed exact {finished,why} object in judge output is narrowed by the ambiguity refusal (two "finished" mentions = no answer) but not fully closed — accepted, cap-bounded.
  • Question detection is heuristic and English-centric — misses are cap-bounded; false hits cost one hand-back. Localized installs should extend the phrase list.
  • Transcript-shape dependence (origin.kind === "human", tool-name scan) matches what lib/transcript-evidence.ts already relies on; if the format drifts, the counter stops resetting and continuation stops sooner, not later.
  • Stale-break stat→unlink window — theoretically deletable fresh lock in a microsecond gap after a 30s-stale one; costs at most one racing commit, cap still bounds.
  • A top-level import failure in any gate kills the whole StopGates chain — pre-existing property of the consolidated dispatcher, unchanged here.
  • Armed no-ISA latency: a Stop that survives the deterministic vetoes spends up to 40s on the judge call (sized from measured haiku-via-CLI latency of 7–25s under load; the cap limits continues, not judge calls). Unarmed installs pay zero.

Evidence (author install, 18 days)

1,341 Stop verdicts: 117 continuations (~105/week armed ≈ 5–10h of dead waiting eliminated weekly at the measured 357s median); judge approved only 50 of 459 consultations (11%) — not a rubber stamp; zero runaways (max streak 3, wall-clock ceiling caught 10, zero counter failures). Reproducible on any install from continuation-gate.jsonl — shadow mode populates it without acting.

Tests

121 pass (bun test): verdict-parser strictness (12 refusal shapes + extra-key + ambiguity refusals), judge failure modes (throw/timeout/garbage/child-deadline), directive grammar (arming forms, clamps, off forms, 9 mention-is-not-consent cases, voice-request vs deliberation), grant fail-closed reading + budget raising, arm-hook merge/read-back/permissions, cap precedence + clamping for both keys, lock contention/staleness, gate-chain arbitration (block precedence, short-circuit, throw isolation), wiring tripwires (registered, LAST, judge after deterministic gates, cap-under-lock shape, ContinuationArm registered sync at UserPromptSubmit).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant