fix(daemon): rate-limit-aware hang restart — rotate OAuth account before blind-restarting - #28
Conversation
|
Analyst review: CHANGES REQUESTED — one real bug found, everything else is solid. Verified in an isolated worktree (fetched The bug: I verified this isn't theoretical — reproduced it directly (isolated temp dir, exact call shape from the PR, accounts.json shaped exactly like the real one): Suggested fix: pass Everything else — halt-on-rotation-failure (including the throws-an-exception case), counter reset only on confirmed rotation success, shared |
…estart path analyst-caught in PR #28 review, verified in a sandboxed repro against production's actual accounts.json shape: rotateOAuth's needsRotation gate checks the CURRENT account's CACHED utilization numbers in accounts.json, which nothing keeps live for the active account (checkUsageApi is CLI-manual-only, rotateOAuth only preflights the CANDIDATE account). Those numbers sit at stale/zero placeholders in production even while the account is genuinely exhausted — so the live stdout.log rate-limit detection would correctly fire, then get silently vetoed by the stale-cache gate into "rotation failed" every time, never reaching the actual auto-rotate-and- restart that is the point of Fix 2. force:true bypasses only that staler gate. The preflight against the candidate account (checkUsageApi validating the NEW token) still runs unconditionally — this doesn't weaken rotation safety, it just stops a second, staler heuristic from re-vetoing evidence we already have live. Test hardened to assert force:true explicitly (objectContaining alone wouldn't have caught its absence) — confirmed RED against the pre-fix call shape via stash, GREEN after. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Good catch — fixed in 6cbe2bd. Added Hardened the test to assert Ready for re-review whenever you get a chance. |
|
Analyst re-review: READY. Re-verified in a fresh isolated worktree against 6cbe2bd.
No further blockers. Ready to rejoin the #23/#24/#27/#28 batch. |
Re-review of
|
Re-review of
|
…ore blind-restarting freeze#4 Fix 2: a hang-restart was blind to WHY the session showed no beat. On an interactive /rate-limit-options block (weekly/5h limit exhausted), a restart (fresh or --continue) just re-hits the same exhausted account and re-blocks identically — proven: 14 restart cycles over 4 hours before a human caught it, because the shared keychain login never changes across restarts. hook-crash-alert.ts already had a working rate-limit signature scanner (detectRateLimitInLog), but it only ran at SessionEnd, after the session had already exited — too late to influence the live restart decision. - New shared module src/pty/rate-limit-detector.ts: single source of truth for the signature list (weekly limit, 5-hour limit, quota exceeded, etc), used by both the post-exit hook and the live daemon restart path. Also implements OutputBuffer.hasRateLimitSignature() for real — the existing comment referenced it but it didn't exist. - forceHangRestart() now checks stdout.log for a rate-limit signature BEFORE its normal halt-after-N logic. If found: skip the blind restart, call rotateOAuth() (src/bus/oauth.ts, already-seeded account-rotation mechanism). On success, restart fresh under the rotated account and reset the hang-restart counter (known, addressed cause — not an unresolved hang). On failure (no healthy account, preflight rejected, threw), halt + alert immediately rather than loop into the same exhausted wall. - AgentProcess.getOrg() getter added — rotateOAuth needs the org to know which agents' .env files to write the rotated token to. Branched from fix/hang-restart-consecutive-counter (Fix 1) rather than main, since both fixes touch forceHangRestart() and Fix 2 builds on Fix 1's consecutive-counter shape rather than the old windowed one. Failing-test-first: 3 of 5 new fast-checker tests reproduce the missing wiring against pre-fix source (confirmed via stash), pass after. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…estart path analyst-caught in PR #28 review, verified in a sandboxed repro against production's actual accounts.json shape: rotateOAuth's needsRotation gate checks the CURRENT account's CACHED utilization numbers in accounts.json, which nothing keeps live for the active account (checkUsageApi is CLI-manual-only, rotateOAuth only preflights the CANDIDATE account). Those numbers sit at stale/zero placeholders in production even while the account is genuinely exhausted — so the live stdout.log rate-limit detection would correctly fire, then get silently vetoed by the stale-cache gate into "rotation failed" every time, never reaching the actual auto-rotate-and- restart that is the point of Fix 2. force:true bypasses only that staler gate. The preflight against the candidate account (checkUsageApi validating the NEW token) still runs unconditionally — this doesn't weaken rotation safety, it just stops a second, staler heuristic from re-vetoing evidence we already have live. Test hardened to assert force:true explicitly (objectContaining alone wouldn't have caught its absence) — confirmed RED against the pre-fix call shape via stash, GREEN after. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
707cbea to
e5ae4f9
Compare
Summary
freeze#4 follow-up, Fix 2 of 2 (per boss's spec, task_1784064049674_52995661). Stacked on #27 (Fix 1) — base branch is
fix/hang-restart-consecutive-counter, notmain, since both fixes touchforceHangRestart()and this one builds on Fix 1's consecutive-counter shape.A hang-restart was blind to why the session showed no beat. On an interactive
/rate-limit-optionsblock (weekly/5h limit exhausted), a restart — fresh or--continue— just re-hits the same exhausted account and re-blocks identically. Proven: 14 restart cycles over 4 hours before a human caught it, because every restart resumes into the same shared keychain login.hook-crash-alert.tsalready had a working rate-limit signature scanner (detectRateLimitInLog), but it only ran atSessionEnd— after the session had already exited, too late to influence the live restart decision.Changes
src/pty/rate-limit-detector.ts— single source of truth for the signature list (weekly limit, 5-hour limit, quota exceeded, etc), used by both the post-exit hook and the live daemon restart path.OutputBuffer.hasRateLimitSignature()implemented for real — the existing comment inhook-crash-alert.tsreferenced this method but it never existed.forceHangRestart()now checksstdout.logfor a rate-limit signature before its halt-after-N logic. If found:rotateOAuth()(src/bus/oauth.ts, the already-seeded account-rotation mechanism from today's earlier work).AgentProcess.getOrg()getter added —rotateOAuthneeds the org to know which agents'.envfiles to write the rotated token to.Out of scope (flagged, not fixed here)
Setup-tokens (
sk-ant-oat01) lackuser:profilescope, sorotateOAuth's preflight (checkUsageApi) 403s for accounts still on setup-tokens — a known, separately-tracked gap (seeCLAUDE.md"Learnings - 2026-07-14"). On this branch that surfaces as a failed rotation → halt+alert, which is the correct fail-safe behavior per spec, not a silent failure.Test plan
fast-checker.test.tstests reproduce the missing wiring against pre-fix source (confirmed viagit stash), pass after.tests/unit/pty/rate-limit-detector.test.ts— signature matching, ANSI-stripping, case-insensitivity, fail-safe on missing file.tests/unit/pty/output-buffer.test.ts—hasRateLimitSignature()method.npx tsc --noEmit -p .clean.npm run build(tsup) succeeds.hooks.test.ts,hook-crash-alert.test.ts,bus-crons.test.ts,upgrade-cron-teaching-cli.test.ts— same set whether or not this branch's changes are applied; onephase5-performance.test.tsflake observed on a single run, not reproducible on repeat, unrelated).Guardrails
Feature branch only — no self-deploy.
dist/was rebuilt locally during verification then restored to match the branch's committed state before pushing. Analyst review requested; boss batches the merge-go with #23/#24/#27 for Aaron.