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))