From d2ec0a112d0a27255f09e5185eeab67dfa1593a5 Mon Sep 17 00:00:00 2001 From: CyberSecurityUP Date: Sat, 19 Sep 2026 23:16:56 -0300 Subject: [PATCH] fix(models): treat subscription session/usage-limit stdout as exhaustion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- neurosploit-rs/crates/harness/src/models.rs | 14 ++++++++++++++ neurosploit-rs/crates/harness/src/pool.rs | 1 + 2 files changed, 15 insertions(+) diff --git a/neurosploit-rs/crates/harness/src/models.rs b/neurosploit-rs/crates/harness/src/models.rs index e4bb108..84613b8 100644 --- a/neurosploit-rs/crates/harness/src/models.rs +++ b/neurosploit-rs/crates/harness/src/models.rs @@ -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) } diff --git a/neurosploit-rs/crates/harness/src/pool.rs b/neurosploit-rs/crates/harness/src/pool.rs index 1317693..e3e5c4d 100644 --- a/neurosploit-rs/crates/harness/src/pool.rs +++ b/neurosploit-rs/crates/harness/src/pool.rs @@ -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))