Skip to content

Compaction's MIN_HEADROOM_RATIO floor produces a post-compaction context cliff #150

Description

@yuanhao

Found by examples/long_horizon.rs running live against Sonnet 5 before the 0.18.0 release.

What happened

A 25-turn run over a tool returning ~60-line records, max_context_tokens: 30_000, LlmCompaction attached:

[compaction #1] Deterministic: 25 msgs / 19504 tok -> 3 msgs / 1665 tok
[compaction #2] Deterministic: 22 msgs / 21371 tok -> 1 msgs /   22 tok
[compaction #3] Deterministic: 25 msgs / 19508 tok -> 3 msgs / 1665 tok
[compaction #4] Deterministic: 25 msgs / 19892 tok -> 3 msgs / 1679 tok

Every compaction took the deterministic fallback. The briefing never landed, so the run got none of what LlmCompaction exists to provide, while still paying for the summarization requests. The agent then answered LOST when asked to recall a fact from before compaction.

Reproduced at trigger_ratio 0.6 and 0.35.

Why (partly understood)

Summarization is spawned when usage crosses trigger_ratio and spliced only when a later turn crosses the budget. With bulky tool output, the span between those two points is one or two turns — less than a summarization round-trip — so compact() finds Phase::Ready empty and falls back.

examples/llm_compaction_live.rs already documents the knob: "YO_TRIGGER … lower buys wall-clock headroom". But lowering it from 0.6 to 0.35 did not help here, which suggests the window is bounded by turns, not token fraction — one turn of a bulky tool can cross the whole gap regardless of where the trigger sits.

compact_headroom_turns exists and may be the right lever; it was left at its default in this run and is untested live.

Not explained

Compaction #2 collapsed 22 messages / 21371 tokens to 1 message / 22 tokens, far below keep_first(2) + keep_recent(6).

This did not reproduce in isolation. compaction_keeps_a_usable_well_formed_transcript (added in src/context.rs) drives the same shape through compact_messages and retains 15–24 messages / ~12.5k tokens, correctly and without orphaning tool calls. So the collapse comes from the live path — most likely LlmCompaction's own shrink_tail, or compaction running on already-compacted history — not from compact_messages.

Why it matters

Not a correctness bug: the loop always makes progress and the fallback is deliberate ("a slow or dead summarizer can never wedge it"). But a user who attaches LlmCompaction to a tool-heavy agent may pay for briefings they never receive, and silently get the lossy behaviour they were trying to avoid. CompactionMethod::Deterministic on the event is the only signal, and only if they listen for it.

Suggested work

  • Establish whether compact_headroom_turns closes the gap; if it does, say so in the LlmCompaction docs next to trigger_ratio.
  • Consider warning once per session when the fallback fires repeatedly — paying for briefings that never land is worth surfacing.
  • Explain or fix the 22 → 1 message collapse.
  • Live coverage for the splice path; every existing LlmCompaction test uses MockProvider, and none of them would have caught a fallback rate of 100%.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P3Design or idea; no implementation commitment

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions