Repository navigation
fix: verify source-message availability before committing decompression (#446) - #449
Conversation
…on (#446) Block/range decompression could report 'Restored N message(s)' and terminally deactivate the summary block even when the indexed originals were absent from host history (removed by native compaction or external deletion): the commit path treated a membership transition as proof of restored content. Add a fail-closed pre-commit gate in createDecompressTool.execute (after the toFile branch, before any state mutation) backed by two pure functions in decompress-logic.ts: - collectRequiredDecompressMessageIds: closed-form set of message IDs whose visibility this decompression would restore — exclusively covered by the target blocks, minus one-tier shielded IDs (transitive consumed closure that sync reactivates; full mode requires everything). - checkDecompressSourceAvailability: splits that set against the fetched history IDs into available/missing. Missing sources => warn log + explicit error naming targets, counts, and missing refs; nothing is mutated, summary coverage preserved. Transient host fetch failures still reject out of prepareDecompressSession before any mutation, so the two failure classes stay structurally distinct. Because every committed ID is verified present, restored counts/stats now reflect real recovered content. Tests: 13 unit + 9 E2E regressions (all-missing / ref-display / retry-after- abort / partial / complete / nested one-tier / nested full abort+success / transient-fetch rejection). Bug-detection proof: with the gate removed the all-missing E2E fails with the issue's exact phantom output 'Decompressed block b5. Restored 1 message(s) (~100 tokens)'. Closes #446
📦 Built Plugin ArtifactBranch: Option A — Install from npm PR tag (recommended)opencode plugin opencode-acp@pr-449 --globalEach push to this PR publishes a new version under the Option B — Install from GitHubopencode plugin "github:ranxianglei/opencode-acp#2026-09-20_decompress-source-availability" --globalOption C — Download artifact
tar xzf opencode-acp-pr449.tgz
cp -r package/dist ~/.cache/opencode/packages/opencode-acp@latest/node_modules/opencode-acp/dist
This comment is automatically updated on each push. |
[bot] 🏷 核对完成:修复了 decompress 在源消息已被宿主历史删除时仍"幻影恢复"并不可逆丢弃摘要的 #446(commit 路径把成员关系转移误当内容恢复),fail-closed 前置门设计正确、测试真实可捕获该 bug,无冲突,可以合并。 Verification report (PR #449 → closes #446)Per the [PR] handling rule I did not redo the work — pulled branch 1. CI — all green on 2. Diff cleanliness: 7 files, +1007/−0, purely additive — 3. Code review (independent trace):
4. Local run (fresh worktree of the PR head):
5. Bug-detection proof (§5.7.3) — done myself, not taken on faith: temporarily disabled the gate ( 6. Test quality (§5.6 spot-check): both test files import from real source ( One note (not blocking): master has moved since the PR base (v1.18.2 release, #437/#441/#443 merged). GitHub reports Merging is your call — per project rules I don't merge PRs: #449 |
Master's new E2E suites (PR #448/#449) build tool factories with a minimal {reload(){}} prompts mock; since promptPack landed, factories also call getRuntimePrompts() at creation time. Give both files the same makeDefaultPromptsMock() helper used by the four pre-existing factory test files so they run against the shipped default-pack descriptions.
Problem (#446)
Block/range decompression could report
Restored N message(s)and terminally deactivate the summary block even when the indexed original messages were absent from the history returned by the host (e.g. removed by native compaction or external deletion). The commit path treated a membership transition (byMessageIdentry'sactiveBlockIdsgoing empty) as proof of content restoration —computeRestoredMessagesnever even receives the fetched messages — so stats were decremented for phantom tokens and the only remaining coverage (the summary) was discarded irreversibly.Repro from the issue confirmed against source: active block covering
msg-gone, history lacking it →{"restoredMessageCount": 1, "restoredTokens": 100, "actualOriginalMessagesFound": 0, "restoredContentPreview": "", "blockDeactivated": true}.Fix
Fail-closed pre-commit gate in
createDecompressTool.execute(lib/compress/decompress.ts), placed after the toFile branch and before any state mutation, backed by two pure functions inlib/compress/decompress-logic.ts:collectRequiredDecompressMessageIds(messagesState, targets, {full?})— closed-form set of message IDs whose visibility this decompression would restore: entries exclusively covered by the target blocks (activeBlockIds.length > 0 ∧ ⊆ targets), minus the one-tier shield (union ofeffectiveMessageIdsover the transitive consumed closure thatsyncCompressionBlockswould reactivate — non-user-deactivated consumed tiers). Full mode has no shield.checkDecompressSourceAvailability(...)— splits that set against fetched-history IDs into available/missing.Behavior:
warnlog + explicit error naming target blocks, counts, and missing refs (model-facingmNNNNNrefs viamessageIds.byRawId, raw-ID fallback): nothing is mutated, stats untouched, block stays compressed and its summary remains available.prepareDecompressSessionbefore any mutation, so transient vs missing-source stay structurally distinct (acceptance fix: move dynamic ACP injections to suffix message for prefix cache stability #6); the error text says so explicitly.Tests
tests/decompress-logic.test.ts(R computation: single/multi-target, one-tier shield, full nested, transitive closure, user-deactivated exclusion, empty membership → ∅).tests/decompress-source-availability.test.tsthrough the realcreateDecompressToolwith controllable host history: all-missing abort (issue repro) + zero state change, ref-display in error, retry-after-abort, partial (lists only missing IDs), complete (count/stats/preview), nested one-tier success with consumed-tier raws absent (shielded), nested full abort + success, transient-fetch rejection.Decompressed block b5. Restored 1 message(s) (~100 tokens); restored → green.npm run typecheckclean; full suite 1312/1314 — the 2 failures are pre-existing sandbox issues (read-only/tmp:soft-block.test.tsimport crash,inactive-block-decompress.test.tshardcoded/tmptoFile path), reproduced identically on clean master.Review
Dual-agent independent review (§5.3 code + §5.6 tests): both approve-with-nits. Applied: transient-fetch pinning test, ref-display test, nested stats assertions, stricter deep-flag assert. Documented as known limitation (DESIGN.md): one-tier nested commit does not verify the reactivated tier's summary-carrier compress message (carriers are hard-protected, never in
byMessageId) — rare trigger, deferred beyond #446 scope.No version bump (release-branch-only rule). No persisted-state-format changes; purely additive API surface.
Closes #446