Skip to content

fix(compaction): apply warm summaries anchored to the summarized prefix - #853

Merged
code-yeongyu merged 4 commits into
mainfrom
fix/warm-summary-anchor
Aug 13, 2026
Merged

fix(compaction): apply warm summaries anchored to the summarized prefix#853
code-yeongyu merged 4 commits into
mainfrom
fix/warm-summary-anchor

Conversation

@code-yeongyu

@code-yeongyu code-yeongyu commented Aug 13, 2026

Copy link
Copy Markdown
Owner

Problem

The idle warm-up exists so summarization happens while the user is not waiting. Its result was
pinned to expectedRevision, a counter agent-session bumps on every appended message. A
session parked in a cache-warm wait keeps appending — wait notices, monitor state, and finally the
user's own next prompt — so the warm summary was guaranteed stale exactly when the blocking route
needed it.

Field logs from a real session:

04:35:04  speculative_started  gen11        # warm-up begins
04:39:44  threshold_trigger                 # user sends a message
04:39:44  warm_consumed        gen11
04:39:44  speculative_stale    gen11        # warm summary thrown away
04:40:33  compaction committed 361068 -> 21553   # 49s fresh summarization, user waiting

Aggregate over that log: 415 warm-ups started, 36 consumed, 26 stale, 10 applied. Commit
3689633 previously made stale non-terminal (regenerate instead of "Compaction did not apply"),
which made the path correct but left the warm result unusable.

Fix

A summary describes the entries before its cut point, so growth after that cut cannot
invalidate it — only a rewrite of the summarized prefix can.
CompactionPreparation.firstKeptEntryId is that cut.

  • New warm-anchor.ts predicate: the anchor still exists, the prefix ids are unchanged, and no
    newer compaction boundary landed behind it.
  • applyGeneratedCompaction consults it when the revision moved, instead of rejecting outright.
  • ApplyCompactionOptions gains expectedFirstKeptEntryId; the core compare-and-apply gate
    re-validates the same anchor before mutating the transcript. expectedRevision is untouched for
    back-compat.

Verification

  • RED → GREEN: warm-summary-anchor.test.ts failed expected 2 to be 1 (warm summary
    discarded, a second summarization billed), then passed at one summarization.
  • Safety case included: a warm summary whose prefix was rewritten by a landed compaction boundary is
    still rejected and regenerated.
  • Compaction suite 342/342 (was 341; +2 new cases, 0 deleted). Root npm run check green.
  • Real-CLI QA (warm-summary-anchor-qa.mjs, RPC + mock provider): 8/8, compaction log records
    speculative_applied with no speculative_stale, one summarization, second reply delivered,
    real auth untouched.

One pre-existing test asserted the defect

speculative-compaction.test.ts had "skips applyCompaction when message revision changes", whose
fixture bumps the revision without mutating the session at all — provably an append-only case.
That assertion now expresses the intact-prefix contract, and a new case covers the rewritten-prefix
rejection it was standing in for.

Scope limit (measured, not assumed)

A file probe on the real CLI shows a second path that this PR does not change:

IDLE_TRIGGER
CORE_ROUTE_ENTER reason=threshold hasJob=true   # core route wins the race
BLOCKING_ROUTE_ENTER hasJob=false              # warm job already invalidated

When the core route (session_before_compact) wins, it invalidates the warm job and regenerates.
Handing the warm summary to that route needs core admission changes (a first attempt produced a
retry storm), so it is deliberately left to a follow-up rather than bolted on here.

Plan: .omo/plans/warm-summary-anchor.md


Summary by cubic

Applies idle warm-up summaries when the summarized prefix is unchanged, instead of discarding them because the message revision advanced. Previously tied to expectedRevision (stale on any append); now validated by a content anchor and still rejected when the anchor is missing or a compaction boundary rewrote the prefix.

  • Core: AgentSession.applyCompaction accepts expectedWarmAnchor and revalidates via a shared warm-anchor validator; expectedRevision remains supported.
  • Extension: speculative.ts builds a WarmAnchorSnapshot from preparation.firstKeptEntryId; uses expectedWarmAnchor when the revision changed and the anchor is valid, otherwise expectedRevision.
  • Predicate: compare the latest compaction boundary by entry id rather than array position to keep valid warm summaries in already-compacted sessions.
  • Types: ApplyCompactionOptions adds optional expectedWarmAnchor; backward compatible.
  • Tests/QA: updated speculative-compaction.test.ts, new warm-summary-anchor.test.ts, and a real CLI scenario warm-summary-anchor-qa.mjs that records speculative_applied and no speculative_stale.

