You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
V2: public context and model-aware runner history diverge across native checkpoints #425
Source-verified V2 integration limitation in drexb-ops/opencode-acp@1fe36e0. Official host sources checked at v2.0.3 (d44b52c) and v2 HEAD c8cc984.
Mismatch
ACP assumes ctx.session.context({sessionID}) is the source history for the current context hook:
lib/v2/host.ts:43-55,165-166 uses that API for both direct-tool history and the context projection.
lib/v2/context.ts:131-148 correlates that result against event.messages.
However official OpenCode uses different history boundaries:
packages/core/src/session/store.ts:179: public context reads SessionHistory.load(..., "latest").
packages/core/src/session/context.ts:159-173: runner history uses the selected model's SessionProviderContext.provenance(model) ?? "local".
packages/core/src/session/history.ts:16-26,78-106: an incompatible provider checkpoint is excluded from model history and the original transcript is re-expanded, while model-neutral public context still starts at the latest checkpoint.
Consequence
After a provider/model change across an incompatible native checkpoint, projected sources and outgoing messages do not describe the same window. ACP's claimCheckpointRanges may classify the re-expanded original region as one opaque checkpoint. Direct-tool history cannot address the original messages present in the actual request. With no outgoing values, normalization of a provider checkpoint produces no decoded content for direct tools.
This is a source-verified risk/limitation, not the current live session's immediate failure (which is a local textual compaction, without providerContext). It needs an installed-host regression before declaring model-switch/native-checkpoint support complete.
Fix / acceptance
Use a host-supported model-aware source snapshot matching the outgoing request, or implement a safe explicit fallback for unavailable provenance. Do not infer removability from array positions. Test compatible native checkpoint, incompatible model switch with re-expansion, and direct compression/decompression against both views. Clearly disclose unsupported windows if the public API cannot represent them.
Added an isolated adapter reproduction using official v2.0.3 toLLMMessages: public view [nativeCheckpoint, nextUser], outgoing view [originalUser, originalAssistant, nextUser] (the incompatible-provider re-expansion described by host history code). ACP reports a valid projection, but the checkpoint claims outgoing indexes [0,1] as opaque; neither original message ID is addressable. Normalizing that same public view without outgoing messages (the direct-tool path) gives the checkpoint zero parts. This confirms the adapter consequence. A full installed-host cross-provider regression remains required.
[bot] 🏷 Confirmed your reproduction against the fork source at b73c4cd (tip of 2026-09-17_opaque-compaction-restore). Root cause is pinpointed and the fix is scoped. Working on it now.
Root Cause
claimCheckpointRanges (lib/v2/projection/normalize.ts:480-506) assigns outgoing indices to a provider-checkpoint by positional inference: it takes every unclaimed outgoing message index between the previous source's last outgoing index and the next source's first outgoing index, and claims them all as the checkpoint's content.
nativeCheckpoint is a provider-checkpoint (compaction + status:"completed" + providerContext), so it's skipped in the main ID-correlation loop (normalize.ts:658).
claimCheckpointRanges runs: previous=undefined→start=0, next=2→end=2. Claims outgoing [0, 1] as the checkpoint's.
Those two indices are the re-expanded originals, not checkpoint content. They're now marked opaque under the checkpoint's ownership. Neither original ID is addressable.
Direct-tool path (outgoing=[]): start=0, end=0 → zero indices → buildProviderCheckpoint iterates nothing → zero parts.
This is the "infer removability from array positions" anti-pattern the issue calls out. It happens to work in the compatible case (one unclaimed message in the range = the checkpoint itself) but silently misattributes in the incompatible case.
Fix Plan
Working on branch off fork/2026-09-17_opaque-compaction-restore:
Remove positional inference for provider-checkpoints. A checkpoint only claims outgoing indices it can positively identify. Since checkpoints don't carry a stable outgoing ID (they're skipped in the ID-correlation loop), they get no outgoing indices from positional inference. Their normalized content is rendered from the source compaction data instead of from outgoing messages.
Uncorrelated outgoing stays opaque. The re-expanded originals (or any other host-added content) remain uncorrelated → opaque → provider-owned. No mutation risk.
Direct-tool path renders checkpoint from source.buildProviderCheckpoint falls back to the compaction's own text when it has no outgoing indices, eliminating the zero-parts gap.
Done — the fix is implemented, dual-reviewed, and submitted as drexb-ops#4 (V2 dev line, base master). Two commits:
1. fix: stop inferring V2 provider-checkpoint ownership from array position (71458d18)
claimCheckpointRanges now collects the unclaimed candidates inside the positional window and claims a provider checkpoint only when exactly one candidate exists. >1 = re-expanded originals after an incompatible model switch → left uncorrelated, so every original stays individually addressable instead of being swallowed into one opaque blob. 0 = checkpoint absent from the outgoing view → also uncorrelated.
Uncorrelated checkpoints render from source data (lowerCompactionText) under an opaque origin key, fixing the zero-parts direct-tool gap. The rendered message carries a known normalized ID, so the patcher can never inject it into outgoing requests.
Unsupported windows are disclosed structurally: providerCheckpoint: true + empty outgoingMessageIndices, documented on V2ProvenanceEntry.providerCheckpoint.
2. fix: keep V2 patches alive when an uncorrelated provider checkpoint is compressed away (37654026)
Found by the code reviewer during dual review — a real blocker my first draft introduced downstream: once the engine compresses the normalized checkpoint away, restoreMissingV2OpaqueSources rejected the entire patch ("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. Fix: skip (not reject) provider checkpoints with empty indices there; non-checkpoint opaque sources keep the fail-closed rejection. Regression test verified to fail against the unfixed code.
Acceptance criteria coverage: compatible native checkpoint (regression lock, byte-identical behavior), incompatible model switch with re-expansion (originals addressable, host entries object-identical through the patch round-trip), direct view (checkpoint rendered from source data, never injected into requests), unsupported windows disclosed. No positional inference remains.
Verification: 4 new tests, each bug-detection test empirically fails with its fix reverted (§5.7.3 methodology); v2 files 33/33; full suite 1414/1415 (single failure is tests/soft-block.test.ts hardcoding /tmp/opencode-dcp-dangerous-<pid> — environmental here, read-only /tmp, passes in CI, same as noted in PR #3); typecheck + build green; devlog complete. No version bump, no publish, no merge (merge stays human-side per repo rules).
Two findings reported here per project rules (no new issues filed by me):
Pre-existing, unrelated to this diff: the direct-tool view (normalizeV2ProjectedHistory(projected, [], …)) reports valid: false ("Patchable origin … has no exact lowered outgoing match") for any plain user/assistant source, even though .messages are fully built and consumed by lib/v2/host.ts:45,52. Verified identical at HEAD without this fix. Functional impact today is nil, but the invalid flag is misleading — worth its own issue if you want it tracked.
Residual coverage gap: unit-level projection/patch/restore coverage now exists for both views, but there is no end-to-end decompress-through-both-views runtime test anywhere in the suite (V1 engine-level decompress tests don't traverse V2 projection/views). Tracked as a follow-up item here rather than scope-creeping this PR.
Known residual limitations (all content-preserving, documented in devlog/…/DESIGN.md §5): a single re-expanded original is positionally indistinguishable from a decoded checkpoint (pre-existing behavior); an earlier incompatible checkpoint widens the next window (fail-safe); multi-message decoded checkpoints are now uncorrelated and rendered from source data.
One topology note for transparency: this session's credentials lack push access to the drexb-ops fork, so the PR head branch lives in ranxianglei/opencode-acp (cross-fork PR — GitHub accepts it because your repo is the fork parent). If you'd prefer the branch moved into the fork, say the word.
[bot] 🏷 Status update (full report posted above): #425 is implemented, dual-reviewed, and submitted as drexb-ops#4 (base master, 2 commits, head 37654026).
What was done
claimCheckpointRanges no longer infers provider-checkpoint ownership from array position — claims only on exactly one unclaimed candidate; re-expanded originals after an incompatible model switch stay individually addressable, and checkpoints absent from the outgoing view are disclosed structurally (providerCheckpoint: true + empty indices) instead of being misclassified.
Direct-tool view renders an uncorrelated checkpoint from source data (lowerCompactionText) instead of zero parts; the rendered message can never be injected into outgoing requests.
Dual-agent review found one real blocker my first draft introduced downstream — restoreMissingV2OpaqueSources would reject the whole patch once such a checkpoint gets compressed away, silently disabling all ACP edits for those sessions. Fixed in commit 2 (lib/v2/projection/restore.ts skips uncorrelated provider checkpoints only; other opaque sources stay fail-closed) with a regression test verified to fail pre-fix.
Verification: typecheck ✓, build ✓, v2 test files 33/33, full suite 1414/1415 (sole failure = environmental /tmp read-only in soft-block.test.ts, passes in CI); each bug-detection test empirically fails with its fix reverted; devlog complete. No version bump, no publish.
Awaiting: human merge of the PR (merge is human-only per repo rules). Two follow-up findings for your triage (disclosed in the detailed report above): a pre-existing direct-view valid:false flag for plain user/assistant sources, and a missing end-to-end decompress-through-both-views test.
Activity
drexb-ops commentedon Sep 17, 2026
Added an isolated adapter reproduction using official v2.0.3
toLLMMessages: public view[nativeCheckpoint, nextUser], outgoing view[originalUser, originalAssistant, nextUser](the incompatible-provider re-expansion described by host history code). ACP reports a valid projection, but the checkpoint claims outgoing indexes[0,1]as opaque; neither original message ID is addressable. Normalizing that same public view without outgoing messages (the direct-tool path) gives the checkpoint zero parts. This confirms the adapter consequence. A full installed-host cross-provider regression remains required.ranxianglei commentedon Sep 17, 2026
[bot] 🏷 Confirmed your reproduction against the fork source at
b73c4cd(tip of2026-09-17_opaque-compaction-restore). Root cause is pinpointed and the fix is scoped. Working on it now.Root Cause
claimCheckpointRanges(lib/v2/projection/normalize.ts:480-506) assigns outgoing indices to a provider-checkpoint by positional inference: it takes every unclaimed outgoing message index between the previous source's last outgoing index and the next source's first outgoing index, and claims them all as the checkpoint's content.Your repro maps exactly onto this:
ctx.session.context)[nativeCheckpoint, nextUser]event.messages)[originalUser, originalAssistant, nextUser]Walkthrough:
nextUsercorrelates by ID → outgoing index 2.nativeCheckpointis aprovider-checkpoint(compaction +status:"completed"+providerContext), so it's skipped in the main ID-correlation loop (normalize.ts:658).claimCheckpointRangesruns:previous=undefined→start=0,next=2→end=2. Claims outgoing[0, 1]as the checkpoint's.outgoing=[]):start=0, end=0→ zero indices →buildProviderCheckpointiterates nothing → zero parts.This is the "infer removability from array positions" anti-pattern the issue calls out. It happens to work in the compatible case (one unclaimed message in the range = the checkpoint itself) but silently misattributes in the incompatible case.
Fix Plan
Working on branch off
fork/2026-09-17_opaque-compaction-restore:Remove positional inference for provider-checkpoints. A checkpoint only claims outgoing indices it can positively identify. Since checkpoints don't carry a stable outgoing ID (they're skipped in the ID-correlation loop), they get no outgoing indices from positional inference. Their normalized content is rendered from the source compaction data instead of from outgoing messages.
Uncorrelated outgoing stays opaque. The re-expanded originals (or any other host-added content) remain uncorrelated → opaque → provider-owned. No mutation risk.
Direct-tool path renders checkpoint from source.
buildProviderCheckpointfalls back to the compaction's own text when it has no outgoing indices, eliminating the zero-parts gap.Tests (per acceptance criteria):
No version bump, no publish, no merge. Fork PR referencing this issue.
Proceeding with implementation. Will follow up here with the PR link once tests pass and dual review is done.
ranxianglei commentedon Sep 17, 2026
继续
fix: keep V2 patches alive when an uncorrelated provider checkpoint i…
ranxianglei commentedon Sep 17, 2026
Done — the fix is implemented, dual-reviewed, and submitted as drexb-ops#4 (V2 dev line, base
master). Two commits:1.
fix: stop inferring V2 provider-checkpoint ownership from array position(71458d18)claimCheckpointRangesnow collects the unclaimed candidates inside the positional window and claims a provider checkpoint only when exactly one candidate exists. >1 = re-expanded originals after an incompatible model switch → left uncorrelated, so every original stays individually addressable instead of being swallowed into one opaque blob. 0 = checkpoint absent from the outgoing view → also uncorrelated.lowerCompactionText) under an opaque origin key, fixing the zero-parts direct-tool gap. The rendered message carries a known normalized ID, so the patcher can never inject it into outgoing requests.providerCheckpoint: true+ emptyoutgoingMessageIndices, documented onV2ProvenanceEntry.providerCheckpoint.2.
fix: keep V2 patches alive when an uncorrelated provider checkpoint is compressed away(37654026)restoreMissingV2OpaqueSourcesrejected the entire patch ("no exact lowered correlation") andlib/v2/context.tspreserved the raw provider request — silently disabling every ACP edit for the rest of incompatible-switch sessions. Fix: skip (not reject) provider checkpoints with empty indices there; non-checkpoint opaque sources keep the fail-closed rejection. Regression test verified to fail against the unfixed code.Acceptance criteria coverage: compatible native checkpoint (regression lock, byte-identical behavior), incompatible model switch with re-expansion (originals addressable, host entries object-identical through the patch round-trip), direct view (checkpoint rendered from source data, never injected into requests), unsupported windows disclosed. No positional inference remains.
Verification: 4 new tests, each bug-detection test empirically fails with its fix reverted (§5.7.3 methodology); v2 files 33/33; full suite 1414/1415 (single failure is
tests/soft-block.test.tshardcoding/tmp/opencode-dcp-dangerous-<pid>— environmental here, read-only/tmp, passes in CI, same as noted in PR #3); typecheck + build green; devlog complete. No version bump, no publish, no merge (merge stays human-side per repo rules).Two findings reported here per project rules (no new issues filed by me):
normalizeV2ProjectedHistory(projected, [], …)) reportsvalid: false("Patchable origin … has no exact lowered outgoing match") for any plain user/assistant source, even though.messagesare fully built and consumed bylib/v2/host.ts:45,52. Verified identical at HEAD without this fix. Functional impact today is nil, but the invalid flag is misleading — worth its own issue if you want it tracked.Known residual limitations (all content-preserving, documented in
devlog/…/DESIGN.md§5): a single re-expanded original is positionally indistinguishable from a decoded checkpoint (pre-existing behavior); an earlier incompatible checkpoint widens the next window (fail-safe); multi-message decoded checkpoints are now uncorrelated and rendered from source data.One topology note for transparency: this session's credentials lack push access to the
drexb-opsfork, so the PR head branch lives inranxianglei/opencode-acp(cross-fork PR — GitHub accepts it because your repo is the fork parent). If you'd prefer the branch moved into the fork, say the word.中文摘要:修复了 V2 公共上下文与模型感知历史在原生 checkpoint 处的分歧——不再按数组位置推断 checkpoint 归属(仅唯一候选才认领,重展开的原始消息保持可寻址),无关联时从源数据渲染并结构化披露不支持窗口;双 agent 评审发现并修复了一个会导致不兼容切换会话中 ACP 编辑被整体静默禁用的 restore 阻塞问题,PR 已提交待人工合并。
ranxianglei commentedon Sep 17, 2026
[bot] 🏷 Status update (full report posted above): #425 is implemented, dual-reviewed, and submitted as drexb-ops#4 (base
master, 2 commits, head37654026).What was done
claimCheckpointRangesno longer infers provider-checkpoint ownership from array position — claims only on exactly one unclaimed candidate; re-expanded originals after an incompatible model switch stay individually addressable, and checkpoints absent from the outgoing view are disclosed structurally (providerCheckpoint: true+ empty indices) instead of being misclassified.lowerCompactionText) instead of zero parts; the rendered message can never be injected into outgoing requests.restoreMissingV2OpaqueSourceswould reject the whole patch once such a checkpoint gets compressed away, silently disabling all ACP edits for those sessions. Fixed in commit 2 (lib/v2/projection/restore.tsskips uncorrelated provider checkpoints only; other opaque sources stay fail-closed) with a regression test verified to fail pre-fix.Verification: typecheck ✓, build ✓, v2 test files 33/33, full suite 1414/1415 (sole failure = environmental
/tmpread-only insoft-block.test.ts, passes in CI); each bug-detection test empirically fails with its fix reverted; devlog complete. No version bump, no publish.Awaiting: human merge of the PR (merge is human-only per repo rules). Two follow-up findings for your triage (disclosed in the detailed report above): a pre-existing direct-view
valid:falseflag for plain user/assistant sources, and a missing end-to-end decompress-through-both-views test.中文摘要:#425 已完成并提交 PR(双评审通过、修复了评审发现的 restore 阻塞问题),等待人工合并。
ranxianglei commentedon Sep 19, 2026
opencodeV2 use https://github.com/ranxianglei/billion-context