fix(runtime): match history-compact checkpoints against the raw ledger prefix - #4845
fix(runtime): match history-compact checkpoints against the raw ledger prefix#4845me2seeks wants to merge 2 commits into
Conversation
…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.
M4n5ter
left a comment
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
[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( |
There was a problem hiding this comment.
[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.
|
Thanks — both P1s addressed in 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 Finding 2 (unreadable targets): Both new regressions fail without these changes and pass with them; full |
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 recordeddecision: 'failedOpen', failOpenReason: 'source_hash_mismatch'withdroppedEvents: 0.Fix
In
buildPriorMessages(pre-turn replay):compactHistory, mid-turn state) hashes;[block, tail]throughreduceEffectiveModelProjectionsbefore 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/runtimefull dist suite: 3217 tests, 0 failures (3 runs)Notes
source_hash_mismatchat replay.context_compaction_failed_open) is only written at send settlement; a stop mid-turn hides it entirely. Left out of scope here, flagged in the issue.