fix(models): treat subscription session/usage-limit stdout as exhaustion

Subscription CLIs (claude) report a hit session limit as ordinary stdout with a
ZERO exit code — 'You've hit your session limit · resets …'. Left as Ok it
became a 'response' each agent then failed to parse, and the run burned every
remaining agent against a dead session instead of pausing. Now the sentinel is
caught (length-guarded so a real finding mentioning 'rate limit' is not misread)
and surfaced as exhaustion, so the pool parks the run for /continue — the
pause-on-quota path that already existed but this case never reached.

Found during a live benchmark when run B collapsed to 0 findings mid-run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
CyberSecurityUPandClaude Opus 5 committed 2026-09-19 23:16:56 -03:00
1 parent b23fae7318
commit d2ec0a112d
2 files changed
+15

No files matched your search

@@ -351,6 +351,20 @@ impl ChatClient {
if stdout.is_empty() {
return Err(anyhow!("{} subscription CLI returned empty output", bin));
}
// Subscription CLIs report a hit session/usage limit as ordinary stdout
// with a ZERO exit code — a short sentence, not an error. Left as Ok it
// becomes a "response" the agent then fails to parse, and the run burns
// every remaining agent against a dead session instead of pausing. Catch
// the sentinel (kept short so a real finding that merely mentions "rate
// limit" is not misread) and surface it as exhaustion so the pool parks.
let low = stdout.to_lowercase();
let session_dead = stdout.len() < 300 && [
"session limit", "you've hit your", "you have hit your", "usage limit",
"resets ", "reset at", "try again later", "come back later",
].iter().any(|k| low.contains(k));
if session_dead {
return Err(anyhow!("{} subscription session/usage limit reached: {}", bin, truncate(&stdout, 160)));
}
Ok(stdout)
}
@@ -15,6 +15,7 @@ pub fn is_exhaustion(e: &anyhow::Error) -> bool {
"quota", "insufficient_quota", "insufficient quota", "out of credit",
"credit balance", "billing", "exhausted", "overloaded", "capacity",
"usage limit", "resource_exhausted", "resource exhausted",
"session limit", "session/usage limit", "you've hit your",
]
.iter()
.any(|k| s.contains(k))