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: initialize persisted V2 ACP state before direct tool execution #419
After ACP reloads (for example, a local development build changes the configured source), persisted state exists for a V2 session and /acp context can load it, but the next direct compress tool call fails:
ACP: session <id> has no initialized state
The V2 direct-tool wrapper calls withSessionMutation, which expects the registry already has live state. It does not use the host session/history to initialize when the plugin instance is new.
Impact
Manual compression fails after a plugin reload/restart even though persisted ACP blocks/refs are present. This was observed after issue #418 had already restored opaque native compaction sources and successfully initialized the session once.
Expected
Before every V2 direct ACP tool definition executes, the adapter should atomically initialize missing session state from host.sessions.messages() under the same session reservation, then run the shared definition. It must preserve V1 behavior, fail closed on inaccessible history, honor lifecycle/subagent/permission guards, avoid duplicate initialization/persistence, and retain same-session serialization.
Acceptance
Regression starts with an empty V2 registry plus persisted state and executes a real direct tool.
State loads once, direct tool no longer returns no initialized state, and persistence/tool effects remain transactional.
Reload/restart-style multiple calls remain serialized with no duplicate refs/blocks.
[bot] 🏷 Triage update — root cause confirmed in code, fix in progress.
Verification (bug triage per AGENTS.md):
Repro path confirmed. The V2 direct-tool wrapper (lib/v2/tools.ts → createV2Tool) runs its guards and then calls the shared definition, which executes under withToolSessionMutation (lib/compress/types.ts:168). That guard calls registry.withSessionMutation(sessionID, run) (lib/state/state.ts:427), which throws ACP: session <id> has no initialized state (line 436) whenever the registry has no live state — exactly the reported error after a plugin reload/restart.
Asymmetry is real. The /acp command path already solves this: executeCommand uses registry.withSessionMutationAndInitialize(host.sessions, …, () => host.sessions.messages(sessionID), …) (lib/v2/commands.ts:180), which atomically initializes missing state from host history under the session reservation. The V2 context hook does the same (lib/v2/context.ts:262). Only the direct-tool path lacks it.
Layer judgment. The shared compress pipeline does call ensureSessionInitialized (lib/compress/pipeline.ts:60), but only against an already-resolved state object — unreachable when the registry is empty because the guard throws first. So the fix belongs in the V2 adapter layer, not the shared engine (which must keep V1 behavior).
Fix plan: before every V2 direct ACP tool execution (after all lifecycle/subagent/permission guards, so denied calls never touch state), atomically initialize missing session state from host.sessions.messages() through the same withSessionMutationAndInitialize entry point used by commands — history load + persisted-state restore + persistence flush all under the session reservation. Live state short-circuits; concurrent calls serialize on the same per-session tail; inaccessible history fails closed with a normal tool error. Plus a regression test starting from an empty V2 registry with persisted blocks/refs executing a real direct tool.
Working on branch off the #418 tip (fork/2026-09-17_opaque-compaction-restore). Will follow up here with the PR link.
Further investigation confirms the cold-registry defect with the real V2 compress wrapper and a valid request: persisted state exists with a saved block, a fresh registry is created, and execution returns ACP compress failed: ACP: session audit-opaque has no initialized state before persisted history can load. The shared withToolSessionMutation calls the non-initializing registry guard before prepareSession can reach ensureSessionInitialized.
Correction to the earlier diagnosis: a reload alone was not established as the cause of every live failure. Logs show repeated context projection rejections from duplicate system text (#422), followed by patch rejections caused by ID insertion before opaque multipart/error tools (#423). A rejected fresh context transaction discards initialization. These must be fixed together: hydrating the tool alone does not ensure saved compression reaches later model requests. Also track phantom system overhead (#421) and false savings for restored non-removable sources (#420).
Audit scope: dual-runtime fork drexb-ops/opencode-acp@1fe36e0 against official OpenCode v2.0.3 source. No production fix made in this audit.
What #809 is: a design doc (explicitly "a discussion artifact, not a finalized plan") for shipping billion-context as a no-launcher npm plugin on OpenCode 2.x — plugins: ["billion-context"], the plugin lazily spawns a loopback-bound proxy from the package root.
Its two touches on opencode-acp:
Load-contract precedent — it cites that opencode-acp already publishes exports["./server"] as evidence the carrier route matches your existing practice. Informational; nothing requested here.
Coexistence / self-disable (open Q4 + the "single-owner rule"): if both the bili local plugin and opencode-acp are listed in one config, both claim the same history as compression owners, because opencode-acp's self-disable triggers (BILLION_CONTEXT_PROXY env, /bili/ baseURL marker) don't fire in bili's plugin-local mode. #809 asks where the mutual-exclusion semantics should live.
Does it change #419? No — orthogonal.#419 is a self-contained V2 cold-registry bug: after a reload the registry is empty, so the shared guard throws no initialized state before prepareSession can reach ensureSessionInitialized. The fix hydrates the registry via withSessionMutationAndInitialize inside createV2Tool before the shared definition runs. It touches none of the self-disable path, so nothing in #809 alters #419's scope or correctness.
Current #419 WIP status (branch 2026-09-17_v2-direct-tool-init, off the #418 tip):
Fix implemented: optional withSessionMutationAndInitialize on the registry interface (lib/compress/types.ts) + ensureV2SessionInitialized() called in createV2Tool (lib/v2/tools.ts) after all guards, before definition.execute.
Not done yet: the dedicated fix: initialize persisted V2 ACP state before direct tool execution #419 regression test (empty V2 registry + persisted blocks/refs → execute a direct tool → no no initialized state, single load, transactional persistence) and WORKLOG.md. Only an import was added to the test file so far.
Treat the coexistence/mutual-exclusion question as a separate owner decision, not part of fix: initialize persisted V2 ACP state before direct tool execution #419. Per our rules I won't spin up new issues from this thread — if you want it tracked, tell me and I'll write up the finding here, or you open the issue. If opencode-acp should expose an explicit mutual-exclusion signal for bili's local mode, that's a small spec-first feature on its own.
Confirming intent: was the link awareness-only, or do you want me to (a) widen #419 scope, or (b) start scoping the coexistence piece? Absent other direction I'll proceed awareness-only: finish #419 as-is, track coexistence separately.
Activity
ranxianglei commentedon Sep 17, 2026
[bot] 🏷 Triage update — root cause confirmed in code, fix in progress.
Verification (bug triage per AGENTS.md):
lib/v2/tools.ts→createV2Tool) runs its guards and then calls the shared definition, which executes underwithToolSessionMutation(lib/compress/types.ts:168). That guard callsregistry.withSessionMutation(sessionID, run)(lib/state/state.ts:427), which throwsACP: session <id> has no initialized state(line 436) whenever the registry has no live state — exactly the reported error after a plugin reload/restart./acpcommand path already solves this:executeCommandusesregistry.withSessionMutationAndInitialize(host.sessions, …, () => host.sessions.messages(sessionID), …)(lib/v2/commands.ts:180), which atomically initializes missing state from host history under the session reservation. The V2 context hook does the same (lib/v2/context.ts:262). Only the direct-tool path lacks it.ensureSessionInitialized(lib/compress/pipeline.ts:60), but only against an already-resolved state object — unreachable when the registry is empty because the guard throws first. So the fix belongs in the V2 adapter layer, not the shared engine (which must keep V1 behavior).Fix plan: before every V2 direct ACP tool execution (after all lifecycle/subagent/permission guards, so denied calls never touch state), atomically initialize missing session state from
host.sessions.messages()through the samewithSessionMutationAndInitializeentry point used by commands — history load + persisted-state restore + persistence flush all under the session reservation. Live state short-circuits; concurrent calls serialize on the same per-session tail; inaccessible history fails closed with a normal tool error. Plus a regression test starting from an empty V2 registry with persisted blocks/refs executing a real direct tool.Working on branch off the #418 tip (
fork/2026-09-17_opaque-compaction-restore). Will follow up here with the PR link.drexb-ops commentedon Sep 17, 2026
Further investigation confirms the cold-registry defect with the real V2 compress wrapper and a valid request: persisted state exists with a saved block, a fresh registry is created, and execution returns
ACP compress failed: ACP: session audit-opaque has no initialized statebefore persisted history can load. The sharedwithToolSessionMutationcalls the non-initializing registry guard beforeprepareSessioncan reachensureSessionInitialized.Correction to the earlier diagnosis: a reload alone was not established as the cause of every live failure. Logs show repeated context projection rejections from duplicate system text (#422), followed by patch rejections caused by ID insertion before opaque multipart/error tools (#423). A rejected fresh context transaction discards initialization. These must be fixed together: hydrating the tool alone does not ensure saved compression reaches later model requests. Also track phantom system overhead (#421) and false savings for restored non-removable sources (#420).
Audit scope: dual-runtime fork drexb-ops/opencode-acp@1fe36e0 against official OpenCode v2.0.3 source. No production fix made in this audit.
ranxianglei commentedon Sep 17, 2026
link ranxianglei/billion-context#809
ranxianglei commentedon Sep 17, 2026
Read billion-context#809 end-to-end. Quick read on how it relates to #419:
What #809 is: a design doc (explicitly "a discussion artifact, not a finalized plan") for shipping
billion-contextas a no-launcher npm plugin on OpenCode 2.x —plugins: ["billion-context"], the plugin lazily spawns a loopback-bound proxy from the package root.Its two touches on opencode-acp:
opencode-acpalready publishesexports["./server"]as evidence the carrier route matches your existing practice. Informational; nothing requested here.BILLION_CONTEXT_PROXYenv,/bili/baseURL marker) don't fire in bili's plugin-local mode. #809 asks where the mutual-exclusion semantics should live.Does it change #419? No — orthogonal. #419 is a self-contained V2 cold-registry bug: after a reload the registry is empty, so the shared guard throws
no initialized statebeforeprepareSessioncan reachensureSessionInitialized. The fix hydrates the registry viawithSessionMutationAndInitializeinsidecreateV2Toolbefore the shared definition runs. It touches none of the self-disable path, so nothing in #809 alters #419's scope or correctness.Current #419 WIP status (branch
2026-09-17_v2-direct-tool-init, off the #418 tip):withSessionMutationAndInitializeon the registry interface (lib/compress/types.ts) +ensureV2SessionInitialized()called increateV2Tool(lib/v2/tools.ts) after all guards, beforedefinition.execute.tsc --noEmitclean; existingtests/v2-runtime-lifecycle.test.ts8/8 pass.no initialized state, single load, transactional persistence) andWORKLOG.md. Only an import was added to the test file so far.My recommendation:
Confirming intent: was the link awareness-only, or do you want me to (a) widen #419 scope, or (b) start scoping the coexistence piece? Absent other direction I'll proceed awareness-only: finish #419 as-is, track coexistence separately.
中文小结:#809 是 billion-context 的部署设计稿,与 #419 正交——#419 只修 V2 冷注册表初始化 bug(reload 后直连工具报
no initialized state),不碰自禁用逻辑;#809 提到 opencode-acp 仅为「加载契约先例」和「双压缩 owner 共存/互斥」两点。建议:#419 保持原范围补上直接工具的回归测试后单独 PR,共存语义作为独立决策另行处理。若链接仅为止会,我就按此推进完成 #419。ranxianglei commentedon Sep 17, 2026
继续
ranxianglei commentedon Sep 19, 2026
opencodeV2 use https://github.com/ranxianglei/billion-context