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
Found by
examples/long_horizon.rsrunning 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,LlmCompactionattached:Every compaction took the deterministic fallback. The briefing never landed, so the run got none of what
LlmCompactionexists to provide, while still paying for the summarization requests. The agent then answeredLOSTwhen asked to recall a fact from before compaction.Reproduced at
trigger_ratio0.6 and 0.35.Why (partly understood)
Summarization is spawned when usage crosses
trigger_ratioand 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 — socompact()findsPhase::Readyempty and falls back.examples/llm_compaction_live.rsalready 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_turnsexists 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 insrc/context.rs) drives the same shape throughcompact_messagesand retains 15–24 messages / ~12.5k tokens, correctly and without orphaning tool calls. So the collapse comes from the live path — most likelyLlmCompaction's ownshrink_tail, or compaction running on already-compacted history — not fromcompact_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
LlmCompactionto a tool-heavy agent may pay for briefings they never receive, and silently get the lossy behaviour they were trying to avoid.CompactionMethod::Deterministicon the event is the only signal, and only if they listen for it.Suggested work
compact_headroom_turnscloses the gap; if it does, say so in theLlmCompactiondocs next totrigger_ratio.LlmCompactiontest usesMockProvider, and none of them would have caught a fallback rate of 100%.