Skip to content

feat(openclaw): ACP-to-HTTP adapter so OpenClaw shares the standing opencode serve (#1243) - #3997

Merged
dezren39 merged 1 commit into
mainfrom
queue/4972-1243-acp-http-adapter
Nov 2, 2026
Merged

dezren39 merged 1 commit into
mainfrom
queue/4972-1243-acp-http-adapter

Conversation

@dezren39

@dezren39 dezren39 commented Nov 1, 2026 •

Copy link
Copy Markdown
Member

Refs #1243 (task 4972). Does not close #1243: per its acceptance, closing waits until the live-smoke result is recorded (post-activation /acp spawn opencode session visible in GET /session on the 13420 server).

What

bin/opencode-acp-adapter.mjs — a single-purpose Node executable following the bin/*.mjs pattern that speaks ACP (JSON-RPC 2.0 over stdio, newline-delimited) on one side and the OpenCode serve HTTP API on the other, targeting the standing shared server (default http://127.0.0.1:13420, --server flag / OPENCODE_ACP_SERVER env; OPENCODE_SERVER_PASSWORD sent as basic auth when set). The adapter spawns no server of its own.

Mapping (per #1243):

  • session/new → POST /session (cwd → ?directory=)
  • session/prompt → POST /session/:id/prompt_async + GET /event SSE streamed back as ACP session/update notifications (text/reasoning part deltas → agent_message_chunk / agent_thought_chunk); prompt resolves on session.idle (session.error → JSON-RPC error)
  • serve permission.updated → ACP session/request_permission → reply posted to POST /session/:id/permissions/:permissionID (once/always/reject; a cancelled or failed client request denies rather than hanging)
  • session/cancel → POST /session/:id/abort; the in-flight prompt resolves with stop reason cancelled

Nix wiring

home/openclaw/default.nix repoints OpenClaw's opencode agent at the adapter in both config spots — the ~/.acpx/config.json entry and the embedded acpx override — with opencode acp retained as a documented one-line fallback in comments at both spots. Nix parse check passes; module evaluation is left to CI.

Tests

bin/opencode-acp-adapter.test.mjs (run: node --test bin/opencode-acp-adapter.test.mjs): a scripted ACP client drives the adapter over stdio against a stub serve HTTP API — initialize, session/new creating a session on the stub with the cwd as directory, a streamed prompt round-trip, a permission round-trip landing on the permissions endpoint, and cancel aborting on the serve. 5/5 pass on the laptop.

Coordination

PR #3275 (#1711, Claude/Codex ACP wrapper packaging) also edits home/openclaw/default.nix but does not touch the opencode agent entries; this PR's two hunks are confined to the opencode agent wiring and its surrounding comments, so any conflict on merge is textual and small. Whichever lands second rebases those hunks.

Still owed after merge + activation

Live smoke per #1243 acceptance: /acp spawn opencode in OpenClaw creates a session visible in GET /session on the 13420 server, with no new private opencode serve process for the adapter-routed session. #1243 stays open until that is recorded.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

…pencode serve (#1243)

bin/opencode-acp-adapter.mjs speaks ACP (JSON-RPC over stdio) to OpenClaw acpx and the OpenCode serve HTTP API to the standing shared server on 127.0.0.1:13420: session/new -> POST /session (cwd as directory), session/prompt -> prompt_async + GET /event SSE streamed back as session/update notifications, permission.updated -> session/request_permission -> POST permissions, session/cancel -> POST abort. No private per-process server is spawned.

home/openclaw/default.nix repoints the opencode agent at the adapter in both spots (~/.acpx/config.json and the embedded acpx override), with opencode acp kept as a documented one-line fallback. Tests: scripted ACP client vs stub serve API, 5/5 pass (node --test).
@dezren39
dezren39 requested a review from a team as a code owner November 1, 2026 21:27
@coderabbitai

coderabbitai Bot commented Nov 1, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 40 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 325c58dc-ca32-47d9-8a52-b55059af3145
Commits

Reviewing files that changed from the base of the PR and between 83709fa and 8e67215.

Files selected for processing: 3
  • bin/opencode-acp-adapter.mjs
  • bin/opencode-acp-adapter.test.mjs
  • home/openclaw/default.nix
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-01T21:31:21.708290Z 8e67215 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8e67215808

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +387 to +388
const session = await serveJson(withDirectory('/session', cwd), { method: 'POST', body: {} });
if (!session?.id) throw new Error('serve POST /session returned no session id');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Use the standing server's v2 session API

The configured target is the OpenCode 2.x server pinned in flake.nix, but this adapter implements the older session contract. The existing client for that same standing server, pm-stack/dispatch/src/opencode.ts, creates sessions with POST /api/session, supplies location.directory in the body, unwraps the {data: …} response, and submits prompts through /api/session/:id/prompt. Here, session creation instead uses /session?directory=… and expects a bare id; subsequent prompts likewise use the incompatible prompt_async route. Consequently, switching OpenClaw to this adapter does not provide a working session flow against its intended server. Implement and test against the pinned server's contract rather than the v1-shaped stub.

Useful? React with 👍 / 👎.

Comment on lines +65 to +67
const password = process.env.OPENCODE_SERVER_PASSWORD;
if (password) {
headers.Authorization = `Basic ${Buffer.from(`opencode:${password}`).toString('base64')}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Load the standing server credential for the configured adapter

In the configured OpenClaw gateway, the adapter only authenticates if OPENCODE_SERVER_PASSWORD happens to be inherited, but home/openclaw/default.nix neither supplies that variable nor installs a wrapper to load it. The standing server requires the password stored in ~/.local/state/opencode/service-prod.json: bin/openchamber-start.sh loads it explicitly, as does the dispatch client. Unlike the previous private-server ACP command, this adapter therefore sends unauthenticated session and event requests in the declared gateway environment, preventing session creation even after the API contract is corrected. Read the standing server's credential at runtime or explicitly wire it into the adapter's launch environment.

Useful? React with 👍 / 👎.

const pending = pendingPrompts.get(sessionId);
if (!pending) return;
pendingPrompts.delete(sessionId);
partTextSent.clear();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Clear streamed-text offsets only for the completed session

When two sessions have prompts in flight, completing either one clears the text offsets for both. The next cumulative text update for the still-running session is then emitted from character zero, duplicating content already delivered to its ACP client. Exercising the current handlers with session B emitting Hello, session A becoming idle, and B updating its part to Hello world produces the concatenated output HelloHello world. Keep offsets scoped to each session and remove only the completed session's entries.

Useful? React with 👍 / 👎.

Comment on lines +185 to +189
debug('event stream closed; reconnecting');
} catch (err) {
debug(`event stream error: ${err.message}; reconnecting`);
}
if (!shuttingDown) await new Promise((resolve) => setTimeout(resolve, 1000));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reconcile pending prompts when the event stream disconnects

If the SSE connection drops during an active prompt and the server completes that prompt before reconnection, its final session.idle event is lost. This reconnect path neither resolves or rejects pending prompts nor queries their current status, and session/prompt has no other completion mechanism. The ACP request therefore remains pending indefinitely, with subsequent prompts rejected as already in flight even after the stream reconnects. Reconcile pending sessions after reconnecting, or explicitly fail their outstanding requests when event continuity is lost.

Useful? React with 👍 / 👎.

Comment on lines +420 to +422
const sessionId = params?.sessionId;
if (!sessionId || !sessions.has(sessionId)) return;
cancelledSessions.add(sessionId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Ignore cancellation after the prompt has already finished

When a cancellation notification races with the prompt's final idle event, the prompt can finish before this handler runs. The handler still records the session as cancelled, but an ensuing idle event cannot remove that marker because cleanup in handleServeEvent requires a pending prompt. The next normally completed prompt then incorrectly returns stopReason: "cancelled". The marker also survives a cancelled prompt ending through session.error. Only mark active prompts as cancelled and clear their cancellation state on every terminal path.

Useful? React with 👍 / 👎.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OpenClaw: share one opencode serve via a small ACP-to-HTTP adapter

1 participant