Repository navigation
fix: V2 provider checkpoints must not claim correlated tool messages (#456) - #468
ranxianglei wants to merge 6 commits into
Conversation
claimCheckpointRanges now claims a provider checkpoint only when exactly one unclaimed candidate exists in its window. More than one means re-expanded originals after an incompatible model switch; zero means the checkpoint is absent from the outgoing view. Both stay uncorrelated so host-owned messages remain individual opaque entries instead of being swallowed into the checkpoint entry. buildProviderCheckpoint renders lowerCompactionText(source) when no outgoing indices exist, so the direct-tool view (outgoing = []) keeps the checkpoint's summary and recent context instead of normalizing to zero parts. Unsupported windows are disclosed via providerCheckpoint + empty outgoingMessageIndices (documented on V2ProvenanceEntry). Tests: compatible native checkpoint regression lock, incompatible model switch with re-expansion (object-identity patch round-trip), direct-view rendering. Bug-catching tests verified to fail at HEAD without the fix. Refs: #425
…s compressed away
Dual-agent review of the provenance guard found a cross-module blocker: once the shared engine compresses the normalized checkpoint away, restoreMissingV2OpaqueSources rejected the whole patch for the uncorrelated entry ('no exact lowered correlation') and lib/v2/context.ts preserved the raw provider request - silently disabling every ACP edit for the rest of incompatible-switch sessions. A provider checkpoint with empty outgoingMessageIndices is now skipped instead of rejected: the outgoing request already carries its information as re-expanded host-owned originals. Non-checkpoint opaque sources keep the fail-closed rejection.
Also applies review nits: protected-flag pin, disclosed invalid direct-view pin, types.ts comment precision, DESIGN.md residual-limitation section, restore regression test. Verified the regression test fails against the unfixed restore.ts.
Refs: #425
claimCheckpointRanges expanded each provider checkpoint across its whole index window and claimed every index not yet claimed - including a role=tool result answering an unexecuted tool call of a source-bound assistant. The stolen result broke exact-correlation validation (invalid-source) and ACP disabled itself fail-closed for the rest of the session. The claimer now skips source-reserved indices (the assistant message carrying a correlated tool call, plus the role=tool results answering it). A checkpoint whose entire window is reserved decodes nothing: it sets Draft.whollyReserved and emits no normalized message, staying disclosed structurally via its sidecar entry. Checkpoints with a tool-free remainder keep claiming it per the existing exactly-one-candidate rule. Fixes #456
…estoration (#456 review finding)
[bot] 🏷 Picked up. This is a
Full review conclusion to follow in a second comment. |
…468 review finding) Two long lines in tests/v2-context-patch.test.ts (introduced by 3765402's 'keeps patches alive when an uncorrelated provider checkpoint is compressed away' test) violated the prettier style, contradicting the PR body's 'touched files prettier-clean' claim. Whitespace/wrapping only, no behavioral change. All five touched files now pass prettier --check. Worklog updated.
[bot] 🏷 Independent verification complete — APPROVE, ready for human merge. This session pulled ① Original issue (#456) — direction correct. Root cause holds up under re-trace: base ② Regression review — existing behaviors inventoried, none broken:
③ Code review — no blockers. Ordering is sound: ID correlation → reserved-set computation → ④ Test validity — real tests, not tautologies. The 3 #456 tests import actual sources and assert with teeth: the unit test reproduces the issue's exact numbers pre/post ( ⑤ Diff cleanliness — one finding, fixed in place. 11 files, all scoped (3 lib + 2 test + 2 devlog dirs). The PR body's "touched files prettier-clean" claim was inaccurate: CI status: no GitHub Actions run exists for this PR — PR now at head 中文摘要:独立复核了 PR #468(修 #456 V2 provider checkpoint 抢占关联工具消息导致 ACP 整会话失效):两层修复逻辑正确、既有行为无回归、测试真实有效,唯一问题是前置提交引入的两行格式违规(已在分支上以纯空白提交 |
Closes #456
Root cause
claimCheckpointRanges(lib/v2/projection/normalize.ts) expanded each provider checkpoint across its index window and claimed the single unclaimed candidate — even when that candidate was therole:"tool"message answering an unexecuted tool call of a source-bound assistant (#425's exactly-one-candidate rule was purely positional, blind to ownership). The stolen result made exact-correlation validation fail fail-closed (invalid-source, "Tool origin … has no exact lowered input/output match"): the tool result vanished from the outgoing request and ACP disabled itself for the rest of the session.Fix (two layers)
b67f433f): areservedForCorrelatedToolsindex set (source-bound assistants' own indices ∪ the role=tool results of their call IDs) is computed after ID correlation and passed intoclaimCheckpointRanges, which skips reserved candidates. A checkpoint whose entire window was reserved sets newDraft.whollyReserved;buildProviderCheckpointthen emits no normalized message for it (sidecar disclosure only — no contentless assistant turn enters the algorithm projection).5444d46e, found in dual-agent code review): the wholly-reserved shape is the first provider-checkpoint entry without anormalizedMessageId.restoreMissingV2OpaqueSources(lib/v2/projection/restore.ts) rejects any protected entry lacking one, whichlib/v2/context.tsturns into "preserve provider request" = ACP off again, one layer downstream. Such entries are now exempt: no normalized message exists by construction, so nothing can be missing and nothing to restore.Prerequisite commits carried in
This branch builds on the unmerged
2026-09-17_v2-checkpoint-provenance-guardline (#425 follow-up):71458d18fix: stop inferring V2 provider-checkpoint ownership from array position37654026fix: keep V2 patches alive when an uncorrelated provider checkpoint is compressed awayThey are required for this fix to make sense (the positional-claim rule they introduced is what #456 refines) and touch no files beyond their own scope. No other content from that branch is included.
335a5f82merges currentorigin/2026-09-17_v2-base(clean, no overlap).Behavior changes (old → new)
invalid-sourcerejection, tool result lost, ACP offproviderCheckpoint: true, empty indices, no normalized message); session valid, tool result intactVerification
valid=false invalid-source "Tool origin source:1:part:0 has no exact lowered input/output match"; post-fixvalid=true,result={messageIndex:2,contentIndex:0},originalContent.length===2, checkpoint entry has no normalized message.tests/v2-message-projection.test.ts×2 (unit, incl. partial-window guard),tests/v2-context-patch.test.ts×1 (context-level throughrestoreMissingV2OpaqueSources+applyV2ContextPatch). Each verified to FAIL with its corresponding lib change reverted.tests/soft-block.test.tsenv-only — hard-codes/tmpmkdir, read-only in this sandbox, fails identically on pristine base; CI/tmpis writable). Typecheck clean; touched files prettier-clean.types.ts).Devlog:
devlog/2026-09-29_v2-checkpoint-reserved-tool/(REQ / WORKLOG / DESIGN).