Summary
classifySpawnFailure falls through to RUNTIME, and RUNTIME is the most aggressive schedule in the table. So an error the classifier does not recognise gets the treatment reserved for "transient local fault, probe hard" — the opposite of what an unrecognised provider-side failure deserves.
This is the layer above #995, which is correct and should land as written. #995 adds one word to a vocabulary; this issue is about which way that vocabulary fails when a word is missing.
Measured
Against the module the fleet runs (cli/src/lib/spawn-retry.js, live/main-tracking @ 0cd2bdc9), intervalMs: 5000, jitter 0, * = circuit open:
class=quota n=1,2,3 -> 900s* 900s* 900s*
class=configuration n=1,2,3 -> 900s* 900s* 900s*
class=rate_limit n=1,2,3 -> 60s* 120s* 240s*
class=runtime n=1,2,3 -> 5s 10s 60s*
RUNTIME is 180× faster than QUOTA on the first retry and is the only class that does not open the circuit at n=1. It is also the value returned by the final return of classifySpawnFailure — i.e. the default for anything unmatched.
It has now happened twice, to two providers
The file records the first instance in a comment above QUOTA_RE:
out of credits is codex's exact wording for an exhausted workspace balance … Without it that outage classified as RUNTIME and drew the shortest backoff — observed live on 2026-08-03 before this pattern was added.
The second was last night: Claude's You've hit your session limit · resets 4am is not in QUOTA_RE (which does contain usage limit), so nine seats probed an account that could not answer until a fixed reset time, on a 5s/10s/60s ladder. #995 adds the string.
Two incidents, two providers, one remedy applied twice. The vocabulary will keep drifting — provider error wording is not an API and changes without notice — so each future reword silently re-selects the most aggressive branch, and the only signal is a fleet-wide stall that someone has to notice by hand.
Suggested fix
Separate "we recognise this as a transient local fault" from "we do not recognise this at all", and give the second the conservative branch:
- add
SPAWN_FAILURE_CLASS.UNKNOWN (or have the fallthrough return QUOTA-equivalent timing), scheduled like CONFIGURATION — long ceiling, circuit open immediately;
- keep
RUNTIME for failures that are positively identified as local and transient (non-zero exit with no recognised provider text, spawn ENOENT of the wrapper itself, etc.);
- log the class alongside the delay, so an unclassified provider outage is visible as
unknown in the seat log instead of presenting as an ordinary runtime blip.
The cost of getting it wrong in the conservative direction is a genuinely transient fault waiting 15 minutes. The cost in the current direction is a fleet hammering a provider through an outage it cannot shorten — and, per #993, burning deliveries that the GC then destroys.
Related pattern
Second classifier found in the same session whose unrecognised verdict resolves toward doing more work:
Both defaults are individually defensible and both bite hardest under precisely the load they exist to damp. Worth a line in docs/development/review-checklist.md: when a classifier feeds a limiter, its unknown verdict must select the conservative branch, and the reviewer should ask what produces unknown under load.
Summary
classifySpawnFailurefalls through toRUNTIME, andRUNTIMEis the most aggressive schedule in the table. So an error the classifier does not recognise gets the treatment reserved for "transient local fault, probe hard" — the opposite of what an unrecognised provider-side failure deserves.This is the layer above #995, which is correct and should land as written. #995 adds one word to a vocabulary; this issue is about which way that vocabulary fails when a word is missing.
Measured
Against the module the fleet runs (
cli/src/lib/spawn-retry.js,live/main-tracking@0cd2bdc9),intervalMs: 5000, jitter 0,*= circuit open:RUNTIMEis 180× faster thanQUOTAon the first retry and is the only class that does not open the circuit atn=1. It is also the value returned by the finalreturnofclassifySpawnFailure— i.e. the default for anything unmatched.It has now happened twice, to two providers
The file records the first instance in a comment above
QUOTA_RE:The second was last night: Claude's
You've hit your session limit · resets 4amis not inQUOTA_RE(which does containusage limit), so nine seats probed an account that could not answer until a fixed reset time, on a 5s/10s/60s ladder. #995 adds the string.Two incidents, two providers, one remedy applied twice. The vocabulary will keep drifting — provider error wording is not an API and changes without notice — so each future reword silently re-selects the most aggressive branch, and the only signal is a fleet-wide stall that someone has to notice by hand.
Suggested fix
Separate "we recognise this as a transient local fault" from "we do not recognise this at all", and give the second the conservative branch:
SPAWN_FAILURE_CLASS.UNKNOWN(or have the fallthrough returnQUOTA-equivalent timing), scheduled likeCONFIGURATION— long ceiling, circuit open immediately;RUNTIMEfor failures that are positively identified as local and transient (non-zero exit with no recognised provider text, spawn ENOENT of the wrapper itself, etc.);unknownin the seat log instead of presenting as an ordinary runtime blip.The cost of getting it wrong in the conservative direction is a genuinely transient fault waiting 15 minutes. The cost in the current direction is a fleet hammering a provider through an outage it cannot shorten — and, per #993, burning deliveries that the GC then destroys.
Related pattern
Second classifier found in the same session whose unrecognised verdict resolves toward doing more work:
classifyTriggerreturns'unknown'when the trigger message is outside a 10-message window;'unknown'is admitted unconditionally by the cascade governor and never counted toward the cap. Fails open exactly when a seat is backlogged.classifySpawnFailurereturnsRUNTIMEwhen no pattern matches;RUNTIMEretries fastest and delays the circuit. Fails open exactly when a provider is down.Both defaults are individually defensible and both bite hardest under precisely the load they exist to damp. Worth a line in
docs/development/review-checklist.md: when a classifier feeds a limiter, its unknown verdict must select the conservative branch, and the reviewer should ask what producesunknownunder load.