feat: two-phase session timeout — silent APIs die at the startup grace, named

The single spawn-armed timer charged API queue latency to the work budget:
the recurring '0 turns / $0.00 / x3 attempts' failure with four budget-bump
receipts (180->300s, 240->360s, 300->420s, 90->300s). Split: startup phase
(no NDJSON byte yet) kills EARLY at min(grace, timeout) with the distinct
exitReason 'timeout_startup' — an availability verdict, not transcript
archaeology — and the work phase arms on the first byte for the REMAINING
budget, so total wall never exceeds the timeout (tier envelopes are
margin-free: tests pass timeout: CAPTURE_MS and bun-budget the same tier).
Local grace 90s (observed queue latency 60-90s), CI floor 300s (TODOS-filed;
shared runners queue harder), both pinned by the new grace tests with fake
-claude shims covering the late-first-byte and silent-API paths.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-08-31 04:50:43 +00:00
co-authored by Claude Fable 5
parent 0c0077100a
commit 59acf5759e
3 changed files with 153 additions and 6 deletions
+4 -1
View File
@@ -59,7 +59,10 @@ describe('session-runner timeout kills the whole process group', () => {
});
const wall = Date.now() - started;
expect(result.exitReason).toBe('timeout');
// The shim never prints NDJSON, so the two-phase timer kills it in the
// STARTUP phase (grace = min(default, timeout) = 3s here) — the
// distinct reason is the point: no byte ever arrived.
expect(result.exitReason).toBe('timeout_startup');
// The old bug's signature was the drain blocking long past the budget
// (600s -> 1400s). Generous 10x bound: timeout 3s + the 5s stderr
// grace race must return promptly once the group is dead.