Repository navigation
A possessive weekly or per-model limit is QUOTA, not RUNTIME (refresh of #1215) #2491
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
34f62dc
ec06516
d9e1d53
3b3c1da
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -27,6 +27,7 @@ export const SPAWN_RETRY_JITTER_MAX_RATIO = 0.2; | |
| // outage has already been misclassified. Twice now: | ||
| // 2026-08-03 codex "Your workspace is out of credits." → `out of credits` | ||
| // 2026-08-18 claude "You've hit your session limit" → `session limit` | ||
| // 2026-08-25 claude "You've hit your weekly limit" → the phrase below | ||
| // The second one is the instructive failure: `usage limit` was already here — | ||
| // it is Claude's OTHER exhaustion wording — so the fleet stalled for an hour on | ||
| // a string one word away from a pattern we had. Both times the miss meant | ||
|
|
@@ -62,7 +63,38 @@ export const SPAWN_RETRY_JITTER_MAX_RATIO = 0.2; | |
| // nothing else until the kernel retired the event. Only the refusal wordings | ||
| // themselves are matched, so an ordinary failure that mentions a flag is not. | ||
| const CONTENT_REFUSED_RE = /(?:flagged for possible cybersecurity risk|this content was flagged|content[_ ]policy[_ ]violation|cyber[_ ]policy)/i; | ||
| const QUOTA_RE = /(?:quota|usage limit|session limit|credit balance|out of credits|billing|insufficient[_ -]?quota|resource exhausted|spending limit)/i; | ||
| // The third miss is why there is now a PHRASE as well as a word list, and it is | ||
| // a different failure from the first two. Measured across the fleet's logs on | ||
| // 2026-08-25: 283 failures reading "You've hit your weekly limit", 55 reading | ||
| // "You've hit your session limit" (matched), and 6 reading "You've reached your | ||
| // Fable 5 limit" (not matched). Three wordings, one sentence shape, and the list | ||
| // had caught exactly one of them. Enumerating per-wording keeps failing because | ||
| // the variable part is a BILLING PERIOD or a MODEL NAME, both of which the | ||
| // providers keep adding, so a list is structurally one release behind. | ||
| // | ||
| // `QUOTA_POSSESSIVE_RE` matches that shape and nothing looser: the subject must | ||
| // be the caller's own allowance ("you've hit/reached YOUR ... limit"), which is | ||
| // what distinguishes exhaustion from a server-side throttle. The negative | ||
| // lookahead keeps "you've hit your rate limit" out — QUOTA is tested first, and | ||
| // without it that string would take the 15-minute cooldown instead of the | ||
| // 60-second rate-limit backoff. The length bound stops it spanning sentences. | ||
| // | ||
| // The character class admits a dot ONLY when a digit follows it, and that | ||
| // carve-out is load-bearing rather than cosmetic (sprint-review, 2026-08-25): | ||
| // a plain `[^.\n]` reads as "stay inside one sentence" and silently also means | ||
| // "no model names". Every model we run is dotted, so "you've reached your Haiku | ||
| // 4.5 limit" fell through to RUNTIME, the shortest backoff — the class excluded | ||
| // the exact variable part this paragraph says keeps changing. | ||
| // | ||
| // Known limit of `\.(?=\d)`, kept rather than papered over: a period with a | ||
| // digit immediately after it is admitted even at a sentence boundary, so | ||
| // "You've reached your goal.5 things have no limit" classes as QUOTA. The space | ||
| // in ordinary prose ("goal. 5 things") is what terminates the match, and | ||
| // adjacent `.`+digit is rare enough in provider copy to accept. The test below | ||
| // pins what the pattern ACTUALLY has — a space after the period ends the match — | ||
| // rather than the stronger property it does not have. | ||
| const QUOTA_RE = /(?:quota|usage limit|session limit|weekly limit|credit balance|out of credits|billing|insufficient[_ -]?quota|resource exhausted|spending limit)/i; | ||
| const QUOTA_POSSESSIVE_RE = /you'?ve (?:hit|reached) your (?!rate[ -]?limit)(?:[^.\n]|\.(?=\d)){0,40}?limit/i; | ||
| const RATE_LIMIT_RE = /(?:rate[ -]?limit|too many requests|\b429\b|overloaded|capacity)/i; | ||
| const CONFIGURATION_RE = /(?:ENOENT|command not found|not on PATH|login required|not logged in|invalid api key|authentication failed|unauthori[sz]ed|forbidden|\b40[13]\b)/i; | ||
|
|
||
|
|
@@ -85,6 +117,17 @@ export const classifySpawnFailure = (error) => { | |
| // Prefer the more specific body/message over the generic status code so a | ||
| // hard quota failure gets the long cooldown rather than a one-minute probe. | ||
| if (QUOTA_RE.test(text)) return SPAWN_FAILURE_CLASS.QUOTA; | ||
| // The possessive shape is deliberately loose about what sits between "your" | ||
| // and "limit", and its `(?!rate[ -]?limit)` lookahead only guards the word | ||
| // IMMEDIATELY after "your" — so "you've hit your API rate limit" walked past | ||
| // it into the 15-minute circuit, where main correctly gives the 60-second | ||
| // rate-limit probe (otto, gate on #2491; it was a regression, not a gap). | ||
| // A throttle wording anywhere in the text therefore wins over the shape. | ||
| // QUOTA_RE is still tested first, so an explicit quota word keeps QUOTA even | ||
| // when the same message also mentions rate limits. | ||
| if (QUOTA_POSSESSIVE_RE.test(text) && !RATE_LIMIT_RE.test(text)) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Keep a textual If the error message is 🤖 Prompt for AI Agents
Source: Coding guidelines |
||
| return SPAWN_FAILURE_CLASS.QUOTA; | ||
| } | ||
| if (error?.status === 429 || RATE_LIMIT_RE.test(text)) { | ||
| return SPAWN_FAILURE_CLASS.RATE_LIMIT; | ||
| } | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.