diff --git a/hive-agent/src/main.rs b/hive-agent/src/main.rs index fb3f0203..554b8dbd 100644 --- a/hive-agent/src/main.rs +++ b/hive-agent/src/main.rs @@ -810,6 +810,52 @@ async fn handle_turn( if outcome.is_ok() { S::ack_turn(socket).await; } + handle_turn_error_recovery::(&outcome, bus, socket).await; + if let Some(stats) = stats { + // Fresh session this turn → mint a `sessions` row and set its id on + // the bus so this turn (and subsequent ones until the next fresh + // start) stamp `turn_stats.session_id`. Takes the one-shot flag + // `run_claude` set when it suppressed `--continue`. + if bus.take_fresh_session() { + let sid = stats.start_session(started_at, &model_at_start); + bus.set_session_id(sid); + } + let ended_at = chrono::Utc::now().timestamp(); + let duration_ms = i64::try_from(started_instant.elapsed().as_millis()).unwrap_or(i64::MAX); + let (open_threads, open_reminders) = S::post_turn_counts(socket).await; + let row = serve_common::build_row(serve_common::TurnRowArgs { + started_at, + ended_at, + duration_ms, + model: model_at_start, + wake_from: from.clone(), + outcome: &outcome, + bus, + open_threads_count: open_threads, + open_reminders_count: open_reminders, + }); + stats.record(&row); + } + let pending = S::inbox_unread(socket).await; + if pending > 0 { + tracing::info!(%pending, "pending messages after turn; fetching next"); + } + TurnControl { + auth_failed: matches!(outcome, Err(turn::TurnError::AuthFailed)), + } +} + +/// The non-happy-path half of `handle_turn`: react to each `TurnError` +/// variant the turn could have failed with (park-and-retry on rate-limit/ +/// stall/auth, requeue-for-a-fresh-turn on prompt-too-long/session-not- +/// found, notify the parent on a hard failure). Split out purely to keep +/// `handle_turn` itself under clippy's line-count lint — no behavior +/// change from when this lived inline. +async fn handle_turn_error_recovery( + outcome: &turn::TurnOutcome, + bus: &Bus, + socket: &Path, +) { if matches!(outcome, Err(turn::TurnError::RateLimited)) { let secs = turn::rate_limit_sleep_secs(); bus.emit_status("rate_limited"); @@ -860,39 +906,7 @@ async fn handle_turn( tracing::warn!("session-not-found; requeueing message for a fresh turn"); S::requeue_inflight(socket).await; } - if let Err(turn::TurnError::Failed(e)) = &outcome { + if let Err(turn::TurnError::Failed(e)) = outcome { S::send_to_parent(socket, format_turn_failure(e)).await; } - if let Some(stats) = stats { - // Fresh session this turn → mint a `sessions` row and set its id on - // the bus so this turn (and subsequent ones until the next fresh - // start) stamp `turn_stats.session_id`. Takes the one-shot flag - // `run_claude` set when it suppressed `--continue`. - if bus.take_fresh_session() { - let sid = stats.start_session(started_at, &model_at_start); - bus.set_session_id(sid); - } - let ended_at = chrono::Utc::now().timestamp(); - let duration_ms = i64::try_from(started_instant.elapsed().as_millis()).unwrap_or(i64::MAX); - let (open_threads, open_reminders) = S::post_turn_counts(socket).await; - let row = serve_common::build_row(serve_common::TurnRowArgs { - started_at, - ended_at, - duration_ms, - model: model_at_start, - wake_from: from.clone(), - outcome: &outcome, - bus, - open_threads_count: open_threads, - open_reminders_count: open_reminders, - }); - stats.record(&row); - } - let pending = S::inbox_unread(socket).await; - if pending > 0 { - tracing::info!(%pending, "pending messages after turn; fetching next"); - } - TurnControl { - auth_failed: matches!(outcome, Err(turn::TurnError::AuthFailed)), - } }