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
fix: reconcile compaction restart and custom-storage fork recovery #407
Two pre-existing state-recovery paths can restore stale or incomplete ACP state:
During session initialization, ACP records the newest compaction timestamp from current history before loading persisted state. It then restores persisted nudge/message-ref/tool-cache state without comparing the current history boundary to the persisted boundary. Because lastCompaction is already current, the next transform does not reset those stale transient fields.
Fork recovery loads the parent state from the default storage location instead of the child session's resolved storagePath. It also consumes legacy four-digit parent message refs without normalizing them to the current five-digit format, so parent-to-fork mapping can fail.
Impact
A restart immediately after native compaction can retain stale refs and nudge baselines. A fork using custom storage or legacy state can lose inherited compression blocks when copied history no longer contains replayable compress inputs.
Proposed fix
Compare the current completed-compaction boundary with the persisted boundary after state load. When history is newer, reset transient refs/nudges/tool cache while preserving compression blocks and stats, then persist the corrected state.
Load parent fork state from the resolved active storage directory. Normalize legacy parent byRef/byRawId aliases before translating IDs.
Add restart-after-compaction, custom-storage fork, and legacy-ref fork regression tests.
Related OpenCode V2 migration: #395. Concurrency work: #404.
Both problems verified real, fixed, and covered by regression tests → PR #409
Verification (before implementing):
Compaction-restart gap — confirmed. In ensureSessionInitialized (lib/state/state.ts), state.lastCompaction = findLastCompactionTimestamp(messages) runs before the persisted load, then nudge anchors/baselines and message refs are restored unconditionally. After a restart between native compaction and the next transform, updatePerTurnState's reset trigger (lastCompactionTimestamp > state.lastCompaction) can never fire because both sides already equal the new boundary — stale transients survive indefinitely.
Fork recovery gaps — confirmed. (a) The parent load at state.ts:348 omitted the child's resolved storageDir, so with a custom storagePath the parent file was never found; (b) mapForkIds (lib/state/rebuild.ts) consumed parent byRef keys verbatim — legacy pre-1.1.0 four-digit refs (m0001) can never match fork five-digit refs (m00001), so translation yields an empty map and inherited blocks are lost whenever the copied history lacks replayable compress inputs.
Fixes:
Post-load reconciliation: compare the current completed-compaction boundary against _persistedLastCompaction; when history is newer, reset transient refs/nudges/tool cache via the existing resetOnCompaction (preserves compression blocks and stats by design — parity with the live-compaction path) and persist the corrected state.
Parent fork state now loads from the resolved active storage directory, with a default-location fallback for the "just configured storagePath" transition (found in code review — without it that scenario degraded from transfer to replay).
New normalizeParentMessageIds() normalizes legacy four-digit parent byRef/byRawId before translation (mirrors the own-session load-time migration).
Tests (tests/restart-compaction-fork-recovery.test.ts, 6 tests): restart-after-compaction reset + negative control (no over-resetting), custom-storage fork e2e, storagePath-transition fallback, legacy-ref fork (unit + full-init e2e). Per §5.7.3 discipline, every bug-repro test was verified to fail against unfixed code (stash fixes → fail; restore → pass); the transition test likewise fails without the fallback.
Quality gates: dual-agent review completed (§5.3 code + §5.6 tests, both APPROVE; findings fixed on-branch — see PR comment); full suite 1269/1269; typecheck/build/Prettier clean; CI on the PR: pr-validation, build, test (Node 22+24) all passing, e2e job running. Devlog: devlog/2026-09-16_compaction-fork-recovery/.
📎 PR: #409 (merge is a human-only operation per project rules — please merge when satisfied.)
Status update — PR #409 is now fully green and up-to-date with master.
While finalizing I noticed master had moved (v1.18.1 release + #403 Windows path fix merged upstream), so I rebased the branch onto the current master tip (06efd39). Along the way an external commit (style: prettier-format new devlog files, by ranxianglei) had been pushed directly to the PR branch — it was preserved via cherry-pick, nothing was clobbered.
Activity
ranxianglei commentedon Sep 16, 2026
[bot] 🏷 Acknowledged — triaging #407 now.
Plan (will verify each claim against the code before implementing):
lib/state/(compaction-restart reconciliation + fork storage/ref normalization).Will report findings as soon as verification is done.
ranxianglei commentedon Sep 16, 2026
Both problems verified real, fixed, and covered by regression tests → PR #409
Verification (before implementing):
ensureSessionInitialized(lib/state/state.ts),state.lastCompaction = findLastCompactionTimestamp(messages)runs before the persisted load, then nudge anchors/baselines and message refs are restored unconditionally. After a restart between native compaction and the next transform,updatePerTurnState's reset trigger (lastCompactionTimestamp > state.lastCompaction) can never fire because both sides already equal the new boundary — stale transients survive indefinitely.state.ts:348omitted the child's resolvedstorageDir, so with a customstoragePaththe parent file was never found; (b)mapForkIds(lib/state/rebuild.ts) consumed parentbyRefkeys verbatim — legacy pre-1.1.0 four-digit refs (m0001) can never match fork five-digit refs (m00001), so translation yields an empty map and inherited blocks are lost whenever the copied history lacks replayable compress inputs.Fixes:
_persistedLastCompaction; when history is newer, reset transient refs/nudges/tool cache via the existingresetOnCompaction(preserves compression blocks and stats by design — parity with the live-compaction path) and persist the corrected state.storagePath" transition (found in code review — without it that scenario degraded from transfer to replay).normalizeParentMessageIds()normalizes legacy four-digit parentbyRef/byRawIdbefore translation (mirrors the own-session load-time migration).Tests (
tests/restart-compaction-fork-recovery.test.ts, 6 tests): restart-after-compaction reset + negative control (no over-resetting), custom-storage fork e2e, storagePath-transition fallback, legacy-ref fork (unit + full-init e2e). Per §5.7.3 discipline, every bug-repro test was verified to fail against unfixed code (stash fixes → fail; restore → pass); the transition test likewise fails without the fallback.Quality gates: dual-agent review completed (§5.3 code + §5.6 tests, both APPROVE; findings fixed on-branch — see PR comment); full suite 1269/1269; typecheck/build/Prettier clean; CI on the PR: pr-validation, build, test (Node 22+24) all passing, e2e job running. Devlog:
devlog/2026-09-16_compaction-fork-recovery/.📎 PR: #409 (merge is a human-only operation per project rules — please merge when satisfied.)
中文摘要:修复了重启落在原生压缩之后导致过期消息引用/nudge 基线被无条件恢复、以及自定义 storagePath 或旧版 4 位引用下 fork 恢复丢失继承压缩块的问题(含审查发现的存储路径过渡场景父状态回退缺失),已加 6 个回归测试并验证修复前必失败,PR #409 可以合并。
fix: reconcile compaction restart and custom-storage fork recovery
fix: reconcile compaction restart and custom-storage fork recovery
ranxianglei commentedon Sep 16, 2026
Status update — PR #409 is now fully green and up-to-date with master.
While finalizing I noticed master had moved (v1.18.1 release + #403 Windows path fix merged upstream), so I rebased the branch onto the current master tip (
06efd39). Along the way an external commit (style: prettier-format new devlog files, by ranxianglei) had been pushed directly to the PR branch — it was preserved via cherry-pick, nothing was clobbered.Final state of #409 :
1576ed4, rebased onto latest master, no conflictsmergeable: true,mergeable_state: cleanReady for human merge whenever you are (merge stays a human-only operation per project rules).
中文摘要:PR #409 已 rebase 到最新 master(含 v1.18.1 与 #403),保留了他人直接推到分支上的 devlog 格式化提交,全部 CI(含 e2e)通过、本地 1280/1280 测试通过,状态 clean,等待人工合并。