Repository navigation
OpenCode structured chat over the Agent Client Protocol - #25845
Conversation
…pted' (70fd4ba) into brennanb2025/acp-d3-grok
…c4655b4) into brennanb2025/acp-d3-grok
…P no longer borrows Codex's location rule The rule a self-supervised agent child runs under (this machine, no WSL, Windows only with process start-time proof) is its own module that Codex and the ACP adapter both use. Grok's registration takes the full account-home resolver signature, and D3's tests build hosts with the agent registry.
… the managed process's close A request the agent or a Stop cancels is answered with the agent's own cancelled reply by the code that owns it (the runtime no longer answers a silent handler), so a Stop needs no separate decline pass. A permission answer still being saved when the agent stopped waiting is reported unconfirmed, since the protocol already answered it cancelled. Cancelling the agent's own turn is the plain cancel. Request rows are matched under their generation-scoped ids. A refusal's reason comes from the dialect's wording path. The child drops its own stderr tail and close policy for the managed process's, and a close whose process tree was not proven gone is reported as the adapter contract asks.
…ing what Grok replays A chat with a saved Grok session reattaches with session/resume where the agent offers it, else session/load. Either way the call runs inside the translator's load window, so what Grok sends while it reattaches (its saved exchange, a task the dead process left running, ended by the restart) opens no turn and writes no row; only context usage reads on. A reply an Orca or Grok crash cut short is no longer completed from Grok's saved history: it reads like a Claude or Codex chat's, with the existing notice. The attach window also closes after a failed attach, and a created session that session/resume reports missing is replaced like one session/load reports missing. The replay reconciliation is removed: the lane no longer reads the journal, and D3's replayed-input grammar test and completed-turn check in the assembler go with it.
…f its own; its lane is replaced
… the launch options main requires
…mpty chat is reusable Main's idle-empty-chat lookup and launch join now take any registered agent, as the rest of the launch path does. The refusal check moved into the prompt turns and the prompt-block conversion beside the turns that send it, keeping both files in their line limit.
…turn; the next send resumes Grok's session/cancel ends only the running turn: work it already moved to the background keeps running and can begin a turn of its own after the person pressed Stop. Stop is now a session boundary, as it is for Claude: the cancel answers open requests and lets Grok end the turn, the host waits a bounded grace for that, then ends the process; the next send relaunches and resumes. The adapter's own bounded close of a turn Grok began is gone. Its named-turn check stays: the host ends the session unless the provider declines a Stop naming a turn that has since ended.
…en sends it A send that reached Grok while a prompt ran was held in the adapter until that turn ended: Steer on a queued card took the card out of the host's editable queue and meant 'send after this turn'. It now cancels the running prompt (session/cancel; the session stays) and sends as the next prompt once Grok answers the cancel, as the common pattern does; a steer behind another cancels it in turn, so the last one runs. The adapter holds a send only while that cancel lands, so its general held-send queue and its holdsDispatch report are gone (every send it holds has its turn open in the journal). An older client's mid-turn send takes the same path. capabilities.steering is unchanged and still unread.
…quire's own abort signal The host owns the acquire it runs, so it now owns its cancellation: each attach's acquire gets an AbortSignal, aborted from outside the session's queue by a close and by a Stop admitted now (the same admission rule as before). The optional abandonStart adapter hook, the router's fan-out to every adapter and the ACP adapter's session-keyed start map are gone; the ACP adapter keeps an unkeyed set of starts only so quit can prove their children gone, and keeps a failed start's unproven child until its exit is proven. The hook also let a later close ask that child again. The host now does that from state it holds: a close of a chat with no live child whose record still names an owner process with no death evidence asks the adapter to release it. The answer is not recorded as proof (the lease probe does that), so an owner pid an earlier Orca left is never killed or marked gone. Claude and Codex ignore the signal and hold no such child; their release is a no-op (tested).
…ll ends with Grok's last words On macOS and Linux the agent's stdout ends before its exit is observed, with or without the supervisor's EOF forwarding, so the connection's loss closed the journal first and its error text became the session's ended reason, dropping Grok's stderr. The reason is now read at the proven exit: the agent's last words when it left any, else why the connection closed. The failure already carried them. Comments that assumed the exit comes first, that early frames past the cap refuse the start, and that dispatch re-checks image support are corrected.
…written, marked as replay or not The reattach window relied on the dialect's replay verdict, and Grok's frames read as live unless they carry isReplay, so an unmarked chat frame during session/resume opened a turn that never ended. D3 now marks every frame inside the window as replay before the translator reads it, so the translator keeps only context usage whatever the agent marked; options and commands are still adopted. The translator's load semantics are unchanged.
…; the resume refreshes only the window
…re draining attaches A Grok that never answered its handshake held quit until the start's own 60 s bound, past the 20 s quit deadline. The host's teardown now aborts each in-flight acquire (and any the drain still begins), so the adapter's own quit controller and its map of starts are gone: a start has one canceller, the host's signal.
…the start has returned The listener stayed on the host's signal until the attach finished committing, so a Close in that window killed the now-live child behind the host's back and it read as Grok crashing. The start now detaches it when it ends; a later Close goes through the session's own stop.
…tart before it launches The attach began its abort controller only after reconciling leases, resolving recovery and probing the previous owner, so a close or admitted Stop in those phases reached nothing and Grok launched anyway. The controller now begins first, and the acquisition checks it before asking the adapter to start.
…apter to start Also renames the close test after the hook it no longer exercises.
…failed cleanup left The re-ask is not a no-op for them: when the adapter still holds the child its cleanup could not prove gone, the close stops it again as a requested close, and Claude persists the handle of the conversation it ran so the next send resumes it. Corrects the tests' and comment's wording; the close awaits the re-ask, bounded by each adapter's kill ladder.
…d sends once it ends A send while Grok ran a turn of its own (a background task waking it) went straight to Grok, which queued it behind that turn where Orca could no longer withdraw it, while Stop treated the same turn as the running reply. The send now waits as a steer, the turn is cancelled once, and the message goes when the turn ends; a Stop withdraws it and an exit rejects it as never sent.
…and is rejected as never sent Pins the bounded steer cancel kept from the runtime: past the bound the connection closes, the running reply reads unverifiable, Grok's end reads as its exit, and the waiting steer is rejected as never sent.
… its exit is proven After the connection broke and the close could not prove Grok's exit, any later stop Orca asked for (the next start, a Stop, a Close) marked the child as closed by Orca, so the crash read as a requested close and Grok's last words were dropped; a send meanwhile was recorded unconfirmed. The connection loss now decides the cause, and a send on that session is rejected as never sent.
…ability made stale CI's unit shards failed on tests outside the PR's own lists. Each encodes something this PR changes on purpose: Grok is now a registered agent (the seam test's unregistered agent is now Cursor); the desktop now advertises registered agents (the restart-offer tests' older client drops that capability explicitly); the attach context carries the start's abort controllers (the forget-status double gains them); and the ACP real-host test rig sends to the host directly (listed beside the other real-host rig in the send ratchet).
…t failure
With quit now aborting a start it would have waited for, the delivery step recorded the aborted
start as the message's failure ("couldn't restart"). After quit has stopped delivery, the step
leaves the message to quit, which settles it as a close does ("The chat closed before this message
was sent."). The test that pinned quit waiting for that start and stopping its child now pins that
nothing is launched behind quit.
…timeline-transitions
…hat-opencode-serve
… delivery callback
…e cross-version manifest Older builds read the list from the installed host, newer ones from the registrations it is built from; the manifest now asks only that Codex is listed, which holds for both.
…a slow startup Field QA: soon after a slow startup, a new tab opened the terminal chat because the local runtime had not reported its capabilities yet, which routes straight to the terminal; the bounded wait covered only an unlearned agent list. A new tab whose chat route waits only on either answer now asks for it and opens nothing until it comes or 3 seconds pass, then decides; the [agent-launch-route] line still names the blocker if it falls back. Tests that never meant to exercise startup now start with the runtime's answer known.
…hat-opencode-serve
…failed at startup When the startup probe fails (a rig whose terminal daemon failed to start), asking again answers null at once, so the new-tab wait ended early and the tab opened the terminal chat. The wait now subscribes to the event that delivers the capabilities, probes once, and stops at the 3 s cap; it applies to both routes, so OpenCode (whose blocker is the unlearned agent list) and Claude (unknown capabilities) both wait for it. Tests run the real capability module with a failed startup probe: Claude and OpenCode open their chat when a later probe lands within the wait, and the terminal chat after the cap otherwise.
…a failed chat Live QA with inline OpenCode credentials (OPENCODE_AUTH_CONTENT) passed the version check at create support and was refused only at create, so the window showed a failed "OpenCode Chat" with Retry where a declined launch opens the terminal chat. Create support now resolves the account a new chat would pin through the same read-only resolver the model catalog and create use, and answers no, logging the account-pin check, when the host refuses to pin it; any other failure is left for create to state. Create's own refusal stays as the backstop.
…hat-opencode-serve
|
Ready for review.
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configuration
📒 Files selected for processing (2)
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 2 remain after this review. 📝 WalkthroughWalkthroughThe pull request adds OpenCode as a structured ACP agent, including account binding, version checks, image prompts, tool-timeline translation, and recovery-history support. It extends account-home types and updates existing structured-agent consumers. Host and renderer launch flows now check structured-agent support, report downgrades, and can wait for host or runtime capability information before routing. Priority: ➖ Normal Merge Risk: 🔵 Low · up to On some Windows setups using Git Bash or MSYS, OpenCode’s structured launch may not find an installation under the Windows profile. This is a bounded platform-specific risk; affected users may need to correct their environment or command path. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/main/native-chat/agent-model-catalog/agent-model-catalog-fingerprint.ts (1)
27-35: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueFingerprints for OpenCode homes depend on the order of locator keys.
JSON.stringify(account.locator)serializes keys in insertion order. It also includes extra fields from newer builds, which the validator accepts. Two equal locators can therefore produce different fingerprints, and the catalog misses its cache. The failure is safe because the result is only a cache miss. Build the parts explicitly from the fields thatagentSessionAccountHomesEqualcompares.
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
12e268e2-f9a2-43bf-ae9d-5c27e2647c52
📒 Files selected for processing (94)
src/main/acp/acp-account-binding.tssrc/main/acp/acp-dialects/acp-dialect.tssrc/main/acp/acp-dialects/opencode-dialect.tssrc/main/acp/acp-launch-specs.tssrc/main/acp/acp-prompt-content.test.tssrc/main/acp/acp-prompt-content.tssrc/main/acp/acp-recovery-history.test.tssrc/main/acp/acp-recovery-history.tssrc/main/acp/acp-structured-acquire.tssrc/main/acp/acp-structured-adapter.test-support.tssrc/main/acp/acp-structured-agent-definitions.tssrc/main/acp/acp-structured-electron-free.test.tssrc/main/acp/acp-structured-fixture-replay.test-support.tssrc/main/acp/acp-structured-host.test-support.tssrc/main/acp/acp-structured-image-prompts.test.tssrc/main/acp/acp-structured-launch-resolution-opencode.test.tssrc/main/acp/acp-structured-launch-resolution.tssrc/main/acp/acp-structured-opencode-recordings.test.tssrc/main/acp/acp-structured-session-adapter-deps.tssrc/main/acp/acp-structured-session-adapter-lifecycle.test.tssrc/main/acp/acp-structured-session-adapter.tssrc/main/acp/acp-structured-turns.tssrc/main/acp/acp-timeline-fixture.test-support.tssrc/main/acp/acp-timeline-opencode-dialect.test.tssrc/main/acp/acp-timeline-requests.tssrc/main/acp/acp-tool-timeline.tssrc/main/acp/fixtures/opencode-v1-deny.jsonlsrc/main/acp/fixtures/opencode-v1-provider-error.jsonlsrc/main/acp/fixtures/opencode-v1-stray-write.jsonlsrc/main/acp/fixtures/opencode-v1-subagent-hang.jsonlsrc/main/acp/fixtures/opencode-v1-tool.jsonlsrc/main/agent-cli-version-probe.test.tssrc/main/agent-cli-version-probe.tssrc/main/agent-launch/agent-launch-executor.tssrc/main/agent-launch/agent-launch-mode.tssrc/main/claude/claude-at-rest-commands.tssrc/main/claude/claude-structured-agent-definition.tssrc/main/claude/claude-structured-launch-resolution.tssrc/main/claude/claude-structured-session-adapter.tssrc/main/codex/codex-structured-agent-definition.tssrc/main/codex/codex-structured-launch-resolution.tssrc/main/managed-data-accounts/service.test.tssrc/main/managed-data-accounts/service.tssrc/main/native-chat/agent-model-catalog/agent-model-catalog-fingerprint.tssrc/main/native-chat/agent-model-catalog/agent-model-catalog-service.tssrc/main/native-chat/agent-session-journal/journal-restart-reconciliation.tssrc/main/native-chat/agent-session-journal/journal-submission-reconciler.tssrc/main/native-chat/agent-session-wire/structured-agent-definition.tssrc/main/native-chat/agent-session-wire/structured-agent-session-launch-env.tssrc/main/native-chat/agent-session-wire/structured-agent-session-provider-support.tssrc/main/native-chat/structured-agent-command-resolution.test.tssrc/main/native-chat/structured-agent-command-resolution.tssrc/main/native-chat/structured-agent-session-create-support.tssrc/main/opencode-usage/opencode-database-discovery.tssrc/main/opencode-usage/scanner-wsl-gate.test.tssrc/main/opencode/opencode-acp-environment.tssrc/main/opencode/opencode-acp-stored-messages.test.tssrc/main/opencode/opencode-acp-stored-messages.tssrc/main/opencode/opencode-data-directory.test.tssrc/main/opencode/opencode-data-directory.tssrc/main/opencode/opencode-structured-account-home.test.tssrc/main/opencode/opencode-structured-account-home.tssrc/main/runtime/agent-session-reservation-admission.tssrc/main/runtime/orca-runtime-get-structured-agent-session-create-support.tssrc/main/runtime/orca-runtime-get-worktree-ps.tssrc/main/runtime/orca-runtime-structured-agent-registration-seam.test.tssrc/main/runtime/rpc/methods/structured-agent-session-agents.test.tssrc/main/runtime/rpc/methods/structured-agent-session-agents.tssrc/main/runtime/structured-agent-create-support-opencode-rig.test.tssrc/main/runtime/structured-agent-launch-support-probe.test.tssrc/main/runtime/structured-agent-launch-support.test.tssrc/main/runtime/structured-agent-launch-support.tssrc/main/runtime/structured-agent-runtime-registrations-acp.test.tssrc/main/runtime/structured-agent-runtime-registrations.tssrc/main/runtime/structured-agent-session-runtime.tssrc/renderer/src/lib/agent-launch-route-registered-agents.test.tssrc/renderer/src/lib/agent-launch-routing.tssrc/renderer/src/lib/agent-session-launch-plan.test.tssrc/renderer/src/lib/agent-session-launch-plan.tssrc/renderer/src/lib/launch-agent-in-new-tab-host-agents.test.tssrc/renderer/src/lib/launch-agent-in-new-tab-host-agents.tssrc/renderer/src/lib/launch-agent-in-new-tab-placement.test.tssrc/renderer/src/lib/launch-agent-in-new-tab.test.tssrc/renderer/src/lib/launch-agent-in-new-tab.tssrc/renderer/src/lib/launch-agent-structured-chat-guard.test.tssrc/renderer/src/runtime/host-structured-agents-sync.tssrc/renderer/src/runtime/host-structured-agents.test.tssrc/renderer/src/runtime/host-structured-agents.tssrc/renderer/src/runtime/local-runtime-capabilities.tssrc/shared/agent-session-account-home.test.tssrc/shared/agent-session-account-home.tssrc/shared/agent-session-record.tssrc/shared/agent-session-stored-agent.tstests/e2e/cross-version-wire/structured-agent-session-surface-manifest.ts
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 8 remain after this review.
| homePath?: string | ||
| } = {} | ||
| ): string { | ||
| const homePath = env.HOME ?? env.USERPROFILE ?? options.homePath ?? homedir() |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
homePath uses the POSIX home on Windows even when USERPROFILE is set.
env.HOME ?? env.USERPROFILE gives HOME priority on every platform. On Windows, Git Bash and MSYS often set HOME to a different directory. The install-directory search (~/.opencode/bin) can then miss the binary. The account resolver, childHomeDirectory, prefers USERPROFILE on win32. Select the home variable by platform so that both code paths use the same home.
An OpenCode chat with no managed profile no longer records data/state folders or a database choice, no longer refuses inline credentials or relative paths, and launches with the user's environment unchanged. Drops the create-support account-pin probe that only those refusals used.
…hat-opencode-serve # Conflicts: # src/main/claude/claude-structured-launch-resolution.ts
…hat-opencode-serve # Conflicts: # src/renderer/src/lib/launch-agent-in-new-tab.ts
…hat-opencode-serve
|
Status update: OpenCode accounts are no longer refused. The problem: when OpenCode had no Orca-managed account, this PR copied the data folder, state folder and database choice onto each chat. As a side effect it refused the chat view for two setups: credentials given inline in What changes for you: OpenCode with inline credentials or relative folders now opens the chat view like any other setup. If the credentials are wrong, the chat shows OpenCode's own error. How (
Since then main has been merged in three times. The last merge ( Not verified live after this change: the inline-credentials chat. Unit and rig tests cover it. |
…hat-opencode-serve # Conflicts: # src/shared/agent-session-account-home.ts # src/shared/agent-session-record.ts
…der the line limit
…hat-opencode-serve # Conflicts: # tests/e2e/cross-version-wire/structured-agent-session-surface-manifest.ts
Main's new host-agents route (stablyai#25845) imports `LaunchAgentInNewTabResult` from the new-tab launcher, which this branch moved into `launch-agent-in-new-tab-result` to keep the launcher under the line limit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ELI5
Today, when you open OpenCode in Orca's chat view, Orca runs OpenCode in a hidden terminal, rebuilds the conversation by reading OpenCode's database, and answers OpenCode's permission questions by typing keys into that terminal. With Settings > Chat UI > "Use updated structured native chat" on, OpenCode now talks to Orca directly over the Agent Client Protocol (ACP), the open protocol Orca's Grok chat already uses: Orca sees every message, tool run and permission request as it happens and answers them itself. With the setting off, nothing changes.
What Changed
The problem. OpenCode's chat in Orca is a terminal session read back from OpenCode's database. Tool output, approvals and Stop all go through a screen Orca has to interpret, so they lag, can be missed, and can't follow the account a chat was started under. An earlier version of this PR fixed that by running a private OpenCode server per chat; that approach is replaced here by the protocol other desktop agent clients use for OpenCode, which needs no server ownership in Orca.
What you see, with the setting on (OpenCode 1.x from 1.18.31, the
opencodecommand)Unchanged: with the setting off, and always for OpenCode 2, OpenCode keeps today's terminal-backed chat. That includes an
opencodecommand that is itself 2.x (Homebrew'sopencodeis 2.x now), and anyopencodeolder than 1.18.31 or whose version can't be read. Orca asks the installed binary its version before it creates the chat, so a launch on this computer opens the terminal chat instead. That fallback is #25947, so merge #25947 before this PR: without it, the same launch shows a failed chat start instead of the terminal.Your Command setting now reaches the structured chat. Before: Settings lets you point an agent at a specific program (its Command), and detection and the terminal chat ran that program, but the structured OpenCode and Grok chats ran whatever
opencode/grokcame first on PATH, which with Homebrew first can be a different install and a different version. Now the structured chat, and the version check before it, run the program your Command names, exactly as Claude and Codex chats already do; with no Command set they search PATH and the agent's install folder as before. A Command that isn't a runnable program stops the chat with "the opencode Command setting is not a runnable program" rather than quietly running another binary. Pi (#25851) picks this up after this PR lands, together with moving onto the shared version check.The first new tab of any registered agent opens its chat, including Grok on main today. Before: Orca decides whether a new agent tab is a chat in the window, from two answers it gets during startup: whether this computer's Orca runtime supports chats, and the list of agents it can run as chats (anything beyond Claude and Codex: OpenCode here, Grok already on main, Pi in #25851). The list was read once and never again, and a tab opened before either answer arrived went straight to the terminal chat, so after a slow or failed read every OpenCode or Grok tab opened as a terminal until restart, with nothing saying why (live QA hit this with a supported OpenCode 1.18.31 and with Pi). Now a new tab that is missing either answer asks for it and waits up to 3 seconds before opening anything, the list is read again as soon as the runtime has answered, and the list no longer depends on opening the chat journal first. If a tab still falls back to the terminal chat, one console line says why.
Mechanism
src/main/acp/acp-launch-specs.ts):opencode acp, on stable 1.x releases from 1.18.31 (the release the recordings capture). A row can name the releases it runs on:agentSession.createSupportrunsopencode --version(5 seconds, 4 KB at most) on the binary a launch would spawn, resolved from the same environment a launch starts from, and every launch asks again before spawning, so a binary swapped after the chat was created never starts. The probe is shared (src/main/agent-cli-version-probe.ts), a version it can't read counts as unsupported, and every check logs one line to Orca's main log ([agent-cli-version] <program> --version: <why>: <what it printed>, or... is supported) so a decision on a real machine can be explained. The launch setsOPENCODE_CLIENT=acpandOPENCODE_ENABLE_QUESTION_TOOL=falselast, after your own variables (OpenCode's question tool has no protocol answer and would wait forever). With full access, Orca answers each permission request "allow once", never "always".native-chat/structured-agent-command-resolution.ts, previously Claude and Codex only): your Command setting when set, refusing withagentCommandNotRunnablewhen it names nothing runnable, else the stock name on PATH and then the agent's install folders. The ACP launch and the create-time version check both call it, so they ask about and spawn the same file. At create an unrunnable Command is let through so the launch can say why, instead of silently opening a terminal.agentSession.agents). That answer now comes from the host's registration list, asagentSession.createSupportalready does, instead of from installing the chat host first; the window reads it again when its own runtime's capabilities land (host-structured-agents-sync.ts); and a new tab whose route waits only on that list, or on this computer's runtime capabilities during startup, asks for the missing answer and opens nothing until it arrives or 3 seconds pass, the same bound as a local chat's admission (launch-agent-in-new-tab-host-agents.ts). A host that does not publish its agents is never waited on.[agent-launch-route], window console), create-support's refusing check ([structured-create-support]), and a host launch the default asked to be a chat ([agent-launch]). Create-support's composition moved out of the unchecked runtime class intostructured-agent-launch-support.ts.acp-dialects/opencode-dialect.ts) reads a command's output and exit code from where OpenCode puts them, and relabels the "always" option.OPENCODE_CONFIG_DIRoverlay is replaced by your own config directory (or removed, including Orca's retired shared plugin directory), and every Orca OpenCode and data-account variable is stripped, so the chat is the only thing reporting its status.src/shared/agent-session-account-home.ts): a managed OpenCode profile id, or "unmanaged". Agent definitions now say whether their records pin one config-directory variable (Claude, Codex, Grok) or a locator (OpenCode), and each ACP agent row carries an account binding that resolves the locator when a chat is created and turns it into the launch environment. An unmanaged chat records no folders and no credentials: every launch passes your current OpenCode variables through unchanged (inline credentials inOPENCODE_AUTH_CONTENT, relative folders andOPENCODE_DBincluded), and OpenCode decides what they mean. No account setup refuses the structured chat.acp-prompt-content.ts): sent only when the agent row offers images and the agent's start advertises them, read and size-checked before anything reaches the agent, and bounded by what one protocol message can carry.Why
The common pattern among desktop agent clients runs OpenCode over ACP: the agent is a process per chat that Stop can end, and nothing in Orca has to own a server's lifetime. The previous server-based version of this PR needed port and password handling, an ownership ledger, two protocol dialects and orphan cleanup; all of that is deleted. The one thing the server route did that ACP cannot (approvals asked by OpenCode's subagents, below) is a known OpenCode bug every ACP client shares, and Stop is the way out.
Differences from the common pattern
--versionand held to a minimum supported release, as the common pattern does per provider. Intended differences: Orca also caps the major line (2.x runsacpin its background service, which a chat's launch environment can't reach), and an unsupported release falls back to the terminal chat instead of showing install guidance, because that chat already works with any release.opencode2, or anopencodethat is 2.x) is not on this lane. Its ACP command runs inside your per-user OpenCode background service, which a chat's launch environment and account do not reach, and a desktop launch that the host refuses has no terminal fallback yet. Follow-ups: the fallback (a declined local structured launch opens the terminal chat, as paired launches already do) is its own PR (fix(native-chat): a local chat this computer can't run opens the agent in a terminal #25947); OpenCode 2 returns after it, with how its chats name the service's account decided there.Known OpenCode bugs this inherits (every ACP client shares them)
/word(for example "/tmp/x is broken") as a command and ends the turn with no reply. The common pattern does not work around it either.Linked Issue
N/A (maintainer).
Visual Proof
Live QA in hidden Orca builds of this branch on macOS (two machines), with a private OpenCode 1.18.31 that Orca was pointed at through the OpenCode Command setting. Every launch ran with its own home and profile, and the real
~/.codex,~/.claude,~/.orca,~/.grokand OpenCode login files were hashed before and after every check: no change came from the test app.opencodethat is OpenCode 2.x (2.0.21): the version check keeps it on the terminal chat, no structured chat starts--version: 1.18.31 is supported; the shell command's row opens to show its output (hi) and exit code; the reply reads "Exit code: 3"sleep 30runs: "Interrupted after 31s", the command's file never written, the OpenCode process ends, the next prompt runs on a new process and remembers the conversationOPENCODE_AUTH_CONTENTin Orca's environment, OpenCode's data folder empty): New tab > OpenCode opens the chat view, not a terminal, and it repliesINLINE-OK; the variable reaches the spawned OpenCode 1.18.31 and no refusal is loggedXDG_STATE_HOME(rel-state) in OpenCode's environment setting: the chat view opens and repliesREL-OK; OpenCode puts its state under the project folder, as it decides{}): the chat view opens (no terminal, no failed-chat card) and the chat answeredhiwith no error shown. Why it could still answer with no stored login was not investigatedOn the image check: the image was attached and sent, but the model used for testing (OpenCode Go's DeepSeek V4.1 Flash) answered the wrong colour for a tiny 1x1 test image, so this does not prove the model saw it; the size bound and capability gate are unit-tested.
Found and fixed during QA: right after Orca started, a new OpenCode tab could open a terminal because the window had not yet learned which agents the host runs as chats (it read that list once at startup). The first launch now waits up to 3 seconds for that answer, which affects every registered agent, including Grok today.
Not verified live for OpenCode: restart recovery and quit mid-reply (shared with Grok's chat lane, verified there), Windows, Linux, SSH and mobile. The first-launch fix (3 s wait) is covered by tests; the live runs above opened the chat after startup had finished. Checks ran at 4a1fce5; later commits add the startup wait, a cross-version manifest update and type fixes.
Also found during QA: inline credentials in
OPENCODE_AUTH_CONTENTwere refused only when the chat was created, so a new tab showed "Chat could not be started" with Retry. That refusal is now gone entirely, as in the common pattern: with inline credentials the structured chat opens and OpenCode receives them unchanged. Re-checked live after that change on head4aa7e63b16a(the three rows above); the QA machine's real login and agent files were unchanged.Not part of this PR: the resolved card's "Answered on desktop-renderer" text names an internal client id for every agent on main today; that is a separate fix.
Testing
Recorded traffic: five scrubbed recordings of real
opencode acp1.18.31 sessions replay through the adapter (acp-structured-opencode-recordings.test.ts): an approved shell command with its exit code and the "Allow until OpenCode restarts" label, a denial, the subagent approval that never comes and Stop, the repeated file write leaving no row, and a provider error shown in OpenCode's own words.Version gate: 2.0.21 refused at create and at launch, 1.18.31 allowed, an unreadable version refused, and the create check asking about the same binary the launch spawns (
structured-agent-launch-support.test.ts,acp-structured-launch-resolution-opencode.test.ts,agent-cli-version-probe.test.ts); removing either check, or letting any version through, fails them.Real version check end to end (
structured-agent-launch-support-probe.test.ts):--versionactually runs against stand-inopencodescripts (2.0.21 first on the login-shell PATH, 1.18.31 elsewhere). The per-agent PATH and the Command setting each make both the create check and the launch use 1.18.31; with neither, 2.x keeps the terminal chat; an unrunnable Command is admitted and the launch refuses withagentCommandNotRunnable. Ignoring the Command setting fails two of them. The probe's refusal log line is asserted inagent-cli-version-probe.test.ts.Unit tests: launch environment and account, including inline credentials, relative folders and
OPENCODE_DBreaching the spawned OpenCode unchanged (acp-structured-launch-resolution-opencode.test.ts,opencode-structured-account-home.test.ts), dialect, image prompts and their size bound, restart recovery including identical-text cases, registrations.Final head
e00b9f6a886(latest main merged, including fix(native-chat): a local chat this computer can't run opens the agent in a terminal #25947, feat(native-chat): say in the chat why Orca stopped a reply, and offer Continue #25675 and [CI] Keep the structured-session compatibility manifest within the file limit #26342): both typechecks pass locally, as do the account, launch and cross-version wire tests. CI on the previous heada8c3aeec15fpassed every check (24 passed, 15 skipped); CI is running on this head.From live QA (a supported 1.18.31 named by the Command setting and the per-agent PATH still opened the terminal chat, with no version-check line in the main log): the host's whole create check under that run's environment (HOME and every XDG directory in the test rig, the Command and per-agent PATH naming a stand-in 1.18.31) admits it, including with inline credentials or relative folders set (
structured-agent-create-support-opencode-rig.test.ts), so the host did not decide; the window did, on an agent list it never learned. Tests: the first launch waits and opens the chat once the host lists OpenCode, decides without it once 3 seconds pass, never waits once the list is learned or with the setting off, and during startup, after a failed first capability probe, opens the Claude or OpenCode chat when a later probe lands within the wait, or the terminal chat (saying why) when none does (launch-agent-in-new-tab-host-agents.test.ts); the list is read again when the runtime's capabilities land (host-structured-agents.test.ts);agentSession.agentsanswers without installing the host (structured-agent-session-agents.test.ts); the window says why a launch opened the terminal chat (agent-launch-route-registered-agents.test.ts). Each was checked to fail with its fix removed. Which of the two silent startup paths the test machine hit (the read skipped while capabilities were unknown, or the host install failing) is not verified; both are removed.Rendered chat verified live (see Visual Proof). Not verified: Windows/Linux/WSL/SSH, the mobile app, and a chat on a real Orca-managed OpenCode profile.
I manually tested these changes locally (hidden Orca builds driven over CDP; see Visual Proof)
Automated tests added/updated, or explained why not below
Review
Reviewed with fresh adversarial reviewers and an independent architecture challenge; findings and fixes are summarized in the review comment.
Agent skill upstream boundary
docs/reference/agent-skill-sharing-upstream-boundary.mdand copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.Notes
SSH and WSL workspaces keep the terminal-backed chat (the ACP lane runs agents only on the machine Orca runs on). An older Orca that reads a chat record with the new OpenCode account locator sets that one record aside, unchanged, rather than failing.