track compacted turns separately in stats

This commit is contained in:
damocles 2026-05-20 19:38:29 +02:00 committed by Mara
parent fceab658f4
commit 5951758b35
3 changed files with 23 additions and 11 deletions

View file

@ -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:#}"))),

View file

@ -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:#}"))),

View file

@ -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,