diff --git a/hive-ag3nt/src/turn.rs b/hive-ag3nt/src/turn.rs index a20c9a4..06c63b4 100644 --- a/hive-ag3nt/src/turn.rs +++ b/hive-ag3nt/src/turn.rs @@ -518,11 +518,19 @@ async fn run_claude(prompt: &str, files: &TurnFiles, bus: &Bus) -> Result<(bool, if line.contains(PROMPT_TOO_LONG_MARKER) { flag_out.store(true, Ordering::Relaxed); } - if RATE_LIMIT_MARKERS.iter().any(|m| line.contains(m)) { - rate_out.store(true, Ordering::Relaxed); - } match serde_json::from_str::(&line) { Ok(v) => { + // Rate-limit detection: only fire on JSON `error` events, + // not on arbitrary text content. An agent discussing a past + // rate limit in its response would otherwise trigger a false + // positive (the full conversation flows through stdout as + // stream-json, so any text the model outputs is visible here). + if v.get("type").and_then(|t| t.as_str()) == Some("error") { + let raw = v.to_string(); + if RATE_LIMIT_MARKERS.iter().any(|m| raw.contains(m)) { + rate_out.store(true, Ordering::Relaxed); + } + } if let Some(u) = crate::events::TokenUsage::from_assistant_event(&v) { last_inference = Some(u); } @@ -536,9 +544,16 @@ async fn run_claude(prompt: &str, files: &TurnFiles, bus: &Bus) -> Result<(bool, bus_out.observe_stream(&v); bus_out.emit(LiveEvent::Stream(v)); } - Err(_) => bus_out.emit(LiveEvent::Note { - text: format!("(non-json) {line}"), - }), + Err(_) => { + // Non-JSON stdout: raw text check is fine here since these + // are claude CLI messages, not conversation content. + if RATE_LIMIT_MARKERS.iter().any(|m| line.contains(m)) { + rate_out.store(true, Ordering::Relaxed); + } + bus_out.emit(LiveEvent::Note { + text: format!("(non-json) {line}"), + }); + } } } });