Migration and rollout

  • No action required for callers using expectedRevision.
  • Extensions may pass expectedWarmAnchor to reuse warm summaries after idle appends.

Written for commit 3265c8e. Summary will update on new commits.

Review in cubic

The idle warm-up moves summarization off the user's critical path, but its
result was pinned to `expectedRevision`, a counter that increments on every
appended message. A session parked in a cache-warm wait appends wait notices,
monitor state, and finally the user's own prompt, so the warm summary was
guaranteed stale exactly when the blocking route needed it: field logs show 415
warm-ups started, 36 consumed and only 10 applied, the rest paying a second full
summarization for work already done.

A summary describes the entries before its cut point, so growth after that cut
cannot invalidate it. Validate `preparation.firstKeptEntryId` against the current
branch instead, and admit the warm result while the summarized prefix is
unchanged; a landed compaction boundary or a missing anchor still rejects.

`speculative-compaction.test.ts` asserted the defect directly - its fixture bumps
the revision without mutating the session at all - so that case now asserts the
intact-prefix contract and a new case covers the rewritten prefix.
Adds an RPC scenario that parks a session over the compaction threshold but
under the hard limit, so `before_agent_start` takes the same
`threshold_trigger` -> `applyBlockingCompaction` path as the field logs, then
asserts the compaction debug log records `speculative_applied` with no
`speculative_stale` and that the blocking path bills no second summarization.

Requests are classified by the exact compaction instruction text rather than
loose word matching, and the sandbox compaction log is captured as evidence
before teardown.
@code-yeongyu

Copy link
Copy Markdown
Owner Author

CI status: the Test (coding-agent) failures are pre-existing on main, not from this PR

main is currently red on its own merge commit. Run 31676716029 (merge of #845, no code from this branch) fails all three coding-agent shards on the same files this PR's run reports:

Shard main run 31676716029 this PR
1/3 claude-sdk-oauth-diagnostic-render, regressions/6784-claude-sdk-oauth-default-lane same cluster
2/3 claude-sdk-oauth-observability, claude-sdk-oauth-stream same cluster
3/3 claude-sdk-oauth-auth-lane, regressions/494-claude-sdk-oauth-installed-sdk-hook-stop claude-sdk-oauth-auth-lane

Every failure is in the claude-sdk-oauth cluster. No compaction test fails. Nothing in this diff touches that subsystem, and no commit between this branch's base (1b7d72e19) and current main touches claude-sdk-oauth source or tests.

Local A/B (same machine, CI=1, --shard=3/3)

files tests failed
this branch 303 14
pristine origin/main 302 14

Identical failure count and identical failing file; the totals differ by exactly this PR's +1 test file / +3 passing tests. The targeted files also pass 14/14 on both branch and main locally, and 56/56 under CI=1 with the new test files co-resident.

What this PR's own gates say

  • Compaction suite 342/342 (341 before; +2 new cases, 0 deleted)
  • Root npm run check — green
  • Static checks, Terminal tools (mac/ubuntu/windows), Inspector handoff (mac/ubuntu/windows), Test (workspaces + scripts), Changelog gate, GitGuardian — all pass
  • Real-CLI QA warm-summary-anchor-qa.mjs — 8/8, compaction log records speculative_applied with no speculative_stale, one summarization, real auth sha256 unchanged
  • cubic skipped: monthly quota exhausted (80,038/80,000 lines, resumes 1 Sep) — the only permitted skip reason

The claude-sdk-oauth breakage on main is a separate defect and deserves its own fix rather than being bundled here.

The core admission gate rejected a warm summary whenever a compaction record sat
later in the branch than the anchor. Compaction records are appended after the
entries they summarize, so a valid next-generation anchor routinely precedes the
boundary it updates: every warm summary in an already-compacted session was
discarded and re-billed, which is the steady state for the long sessions this
feature exists to serve.

Compare the latest compaction entry by ID against the snapshot instead. Simply
dropping the positional rule would have admitted a summary built on one branch's
boundary onto a sibling branch, so the snapshot now carries the anchor, its
prefix entry ids, and the boundary id, and both gates run the one validator in
core/compaction/warm-anchor.ts rather than two rules that drifted apart.

The negative test now also asserts a fresh summary is generated and applied, so
it cannot pass by the route giving up; its fixture keeps a message past the new
boundary because a compaction record at the branch tip makes prepareCompaction
bail.
# Conflicts:
#	packages/coding-agent/CHANGELOG.md
@code-yeongyu
code-yeongyu merged commit 36b15c5 into main Aug 13, 2026
15 checks passed
@code-yeongyu
code-yeongyu deleted the fix/warm-summary-anchor branch August 13, 2026 07:59
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