Skip to content

fix(runtime): match history-compact checkpoints against the raw ledger prefix - #4845

Open
me2seeks wants to merge 2 commits into
apache:mainfrom
me2seeks:fix/4842-compact-replay-match
Open

fix(runtime): match history-compact checkpoints against the raw ledger prefix#4845
me2seeks wants to merge 2 commits into
apache:mainfrom
me2seeks:fix/4842-compact-replay-match

Conversation

@me2seeks

@me2seeks me2seeks commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #4842.

A history-compact checkpoint was pinned at creation over raw RuntimeEvents, but pre-turn replay matched it against the transition-folded view. Any durable stale tool-result archive transition (enabled by default) inside the covered prefix made the digest mismatch, so the replay silently failed open and the next request sent the full uncompacted history — the transcript showed "Context compacted." while ctx stayed pinned at 100%.

Verified against a live session ledger: digest over the raw prefix matches the checkpoint exactly; over the folded prefix (6 transitions, 6 covered events rewritten) it reports source_hash_mismatch; the turn's own diagnostic recorded decision: 'failedOpen', failOpenReason: 'source_hash_mismatch' with droppedEvents: 0.

Fix

In buildPriorMessages (pre-turn replay):

  • match the durable checkpoint against the raw ledger prefix — the only view every creation path (standalone compactHistory, mid-turn state) hashes;
  • then fold the projected [block, tail] through reduceEffectiveModelProjections before materializing messages, preserving the refactor(runtime): unify durable model-context projection authority #4283 invariant that the model never sees content a committed transition removed.

This revives every existing raw-hashed checkpoint instead of orphaning them, and the pinned digest can no longer drift when a later transition commits (raw ledger rows are immutable).

Tests

New regression test in ai-sdk-backend.test.ts: seeds a real stale tool-result archive transition through a first send, runs standalone compaction, then asserts the next send replays through the checkpoint (summary block in the prompt, covered raw history absent). Fails without the fix, passes with it; stable across repeated full-suite runs.

  • @maka/runtime full dist suite: 3217 tests, 0 failures (3 runs)
  • typecheck + biome lint clean

Notes

…r prefix

Both checkpoint creation paths (standalone compactHistory and the mid-turn
state) pin the coverage source digest on raw RuntimeEvents, but pre-turn
replay matched the checkpoint against the transition-folded view. Any durable
stale tool-result archive inside the covered prefix then failed the digest,
the replay silently failed open, and the next request paid the full
uncompacted history while the transcript still showed "Context compacted."

