Skip to content

fix(compaction): force/truncated bypass the minMessages guard - #73

Merged
jonnyparris merged 1 commit into
mainfrom
fix/compaction-force-bypass
May 24, 2026
Merged

fix(compaction): force/truncated bypass the minMessages guard#73
jonnyparris merged 1 commit into
mainfrom
fix/compaction-force-bypass

Conversation

@jonnyparris

Copy link
Copy Markdown
Owner

What

Move the force / contextWasTruncated short-circuit above the minMessages guard in shouldCompact().

Why

Autocompaction is wired in three places (loop-entry, mid-loop, pre-step) but it was unreachable on the first turn when the model ballooned input tokens — exactly the scenario where compaction matters most.

Reproduced this morning by running 3 prompts on fresh sessions. Each had:

  • messageCount: 2 (user + assistant)
  • totalTokenInput: 199,963 / 199,662 / 213,691
  • compactionCount: 0
  • Final assistant turn: 'context budget exhausted, please retry in a new session'

Trace through the code:

  1. onChatMessage runs one streamText iteration
  2. The model makes many tool calls in that single turn (clone + explore + read…)
  3. Each tool result inflates the message array fed back to streamText, but is not persisted as a separate role:assistant row — messageCount stays at 2
  4. runWatchdogCheck / wrap-up / pre-step compaction all call maybeCompactContext({ force: true })
  5. maybeCompactContext calls shouldCompact({ messageCount: 2, force: true, … })
  6. shouldCompact returns false because messageCount < 6, ignoring force
  7. No compaction happens, the wrap-up injection fires, session ends prematurely

The minMessages guard was added to avoid pointlessly compacting brand-new sessions (compacting one message is a no-op). But force and contextWasTruncated are emergency signals — the caller has already decided compaction is warranted. They should bypass the guard.

How

Reordered the two checks. force / contextWasTruncated now short-circuit immediately, requiring only realMessageCount >= 2 (you need something to compact). Non-emergency compaction still respects the minMessages threshold.

// Before
if (messageCount < minMessages) return false;  // force never reached
if (force || contextWasTruncated) return true;

// After
if (force || contextWasTruncated) return realMessageCount >= 2;
if (messageCount < minMessages) return false;

Tests

  • New: force=true bypasses the minMessages guard (2 messages is enough)
  • New: contextWasTruncated=true bypasses the minMessages guard
  • New: force=true with only 1 real message still returns false (nothing to compact)
  • All 21 existing compaction tests still pass (compaction-policy-unit, compaction-pipeline, compaction-e2e)

Linked context

beep-boop-🤖

Autocompaction was unreachable on the first turn even when the model
ballooned the input past 200k tokens, because shouldCompact()'s
minMessages>=6 guard returned false BEFORE the force/contextWasTruncated
short-circuit ran.

Symptom in production: 3 separate sessions ran a single assistant turn
with many tool calls (clone + explore + read…) that pushed
totalTokenInput past 199k. messageCount stayed at 2 (user + assistant).
compactionCount stayed at 0. The 'wrap-up' system injection fired
when the budget warning hit ~80%, the model emitted a 'context
exhausted' summary, and the session went idle without ever compacting.

Fix: move the force/contextWasTruncated check above the minMessages
guard. They are emergency signals — the caller has already determined
compaction is warranted. We still require >=2 real messages because
compacting one or zero messages is a no-op or crash.

Tests:
- New regression test: force=true with 2 messages now returns true
- New regression test: contextWasTruncated=true with 3 messages now returns true
- New edge case: force=true with 1 message still returns false
- All 21 existing compaction tests still pass
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant