Repository navigation
V2: provider-checkpoint range claim swallows a correlated tool message, losing the tool result and disabling ACP for the session #456
Description
Activity
🤖 Powered by ework · qwen3.8-27b
[bot] 🏷 Triage complete — bug confirmed by repro, root cause matches your analysis exactly.
Verification (evidence): I replayed your synthetic B2 shape through
normalizeV2ProjectedHistoryon the current V2 line (base37654026, tip of2026-09-17_v2-checkpoint-provenance-guard, which carries the #425 fix):without providerContext: valid=true aidx=[1,2] result={2,0} origLen=2 with providerContext: valid=false aidx=[1] result=null origLen=1 ckptIdx=[2] invalid-source "Tool origin source:1:part:0 has no exact lowered input/output match"Byte-for-byte identical to your report, including the rejection message and the checkpoint claiming index 2.
Layer judgment: Your analysis is at the right layer — this is a distinct residual case of the #425 family, not a symptom of another bug. The #425 fix made
claimCheckpointRangesclaim only when exactly one unclaimed candidate exists in the window; it did not ask what that candidate is. When the single candidate is therole:"tool"message answering a correlated assistant call, the claim strips the result from its call, the post-checkpoint result-correlation loop (which deliberately skips claimed indices so decoded provider content isn't misattributed) can no longer find it, andloweredToolCorrelationIsExactfails closed for the whole session. The proposed fix (source-reserved index set passed into the claimer + wholly-reserved checkpoint emits no normalized message) addresses the root cause directly.Duplicate screen: Searched open+closed issues for
claimCheckpointRanges/providerContext/ checkpoint-tool-message overlap → no duplicates. Related-but-distinct from #425 (closed): #425 = public-context vs runner-history divergence across native checkpoints; this = the claimer stealing a correlated tool result inside an otherwise-compatible view.Plan: branch
2026-09-29_v2-checkpoint-reserved-tooloff37654026; implement the two-step fix inlib/v2/projection/normalize.ts(+ one Draft flag), add the T2.x tests you specified (result{messageIndex: 2, contentIndex: 0},originalContent.length === 2underproviderContext, wholly-reserved checkpoint emitting no normalized message) plus guards for the partial-window remainder and the existing direct-view/incompatible-switch render-from-source paths. Devlog entry included. Dual-agent review, then PR closing this issue.中文摘要:已复现并确认根因(provider-checkpoint 区间认领把已关联的 tool-result 消息吞掉导致整会话 fail-closed),与你的分析一致,开始按方案实现。
🤖 Powered by ework · qwen3.8-27b
[bot] 🏷 Fixed — PR: #468 (base
2026-09-17_v2-base, branch2026-09-29_v2-checkpoint-reserved-tool).Verified your analysis first: reproduced the exact shape pre-fix (
valid=false,invalid-source "Tool origin source:1:part:0 has no exact lowered input/output match", checkpoint claiming index 2) and confirmed the root cause as described — at claim time no window index is already claimed (the window sits strictly between correlated neighbours), so #425's exactly-one-candidate rule claims whatever single index is in the window, including a correlated tool result.Fix, two layers:
- Claim layer (as proposed):
reservedForCorrelatedToolsset (source-bound assistants' own indices ∪ role=tool results of their call IDs) computed after ID correlation;claimCheckpointRangesskips reserved candidates; a wholly-reserved window setsDraft.whollyReserved→buildProviderCheckpointemits no normalized message (sidecar disclosure only, no contentless assistant turn). Partial windows keep the tool-free remainder per your guard. - Restore layer (found in dual-agent code review, would have re-created the same symptom one layer downstream): the wholly-reserved shape is the first provider-checkpoint entry with no
normalizedMessageId;restoreMissingV2OpaqueSources(restore.ts) rejects any protected entry lacking one, andlib/v2/context.tsturns that rejection into "preserve provider request" = ACP off for the session again. Such entries are now exempt — nothing exists to be missing or restored. Regression-tested at context level.
Behavior changes (old → new):
- Window = only correlated tool indices: claimed → session-wide rejection / tool result lost / ACP off ⇒ claims nothing, sidecar-only entry, session valid, result intact.
- Mixed reserved+free window (>1 candidates): no claim ⇒ claims the free remainder.
- Restore layer: wholly-reserved entry rejected ⇒ accepted (nothing restored).
- Unchanged: compatible-view single-candidate claim, incompatible-switch non-claim, direct-view render-from-source, plain compactions — all pre-existing tests green.
Verification: 3 new tests (2 unit in
tests/v2-message-projection.test.tsincl. the partial-window guard, 1 context-level through restore+patch intests/v2-context-patch.test.ts); each verified to fail with its lib change reverted. Full suite 1417/1418 — sole failure is env-onlytests/soft-block.test.ts(hard-coded/tmpmkdir, read-only in this sandbox, fails identically on pristine base). Typecheck clean, touched files prettier-clean.Note: the branch carries the two unmerged #425-guard commits (
71458d18,37654026) as prerequisites since this fix refines the claim rule they introduced — labeled in the PR body. Awaiting CI + human merge.- Claim layer (as proposed):
🤖 Powered by ework · qwen3.8-27b
[bot] 🏷 Review status update for PR #468:
- Code review (dual-agent): one BLOCKER found and fixed before this reply — the restore-layer rejection (
restoreMissingV2OpaqueSourceswould have re-disabled ACP on the new sidecar-only checkpoint shape); two minors addressed as contract-comment disclosures intypes.ts. - Test review (independent agent): APPROVE. All 3 new tests mutation-verified — reverting
normalize.tsfails the claim-layer tests at the exact assertions pinning the bug (valid=falsepre-fix; partial-window test fails[3] vs []), reverting onlyrestore.tsfails the context-level test in isolation while all other tests stay green. No tautological assertions; fixtures verified against source math to genuinely hit the wholly-reserved / partial-window paths. - CI note:
pr-checks.ymlonly triggers on PRs targetingmaster, so no GitHub Actions run on this2026-09-17_v2-base-targeted PR (same as the existing fix(v2): map repeated identical system text by ordered occurrence #429/fix(v2): stop compaction usage from becoming phantom system overhead (#421) #430 pattern). Local gates all pass on the final tree: typecheck clean, build success, full suite 1417/1418 (sole failure is the env-only/tmphard-code intests/soft-block.test.ts, identical on pristine base). - PR is
mergeable: true. Ready for human merge whenever you are.
- Code review (dual-agent): one BLOCKER found and fixed before this reply — the restore-layer rejection (
Same routing note as in #455: per #442, opencode-acp stays on OpenCode 1.x and the in-repo OpenCode 2.x native port (
lib/v2/*onv2-base) is retired — OpenCode 2.x goes through billion-context (bili opencode/bili plugin install opencode; delivered as billion-context#754). So the provider-checkpoint range claim swallowing a correlated tool message finding won't be picked up in this repo; if it still applies, please continue it at https://github.com/ranxianglei/billion-context/issues. Thanks for the detailed analysis.Для OpenCode 2.x используйте billion-context; этот репозиторий остаётся на OpenCode 1.x (#442).
Reacted by ranxianglei
Scope
V2 port of opencode-acp on OpenCode 2.0.18.
providerContextis emitted only by native compaction, so the live repro is a captured/synthetic replay of the observed lowered shape rather than an interactive session.Related but distinct: #425 covers public-context vs runner-history boundaries across native checkpoints. This issue is about the range claim stealing a correlated tool message and the resulting loss of the tool result.
Root cause
claimCheckpointRangesinlib/v2/projection/normalize.ts:488-510expands each provider checkpoint across the whole index range between its neighbouring checkpoints and claims every index not yet claimed — including indices that carry a tool call a source-bound assistant message already correlates, or the tool result answering that call.The resulting origin has no exact lowered input/output match, so exact-correlation validation rejects it:
invalid-source— "Tool origin source:1:part:0 has no exact lowered input/output match".Consequence
The tool result disappears from the model output entirely, and because the rejection is fail-closed, ACP turns itself off for the rest of the session. Without
providerContextthe identical scenario passes:Repro
Synthetic B2 shape replay — user / assistant+tool-call /
role:"tool"+tool-result / compaction — throughnormalizeV2ProjectedHistory. Adding theproviderContextfield is the only difference.Proposed fix
In
lib/v2/projection/normalize.ts:claimCheckpointRangesand skip them. Reserved indices carry a tool call that a source-bound assistant already correlates, or the tool result answering it; letting the checkpoint swallow them strips the result of its call and makes exact-correlation reject the whole session.draft.normalized = undefinedinstead.Guard: the checkpoint's own
outgoingMessageIndicesmust keep the tool-free remainder, and a checkpoint that is not wholly reserved must behave exactly as before.Tests
tests/v2-message-projection.test.ts: T2.x — unit fornormalizeV2ProjectedHistoryassertingresult = {messageIndex: 2, contentIndex: 0}andoriginalContent.length === 2underproviderContext, plus the wholly-reserved checkpoint emitting no normalized message.