Skip to content

fix: initialize persisted V2 ACP state before direct tool execution #419

Closed

Description

@drexb-ops

Reproduction

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.
  • Full V2 tests and installed V1/V2 E2E pass.

Related: #395, #418.

Activity

  1. ranxianglei commented on Sep 17, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

    [bot] 🏷 Triage update — root cause confirmed in code, fix in progress.

    Verification (bug triage per AGENTS.md):

    1. 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.
    2. 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.
    3. 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.

  2. drexb-ops commented on Sep 17, 2026

    @drexb-ops
    ContributorAuthor

    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.

  3. ranxianglei commented on Sep 17, 2026

    @ranxianglei
    Owner
  4. ranxianglei commented on Sep 17, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

    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-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:

    1. 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.
    2. 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.
    • tsc --noEmit clean; existing tests/v2-runtime-lifecycle.test.ts 8/8 pass.
    • 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.

    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。

  5. ranxianglei commented on Sep 17, 2026

    @ranxianglei
    Owner

    继续

  6. ranxianglei commented on Sep 19, 2026

    @ranxianglei
    Owner
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions