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
Mcode lists OpenCode as coming soon, so a user who installed and authenticated the opencode CLI cannot run a thread on it, pick its models, switch a thread to it, or hand work to it. Every other harness in this comparison (Synara, T3 Code) already serves OpenCode sessions, and both chose the same door: a local opencode serve process driven through the generated SDK, not opencode acp.
Solution
Detect the opencode CLI, pool one serve process per working directory with idle close and proven kill, and run turns through the SDK with live event streaming, session resume, paged history, and permission mediation. The provider speaks only canonical contracts: exhaustive SSE mapping into typed ingress, push turn-diff evidence, capability-declared approval review, and no provider-owned filesystem surface. It reuses the server-owned policy, diff, and invalidation seams defined by the provider-completeness program, so Review UI and core orchestration never branch on OpenCode.
flowchart LR
R[Renderer] <-->|WS RPC + agent.event| S[Mcode server]
S -->|sendTurn| P[OpenCodeProvider]
P -->|acquire binary+cwd| O[Server pool]
O -->|spawn or reuse| A[serve cwd A]
O -->|spawn or reuse| B[serve cwd B]
A -.->|SSE /event + REST| P
P -->|canonical events| E[Event pipeline]
E -.->|push| R
P -->|terminateTree| K[OS process tree]
Loading
sequenceDiagram
participant C as Composer/AgentService
participant P as OpenCodeProvider
participant O as Server pool
participant V as opencode serve
participant I as Provider ingress
C->>P: sendTurn (resolved modes, resume cursor)
P->>O: acquire key binary+cwd
O->>V: spawn serve --port N (on miss, 20s ready wait)
P->>V: create client + re-adopt ses_ (404-only fresh)
P->>V: event.subscribe, then prompt_async 204
V->>P: SSE deltas, tools, questions, permissions
P->>I: canonical ProviderRuntimeEvent
P->>V: permission reply once/always/reject
P->>P: session.status idle ⇒ turn.completed
C->>P: stop ⇒ session.abort + children, release ref
Loading
stateDiagram-v2
[*] --> Cold
Cold --> Starting: acquire miss
Starting --> Ready: ready-prefix seen
Ready --> Idle: refs reach 0
Idle --> Ready: acquire hit
Idle --> Cold: 5-min TTL or exit seen
Loading
User Stories
As a user, I can enable OpenCode in provider settings when opencode --version succeeds, so that my existing OpenCode login works without new credentials.
As a user, I can pick an OpenCode model per thread from the discovered catalog, so that provider choice stays per task.
As a user, I can send a turn and watch text, tool calls, and progress stream live, so that long runs stay legible.
As a user, I can stop a turn and see it settle promptly with children stopped, so that a runaway agent never keeps working.
As a user, I can restart the app and resume the same OpenCode session, so that context survives restarts.
As a user, I can reopen a heavy thread without a hang, so that long histories load paged with a visible error on failure.
As a user, I can answer permission and question cards inline, so that supervised turns proceed safely.
As a user, I can run a Plan turn that only allows read-only tools, so that planning never mutates files.
As a user, I can run threads in two worktrees at once with isolated servers, so that parallel work never shares a backend.
As a user, I can attach to an external serve URL instead of a spawned one, so that a shared backend stays shared.
As a user, I see a Last turn comparison with a source or fidelity label when native evidence is absent, so that I trust what the diff claims.
As a user, I can open Review mid-turn for a Live view that settles on completion, so that I inspect work early.
As a user, I can request automatic review and see reviewing, approved, or denied states, so that review is explicit.
As a user, I see a manual-review fallback when automatic review is unavailable, so that I am never silently unreviewed.
As an administrator, a managed rule requiring automatic review blocks dispatch when it cannot run, so that policy never weakens silently.
As a user, I see Full Access turns carry no review label, so that bypassing review is never mislabelled.
As a user, I see warnings, reroutes, config, and auth notices as bounded thread or session diagnostics, so that provider signals never break the screen.
As a user, I see Files, autocomplete, preview, and Review refresh after workspace changes, so that surfaces stay current.
As a user, I can fork or switch a thread to or from OpenCode through handoff, so that provider choice is reversible.
As a maintainer, I can see every SSE type mapped, state-only, diagnostic, or ignored-with-reason, so that coverage is auditable.
As a maintainer, I can add OpenCode diff or review support through declared capabilities and conformance tests, so that core and UI stay untouched.
As a verifier maintainer, I can prove OpenCode journeys across web and desktop clients, so that the provider matrix stays green.
As a user, I see one active presentation for reroutes and notices without duplicates across reconnects, so that signals stay calm.
Decision Sources
Provider-neutral Codex protocol completeness #1610 provider-completeness program: typed ingress only, push ITurnDiffSource with no provider pull, server-owned TurnDiffService and ApprovalReviewPolicy, static capability declarations, no provider filesystem surface, deferred remote watches.
Show and persist provider-native Codex Last turn diffs #1612: push diff identity by turn, execution, delivery attempt, revision; snapshot, invalidated, and indeterminate-empty states; settled record one-to-one with the assistant message; byte limit documented; never truncate.
Run provider-neutral automatic review for supported turns #1614: small IAgentProvider; optional side-effect-free support inspection with available, required, and unavailable outcomes; resolved review and permission modes travel atomically with sendTurn.
Enforce automatic-review fallback and permission safety #1615: unavailable automatic falls back to visible manual-required or blocks under managed policy; Full Access is not review; strict routing waits for a real permission request; one terminal outcome per attempt; redacted payloads.
External prior art: Synara pools serve per working directory with a 5-minute idle TTL and drives it via createOpencodeClient; T3 Code spawns local serve when no URL is set and reads turns from the SSE stream. Known failure modes adopted as requirements: envelope-shape skew, unpaginated history hydration, create-instead-of-resume context loss, and shared-server port contention.
Working visual companion: .dev/verification/opencode-serve-architecture.html in the main worktree (topology, turn sequence, pool and turn state, contract drafts). Direction accepted in review; it is a working file, not an immutable prototype.
Accepted variant: pooled serve per working directory driven over SDK and SSE.
Immutable accepted version: none. Mutable working file, supplementary only.
Implementation Decisions
New provider module owns the IAgentProvider implementation plus session-eviction behavior. It composes the existing shared session runtime (pool, idle eviction, process cleanup) with a small protocol seam: spawn, busy check, graceful interrupt, teardown, and staleness check.
New server-pool module owns one serve child per binary, working directory, and hostname key. It waits for the ready prefix with a bounded timeout, tracks reference counts, closes idle entries after five minutes, watches for unexpected exits, and proves process-tree termination on close. An explicit external URL bypasses ownership entirely.
The provider keeps per-thread session state only: borrowed pool key, adopted upstream session id, active-turn guard, serialized prompt chain, and diff revision cursor. The pool owns processes; the turn owns prompts.
Session resume re-adopts the stored upstream session id behind a versioned cursor. Only a confirmed missing-session signal starts fresh; any other failure propagates so a live thread never resets to empty.
History hydration pages with limit and cursor plus timeout and abort. No unbounded full-history fetch.
Prompts go through the asynchronous send that returns immediately; completion is detected by idle session status with bounded polling, not by stream close.
The event mapper is pure and exhaustive: every upstream event type classifies as mapped, state-only, diagnostic, or ignored-with-reason, and the envelope parser accepts both wrapped and flat shapes. Raw upstream payload never reaches persistence, transport, or UI.
Permission and question events route through the existing permission request flow; the provider replies once, always, or reject. Policy code never answers a request.
Turn-diff evidence travels through the push seam keyed by turn, execution, delivery attempt, and monotonic revision, with snapshot, invalidated, and indeterminate-empty states reconciled against file-effect data. If the upstream exposes no usable native diff, the provider uses reconstruction and Git fallback only.
Approval review travels through the capability seam: static declaration plus side-effect-free support inspection, modes mapped to native turn configuration, native lifecycle mapped to canonical events, atomic with dispatch. Full Access bypasses review and emits no review lifecycle.
Permission and review modes on dispatch are the resolved values only; an unresolved default never ships.
Provider ports stay narrow: settings and skills only. No filesystem watch or mutation surface on the provider; file consumers read through the server-owned invalidation service.
Upstream binary and server versions plus SDK version are recorded in implementation evidence, mirroring the upstream-commit rule of the completeness program.
Decision-encoding shapes (trimmed from the working companion):
// Versioned resume cursor. Unknown versions are ignored, never misread.typeOpenCodeResumeCursor={schemaVersion: 1;sessionId: `ses_${string}` };// SSE envelope accepted in both wrapped and flat shapes.typeOpenCodeSseEnvelope={type: string;payload?: {type: string;properties: Record<string,unknown>};properties?: Record<string,unknown>;};// Pool isolation boundary. Never share a server across working directories.typeOpenCodePoolKey=Readonly<{binaryPath: string;cwd: string;hostname: string}>;// Dispatch carries resolved modes only; unresolved defaults never ship.typeOpenCodeDispatchModes={permissionMode: "full"|"supervised";approvalReviewMode: "manual"|"automatic";};
Testing Decisions
Good tests prove external behavior at the seam: streamed turns complete, stops abort children, resume keeps context, heavy history loads paged, unknown events become bounded diagnostics. They never assert spawn internals, timer wiring, or SDK call order.
Pure mapper tests use recorded fixtures in both envelope shapes plus unknown-type and oversized-payload cases, following the exhaustive-registry proof required for typed ingress.
Pool tests prove keyed reuse, idle close with proven tree kill, unexpected-exit cleanup, and external-URL non-ownership.
The provider passes the shared review and diff conformance suites as the second adapter alongside the existing ones, proving generic core behavior without provider branches.
Regression cases replay the adopted external failures: envelope skew dropping all events, unbounded history hanging heavy threads, create-instead-of-resume losing context, and shared-server contention.
Live proof extends the verifier feature map to OpenCode: native versus fallback diff with source labels, notice and diagnostic variants, automatic and manual-required and managed-block and Full Access and strict-handoff routes, retry staleness rejection, invalidation across the four surfaces, and live-to-settled reconnect, on web and desktop.
Out of Scope
An opencode acp stdio path as the main integration.
A raw upstream-event bridge into ingress, transport, or UI.
A provider pull method for settled turn diffs.
Migration of cumulative, branch, staged, unstaged, or commit comparisons off Git.
Provider filesystem watch or mutation methods, including write, remove, and copy.
A pre-send review configuration call or live reviewer update in the first slice.
Treating Full Access as automatic review, or a strict-routing notice as a grant.
Persisting raw or unstable upstream payloads.
Further Notes
Build order, smallest stable slice first: pool with proven kill; turn with subscribe, async send, and idle completion; exhaustive mapping with permissions; hardening with paged history, resume cursor, external attach, and availability gate.
Open evidence questions: which upstream evidence qualifies as native turn diff (unified versus before-and-after blocks); the exact capability and version predicate enabling review and diff support; the accepted byte limit rejecting native diff evidence before persistence.
Sequencing against the completeness program: the pool and turn slices can proceed on the existing ingress seam; the diff and review adapters land on the server-owned services as those slices land.
Problem Statement
Mcode lists OpenCode as coming soon, so a user who installed and authenticated the
opencodeCLI cannot run a thread on it, pick its models, switch a thread to it, or hand work to it. Every other harness in this comparison (Synara, T3 Code) already serves OpenCode sessions, and both chose the same door: a localopencode serveprocess driven through the generated SDK, notopencode acp.Solution
Detect the
opencodeCLI, pool oneserveprocess per working directory with idle close and proven kill, and run turns through the SDK with live event streaming, session resume, paged history, and permission mediation. The provider speaks only canonical contracts: exhaustive SSE mapping into typed ingress, push turn-diff evidence, capability-declared approval review, and no provider-owned filesystem surface. It reuses the server-owned policy, diff, and invalidation seams defined by the provider-completeness program, so Review UI and core orchestration never branch on OpenCode.User Stories
opencode --versionsucceeds, so that my existing OpenCode login works without new credentials.serveURL instead of a spawned one, so that a shared backend stays shared.Decision Sources
ITurnDiffSourcewith no provider pull, server-ownedTurnDiffServiceandApprovalReviewPolicy, static capability declarations, no provider filesystem surface, deferred remote watches.IAgentProvider; optional side-effect-free support inspection with available, required, and unavailable outcomes; resolved review and permission modes travel atomically withsendTurn.serveper working directory with a 5-minute idle TTL and drives it viacreateOpencodeClient; T3 Code spawns localservewhen no URL is set and reads turns from the SSE stream. Known failure modes adopted as requirements: envelope-shape skew, unpaginated history hydration, create-instead-of-resume context loss, and shared-server port contention..dev/verification/opencode-serve-architecture.htmlin the main worktree (topology, turn sequence, pool and turn state, contract drafts). Direction accepted in review; it is a working file, not an immutable prototype.Prototype Evidence
.dev/verification/opencode-serve-architecture.html(main worktree).serveper working directory driven over SDK and SSE.Implementation Decisions
IAgentProviderimplementation plus session-eviction behavior. It composes the existing shared session runtime (pool, idle eviction, process cleanup) with a small protocol seam: spawn, busy check, graceful interrupt, teardown, and staleness check.servechild per binary, working directory, and hostname key. It waits for the ready prefix with a bounded timeout, tracks reference counts, closes idle entries after five minutes, watches for unexpected exits, and proves process-tree termination on close. An explicit external URL bypasses ownership entirely.Decision-encoding shapes (trimmed from the working companion):
Testing Decisions
Out of Scope
opencode acpstdio path as the main integration.Further Notes