From c8120f9edc0f98780110c24a220fe4bff987ac50 Mon Sep 17 00:00:00 2001 From: damocles Date: Mon, 25 May 2026 21:59:11 +0200 Subject: [PATCH] turn: propagate auth_failed out of compact_session --- hive-ag3nt/src/turn.rs | 50 ++++++++++++++++++++++++++++++------------ 1 file changed, 36 insertions(+), 14 deletions(-) diff --git a/hive-ag3nt/src/turn.rs b/hive-ag3nt/src/turn.rs index 8e5ceda0..afc72c27 100644 --- a/hive-ag3nt/src/turn.rs +++ b/hive-ag3nt/src/turn.rs @@ -306,9 +306,13 @@ pub async fn drive_turn(prompt: &str, files: &TurnFiles, bus: &Bus) -> TurnOutco maybe_auto_reset(bus); let outcome = match run_turn(prompt, files, bus).await { TurnOutcome::PromptTooLong => { - if let Err(e) = compact_session(files, bus).await { - tracing::warn!(error = %format!("{e:#}"), "compact failed"); - return TurnOutcome::Failed(e); + match compact_session(files, bus).await { + Ok(true) => return TurnOutcome::AuthFailed, + Ok(false) => {} + Err(e) => { + tracing::warn!(error = %format!("{e:#}"), "compact failed"); + return TurnOutcome::Failed(e); + } } run_turn(prompt, files, bus).await } @@ -370,11 +374,17 @@ async fn maybe_checkpoint_and_compact(files: &TurnFiles, bus: &Bus) -> bool { text: format!("checkpoint turn failed ({e:#}) — compacting anyway"), }), } - if let Err(e) = compact_session(files, bus).await { - tracing::warn!(error = %format!("{e:#}"), "post-checkpoint compact failed"); - bus.emit(LiveEvent::Note { - text: format!("/compact after checkpoint failed: {e:#}"), - }); + match compact_session(files, bus).await { + Ok(true) => bus.emit(LiveEvent::Note { + text: "/compact hit 401 — next turn will trigger the re-login flow".into(), + }), + Ok(false) => {} + Err(e) => { + tracing::warn!(error = %format!("{e:#}"), "post-checkpoint compact failed"); + bus.emit(LiveEvent::Note { + text: format!("/compact after checkpoint failed: {e:#}"), + }); + } } true } @@ -506,19 +516,31 @@ pub async fn run_turn(prompt: &str, files: &TurnFiles, bus: &Bus) -> TurnOutcome /// compact state matches a normal turn's. Only the prompt over stdin /// differs (`/compact` vs the wake-up payload). /// +/// Returns `true` if claude reported a 401 mid-compact — the caller +/// should bail into the re-login flow instead of treating compaction +/// as succeeded. (`prompt_too_long` / `rate_limited` are unlikely on +/// a slash command and are folded into the boolean by being ignored +/// — the next real turn surfaces them through the normal path.) +/// /// # Errors /// /// Returns an error if the `claude --print /compact` invocation fails /// (non-zero exit or I/O error). -pub async fn compact_session(files: &TurnFiles, bus: &Bus) -> Result<()> { +pub async fn compact_session(files: &TurnFiles, bus: &Bus) -> Result { bus.emit(LiveEvent::Note { text: "context overflow — running /compact on the persistent session".into(), }); - let (_, _, _) = run_claude("/compact", files, bus).await?; - bus.emit(LiveEvent::Note { - text: "/compact done".into(), - }); - Ok(()) + let (_, _, auth_failed) = run_claude("/compact", files, bus).await?; + if auth_failed { + bus.emit(LiveEvent::Note { + text: "/compact hit 401 — bubbling up for re-login flow".into(), + }); + } else { + bus.emit(LiveEvent::Note { + text: "/compact done".into(), + }); + } + Ok(auth_failed) } #[allow(clippy::too_many_lines)]