From 5951758b350813430abd92a6fe2b1ba1478acc7a Mon Sep 17 00:00:00 2001 From: damocles Date: Wed, 20 May 2026 19:38:29 +0200 Subject: [PATCH] track compacted turns separately in stats --- hive-ag3nt/src/bin/hive-ag3nt.rs | 3 ++- hive-ag3nt/src/bin/hive-m1nd.rs | 3 ++- hive-ag3nt/src/turn.rs | 28 +++++++++++++++++++--------- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/hive-ag3nt/src/bin/hive-ag3nt.rs b/hive-ag3nt/src/bin/hive-ag3nt.rs index 16b0705..364ae1f 100644 --- a/hive-ag3nt/src/bin/hive-ag3nt.rs +++ b/hive-ag3nt/src/bin/hive-ag3nt.rs @@ -220,7 +220,7 @@ async fn serve( // compaction so it shouldn't reach here, but if it // does we also skip the ack (safer to redeliver than // to lose the message). - if matches!(outcome, turn::TurnOutcome::Ok) { + if matches!(outcome, turn::TurnOutcome::Ok | turn::TurnOutcome::Compacted) { ack_turn(socket).await; } // Rate-limited: park until the quota resets, then requeue @@ -458,6 +458,7 @@ fn build_row( }; let (result_kind, note) = match outcome { turn::TurnOutcome::Ok => ("ok", None), + turn::TurnOutcome::Compacted => ("compacted", None), turn::TurnOutcome::PromptTooLong => ("prompt_too_long", None), turn::TurnOutcome::RateLimited => ("rate_limited", None), turn::TurnOutcome::Failed(e) => ("failed", Some(format!("{e:#}"))), diff --git a/hive-ag3nt/src/bin/hive-m1nd.rs b/hive-ag3nt/src/bin/hive-m1nd.rs index 74cc488..3f712a3 100644 --- a/hive-ag3nt/src/bin/hive-m1nd.rs +++ b/hive-ag3nt/src/bin/hive-m1nd.rs @@ -186,7 +186,7 @@ async fn serve( // Ack only on a clean turn-end; Failed / RateLimited leave // the popped ids in-flight for the next boot's requeue. // Mirrors hive-ag3nt; see that loop for full rationale. - if matches!(outcome, turn::TurnOutcome::Ok) { + if matches!(outcome, turn::TurnOutcome::Ok | turn::TurnOutcome::Compacted) { ack_turn(socket).await; } // Rate-limited: park until the quota resets, then requeue @@ -378,6 +378,7 @@ fn build_row( }; let (result_kind, note) = match outcome { turn::TurnOutcome::Ok => ("ok", None), + turn::TurnOutcome::Compacted => ("compacted", None), turn::TurnOutcome::PromptTooLong => ("prompt_too_long", None), turn::TurnOutcome::RateLimited => ("rate_limited", None), turn::TurnOutcome::Failed(e) => ("failed", Some(format!("{e:#}"))), diff --git a/hive-ag3nt/src/turn.rs b/hive-ag3nt/src/turn.rs index 06c63b4..69843e7 100644 --- a/hive-ag3nt/src/turn.rs +++ b/hive-ag3nt/src/turn.rs @@ -168,6 +168,11 @@ pub async fn write_system_prompt( #[derive(Debug)] pub enum TurnOutcome { Ok, + /// Turn completed and proactive context-size compaction fired afterwards. + /// Treated like `Ok` for ack and failure-notification purposes; recorded + /// as `result_kind = "compacted"` in turn stats so the stats page can + /// distinguish normal turns from turns that triggered a compaction. + Compacted, /// claude saw "Prompt is too long" — the session needs compacting. /// Run `compact_session()` then retry the same wake-up prompt. PromptTooLong, @@ -265,9 +270,12 @@ pub async fn drive_turn(prompt: &str, files: &TurnFiles, bus: &Bus) -> TurnOutco // Proactive: a turn just completed on a still-healthy session. If its // context crossed the watermark, checkpoint + compact before a later // turn overflows into the reactive path. Best-effort — never changes - // the outcome of the turn that already succeeded. + // the outcome of the turn that already succeeded, but records it as + // `Compacted` so turn stats can distinguish it from a plain `Ok`. if matches!(outcome, TurnOutcome::Ok) { - maybe_checkpoint_and_compact(files, bus).await; + if maybe_checkpoint_and_compact(files, bus).await { + return TurnOutcome::Compacted; + } } outcome } @@ -276,17 +284,18 @@ pub async fn drive_turn(prompt: &str, files: &TurnFiles, bus: &Bus) -> TurnOutco /// has crossed the watermark, run one notes-checkpoint turn so the agent /// can persist durable state, then `/compact`. Best-effort: a failed /// checkpoint or compaction is logged + surfaced as a Note but never -/// fails the turn that already succeeded. -async fn maybe_checkpoint_and_compact(files: &TurnFiles, bus: &Bus) { +/// fails the turn that already succeeded. Returns `true` if compaction +/// was attempted (watermark crossed), `false` if skipped. +async fn maybe_checkpoint_and_compact(files: &TurnFiles, bus: &Bus) -> bool { let watermark = compact_watermark_tokens(bus); if watermark == 0 { - return; // proactive compaction disabled + return false; // proactive compaction disabled } let Some(used) = bus.last_ctx_usage().map(|u| u.context_tokens()) else { - return; // no usage reading yet — nothing to compare against + return false; // no usage reading yet — nothing to compare against }; if used < watermark { - return; + return false; } bus.emit(LiveEvent::Note { text: format!( @@ -298,7 +307,7 @@ async fn maybe_checkpoint_and_compact(files: &TurnFiles, bus: &Bus) { // session is somehow already too far gone to run even this, fall // through to compaction anyway — the checkpoint is best-effort. match run_turn(CHECKPOINT_PROMPT, files, bus).await { - TurnOutcome::Ok => {} + TurnOutcome::Ok | TurnOutcome::Compacted => {} TurnOutcome::PromptTooLong => bus.emit(LiveEvent::Note { text: "checkpoint turn overflowed the window — compacting without it".into(), }), @@ -315,6 +324,7 @@ async fn maybe_checkpoint_and_compact(files: &TurnFiles, bus: &Bus) { text: format!("/compact after checkpoint failed: {e:#}"), }); } + true } /// Pre-turn auto-reset check. If context is large AND the prompt cache has @@ -360,7 +370,7 @@ fn maybe_auto_reset(bus: &Bus) { /// agent and manager loops agree on outcome semantics. pub fn emit_turn_end(bus: &Bus, outcome: &TurnOutcome) { match outcome { - TurnOutcome::Ok | TurnOutcome::PromptTooLong => { + TurnOutcome::Ok | TurnOutcome::Compacted | TurnOutcome::PromptTooLong => { bus.emit(LiveEvent::TurnEnd { ok: true, note: None,