Skip to content

fix(acp): retain command streaming and explicit cancellation - #53913

Open
danyel117 wants to merge 5 commits into
anomalyco:v2from
danyel117:goal-acp-stream
Open

danyel117 wants to merge 5 commits into
anomalyco:v2from
danyel117:goal-acp-stream

Conversation

@danyel117

@danyel117 danyel117 commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Issue for this PR

Closes #53914

Related report: prevalentWare/opencode-goal-plugin#59

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

Registered command executors can submit several executions before returning. ACP currently correlates the wrong initial inbox and stops at the first terminal event. Continue consuming command-owned events until the executor ends, retain the latest outcome, and drain its last in-flight execution before replying.

Pass the per-command abort signal to Promise executors. Add an explicit session interruption hook so cooperating plugins can persist Cancel even between execution cycles. ACP transport teardown aborts local work without sending the explicit user interrupt API. Each cancellation request cleans its intent after interrupt completion, including cancellation during final usage reporting.

Plan metadata projection is independent in #53929 (issue #53928).

How did you verify your code works?

Required Bun 1.4.2: full upstream lint and all 36 package typechecks pass. This lifecycle slice's ACP suite: 103 pass, 0 fail. Core session activation suite: 4 pass, 0 fail, including cold idle interruption hooks and Promise executor signal isolation. The late-cancel regression fails before its fix and passes after it.

Native isolated ACP with a local SSE fixture: command remains pending across multiple automatic executions and a 311-second continuation interval. Cancel stops a foreground worker and cancels an idle goal with no later restart. Closing ACP and its standalone server preserves the active goal. No live user state or provider credentials used. The complete combined implementation was also verified with live/replayed plans before slicing.

Screenshots / recordings

Verified ACP JSON-RPC transcripts; no client UI changes.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

Chain Context

Independent host slice: 284 changed lines, starts at v2 434f7b2 and ends with complete command streaming and explicit cancellation. Plan projection follows independently under #53928. Plugin persistence/planning is outside this repository. Rollback restores the previous adapter and plugin SDK behavior.

v2
 ├── 📍 #53913 command lifetime and Cancel
 └── #53929 plan projection

@github-actions github-actions Bot added contributor needs:compliance This means the issue will auto-close after 2 hours. needs:issue labels Oct 8, 2026
@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

Thanks for your contribution!

This PR doesn't have a linked issue. All PRs must reference an existing issue.

Please:

  1. Open an issue describing the bug/feature (if one doesn't exist)
  2. Add Fixes #<number> or Closes #<number> to this PR description

See CONTRIBUTING.md for details.

@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

The following comment was made by an LLM, it may be inaccurate:

@danyel117
danyel117 marked this pull request as ready for review October 8, 2026 09:19
@github-actions github-actions Bot removed the needs:compliance This means the issue will auto-close after 2 hours. label Oct 8, 2026
@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

Thanks for updating your PR! It now meets our contributing guidelines. 👍

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked this with the new wire tests: both fail on the base and pass here. The ACP suite passes (104/104), the core activation tests pass, and cli and plugin typecheck cleanly. The approach works: streaming lasts as long as the executor runs, the final run is drained, the abort signal reaches Promise executors, and a metadata.acp.plan validated against a schema becomes ACP plan updates. Main concerns:

  • After a command returns, deciding whether to drain depends on how far the event consumer has got. The final run's updates can be dropped (inline comment).
  • Any inbox delivery now starts a command turn, so other prompts in the same session can show up in it (inline comment).
  • The second new test relies on a 20 ms setTimeout before checking that nothing has responded yet. That kind of negative check with a sleep can pass by chance. Please tie it to an event instead, or drop that assertion.

if (prompt.command) {
yield* Deferred.succeed(turn.commandFinished, undefined)
const state = yield* Ref.get(turn.state)
if (state.executing) return yield* Fiber.join(events)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This reads the turn state once, right after session.command returns. Events still waiting in the subscription have not been folded in yet. If the executor's last run started and finished on the server but the consumer has not yet processed its session.execution.started, executing is still false. The turn then returns state.terminal ?? "succeeded" straight away and that run's text and tool updates are dropped (the outcome may also be out of date). The new test avoids this only by waiting for the first chunk before releasing the gate. Could the consumer catch up first, e.g. by draining events up to a marker, or by waiting for the matching terminal event, before checking executing?

}
if (!sessionID || (sessionID !== ctx.sessionID && !child)) return { state, outputs: [] }
if (event.type === "session.inbox.delivered" && event.data.inboxID === ctx.start.id)
if (event.type === "session.inbox.delivered" && (event.data.inboxID === ctx.start.id || ctx.command))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With ctx.command set, any session.inbox.delivered for this session starts the turn, not only the command's own. A prompt from another client, or a queued one, delivered while the command is running would then be streamed into this ACP turn, and its terminal events would count toward the command's outcome. Can the inbox IDs created by the executor be linked to the command, e.g. by having session.command return or accept the ID?

@danyel117 danyel117 changed the title fix(acp): stream command executions and plugin plans fix(acp): retain command streaming and explicit cancellation Oct 8, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant