Capacity Guard & Account Failover
Every autonomous surface in vita runs on a Claude account with usage limits. When an account exhausts, the surfaces that depend on it die β silently, and usually overnight. The capacity guard's job is stated plainly:
bio-zack stops being the failover mechanism.
Capacity exists during nearly every outage. It just wasn't being selected.
Status: phases 1β3 shipped 2026-07-26 (
dd07d598e,0cbb34343), fromplans/proposals/2026-07-06-capacity-kill-guard-and-spine-failover.mdβ ranked #1 [94] across 11 sleep revisions and unbuilt the whole time.
Why
Five user-facing exhibits in three weeks:
| Date | What happened |
|---|---|
| Jul-6 | /sleep dark all night while another account sat at 7d:0% usage |
| Jul-8 | a live user turn was dropped; /sleep dead at reflect, exec loop dead 5β11am, telegram crashed |
| Jul-10 | account-2 hit max β bio-zack manually failed over |
| Jul-18 | manual failover again |
| Jul-26 | his first action home from a family trip was hand-switching accounts after a weekly limit killed morning_kickoff, deferred_prompt, mastermind_ideas, garmin_review, and board_meeting |
The canonical exhibit is the Jul-8 blackout (executive cycle c404), where the credit-exhaustion and wedged-stepwise signatures were deliberately kept unflattened in the record because they are different failures with different fixes.
Architecture
a surface fails
β
βΌ
classify_failure(text)
β
βββ "cancelled" βββΆ not a capacity problem, don't failover
βββ "error" βββΆ ordinary failure, normal retry
βββ "capacity_kill"
β
βΌ
parse_reset(text) βββΆ CapacityKill(reset_at)
β
βΌ
record_block(account, reset_at) data/capacity/blocks.json
(confirmed exhaustion OUTRANKS stale telemetry; expires at reset)
β
βΌ
pick_failover_target(states, exclude=blocked)
β 3-tier availability model
β (fresh telemetry > aged telemetry > unknown)
βΌ
switch_account() βββΆ verify with `claude auth status`
β
βββ success βββΆ record_event(action="failover") βββΆ alert
βββ no target ββΆ defer_run(until=reset_at) βββΆ alert
Reading the real error vocabulary
The guard does not pattern-match optimistically. classify_failure() works
against the actual Anthropic capacity-kill strings recorded from live
failures, in both forms they appear, and distinguishes three classes:
capacity_killβ a genuine limitcancelledβ a turn that was interrupted (never triggers failover)errorβ anything else
Two guards keep it honest. _NOT_CAPACITY_SIGNS and a _USAGE_CONTEXT
requirement prevent false positives on messages that merely mention "limit".
And a loud parse_miss antibody fires when a message looks like a capacity
kill but the reset time can't be parsed β rather than silently guessing a reset
and re-arming into a still-dead window.
The blocks ledger
data/capacity/blocks.json records confirmed exhaustion, and it outranks
telemetry.
This matters because the usage telemetry in ~/.claude/accounts/usage.json can
be stale β it reflects the last time a client wrote it, not the current truth.
An account that just returned a capacity kill is definitively blocked, whatever
the telemetry says. Blocks expire automatically at the parsed reset time, so a
stale block can't strand an account that has since recovered.
Account selection uses a three-tier availability model that prefers fresh
telemetry (5-hour window under ~90 min old, 7-day window under ~24 h) and
degrades explicitly to unknown rather than assuming availability. The tier
that produced a choice is recorded on the event, so "the guard switched onto an
account that was also exhausted" is diagnosable rather than mysterious.
Wiring
The guard is not a standalone daemon β it sits in the paths that actually die:
_sleep_loop(api) β a capacity kill now stops the 5/15/30-minute retry burst instead of exhausting all three attempts inside the same dead window._run_sleeppreflight β checks before starting rather than discovering mid-run.prompt_executorβ a capacity-killed deferred prompt is re-armed past the reset instead of being destroyed. This is the leg that turns a dropped user request into a late one.
Alerts that can't be silenced
Part of the original failure was that the alerts about capacity kills were
themselves being dropped by admission control. notify_sleep_failure now uses
on_reject="raise", and both it and notify_capacity_event are exempt from
admission rate limits. An alert about the spine being down is not a candidate
for rate-limiting.
Honest-failure ledger: data/capacity/events.jsonl. Kill switch:
data/capacity/auto_failover_off (create the file to disable automatic
failover entirely).
Phase 3: verification legs
Shipped the same night (0cbb34343):
- Morning-after verifier β checks that what the guard claimed it did overnight actually happened, closing the loop on the guard's own reporting.
- Kill sweeper β sweeps stepwise jobs killed by capacity exhaustion so they surface as capacity events rather than mysterious wedges.
- Real error vocabulary β the
classify_failure/parse_resetwork described above.
A companion leg, _run_services_liveness() in
flows/sleep/data_source_health.py, gives every ./run service a nightly
liveness row β the service-monitor included, since an unwatched watcher was its
own gap (issue f576911c).
Honest gaps
- The guard has never fired in anger. Everything is verified against real recorded error strings and live telemetry/queue/ledger paths β not against a genuine kill. That is the single biggest caveat on this page.
- Failover is still manual for some surfaces. The wiring covers the sleep loop, sleep preflight, and deferred prompts; other surfaces still die.
- Service liveness is nightly detection only β no real-time alert, no auto-recovery.
- Phase 4 (exec-loop event labeling, degraded-
/sleepmode, model right-sizing) is unbuilt.
Success and atrophy signals
Working: the next real capacity kill produces an events.jsonl row, a
failover (or a deferred queue.db row), a delivered alert, and a completed run
β with zero bio-zack involvement.
Failing: a kill happens and the ledger is empty, or the guard repeatedly switches onto accounts that are also exhausted (check the recorded signal tier).
Atrophy: if six months pass with no firing and no manual failovers either, the fleet grew enough headroom and this can be retired.
Related
- Verification & Anti-Lying Machinery β reliability on the claims axis
- Scheduling Engine β the loops the guard protects
- Executive Loop β the biggest consumer of account capacity
- Operating Vita β
./runservice management