Match the durable checkpoint against the raw prefix instead, then fold the
projected [block, tail] through the projection-transition reducer before it
becomes messages: existing raw-hashed checkpoints replay again, the pinned
digest can no longer drift when a later transition commits, and the model
still never sees content a committed transition removed (apache#4283).

Regression: a covered prefix carrying a stale tool-result transition fails
open before this change and replays the checkpoint after it.

Fixes apache#4842.
@github-actions github-actions Bot added the effort/M Under 500 readable lines label Sep 5, 2026
@me2seeks
me2seeks requested a review from Astro-Han September 5, 2026 09:25

@M4n5ter M4n5ter left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed exact head 0f71d6ddc22c49d6dbb578b10553a62ad30356d7. This is not safe to merge yet: matching the checkpoint against raw history fixes the reported hash mismatch, but it opens two paths that can restore Tool Result content after the durable projection ledger removed or withheld it. The two inline findings have deterministic exact-head probes and base/minimal-fix controls. I found no separate simplification issue.


Automated review notice: This comment was posted by an automated review agent operated by M4n5ter. It is not an independent human review and does not replace one.

// (#4842). The projected [block, tail] is then folded through the
// transition reducer before it becomes messages, so a committed
// transition still cannot resurrect content for the model (#4283).
const budgeted = applyRuntimeEventContextBudget(rawPriorRuntimeContext, contextBudget);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Do not make a raw-derived checkpoint authoritative after a projection transition. Both checkpoint creation paths still summarize raw RuntimeEvents; the standalone path even exposes them as source.foldedRuntimeEvents. I changed this regression's summarizer to echo a real stale Tool Result that had already been archived by a durable transition. On this head, the next provider prompt replayed that value inside the checkpoint block; with only these two changed replay lines restored to the base ordering, the checkpoint failed open and the prompt contained only the archived placeholder. This therefore turns an existing raw-created checkpoint into a persistent content-restoration authority, contrary to the invariant that compaction must consume effective model history. Please separate immutable coverage identity from model-visible summary input (or version the checkpoint contract), make new standalone and mid-turn summaries consume the effective prefix, and add an echoing-result regression that proves later prompts cannot restore the transitioned body.

// transition reducer before it becomes messages, so a committed
// transition still cannot resurrect content for the model (#4283).
const budgeted = applyRuntimeEventContextBudget(rawPriorRuntimeContext, contextBudget);
let runtimeContext = await this.deps.compaction.foldEffectiveModelHistory(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Preserve scoped unreadable-target withholding in this second fold. prepareContextBudgetPolicy() correctly passes loaded.unreadableTargets to the reducer, which replaces a target with the safe projection-failure sentinel because an undecodable record may be the record that removed its raw body. This new path discards that result and refolds from raw through foldEffectiveModelHistory(), whose fast path and reducer call both ignore unreadableTargets. A backend-level probe with unreadableTargets = {'rt-result::tool_result'} therefore sent the raw sentinel value to the provider on this head; the base ordering and a minimal mutation that forwards the set both withheld it. Please make this helper carry loaded.unreadableTargets (and only fast-path when both collections are empty), then cover the pre-turn call site rather than only the reducer.

…onor unreadable targets

Review on the raw-prefix match (apache#4845) found two restoration paths:

- Both creation paths fed raw RuntimeEvents to the summarizer, so a summary
  could quote a Tool Result body a durable projection transition had already
  removed; reviving that checkpoint on the raw match would restore the body
  on every later replay. Coverage identity stays pinned on the raw prefix
  (the immutable view every creation and match site shares), while the
  summary input is now the effective, transition-folded prefix — for the
  standalone fold and the mid-turn fold alike. Checkpoints minted under the
  v2 source policy were never audited for this, so the policy version moves
  to v3 and they are superseded on load: the session re-summarizes instead
  of replaying an unverified summary.

- The post-match fold of the projected [block, tail] dropped the
  unreadable-target set, so a target whose transition record this build
  cannot decode replayed its raw body instead of the withholding sentinel.
  foldEffectiveModelHistory now forwards the set and only fast-paths when
  both collections are empty.

Regressions: an echoing summarizer proves the checkpoint block and the next
prompt carry the archive placeholder, not the transitioned body; a pre-turn
replay with an unreadable transition record shows the withholding sentinel.
Both fail without these changes and pass with them.

Refs apache#4842.
@me2seeks

me2seeks commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

Thanks — both P1s addressed in dfd53e950b:

Finding 1 (summary as restoration authority): creation still pins coverage identity on the raw prefix (the immutable view both match sites share), but the summarizer now reads the effective, transition-folded prefix on both creation paths (standalone compactHistory and the mid-turn fold). An echoing-summarizer regression proves the checkpoint block and the next provider prompt carry the archive placeholder, never the transitioned body. Because v2-policy checkpoints were never audited for this, HISTORY_COMPACT_SOURCE_POLICY_VERSION moves to v3: existing v2 checkpoints are superseded on load (not corrupt — the session re-summarizes instead of replaying an unverified summary).

Finding 2 (unreadable targets): foldEffectiveModelHistory now forwards loaded.unreadableTargets into the reducer and only fast-paths when both collections are empty, so the post-match fold of [block, tail] withholds the body behind the projection-failure sentinel exactly like the pre-match fold. Covered by a pre-turn replay regression with unreadableTargets = {'<event>::tool_result'}.

Both new regressions fail without these changes and pass with them; full @maka/runtime suite 3219/3219, runtime-host 1698, storage 1128, core 824 — all green; typecheck + biome clean.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/M Under 500 readable lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

History compaction fails open with source_hash_mismatch whenever a stale tool-result transition covers the prefix

2 participants