Skip to content

Serve OpenCode sessions through a pooled serve + SDK provider #1621

Description

@chuks-qua

Problem Statement

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

  1. As a user, I can enable OpenCode in provider settings when opencode --version succeeds, so that my existing OpenCode login works without new credentials.
  2. As a user, I can pick an OpenCode model per thread from the discovered catalog, so that provider choice stays per task.
  3. As a user, I can send a turn and watch text, tool calls, and progress stream live, so that long runs stay legible.
  4. As a user, I can stop a turn and see it settle promptly with children stopped, so that a runaway agent never keeps working.
  5. As a user, I can restart the app and resume the same OpenCode session, so that context survives restarts.
  6. As a user, I can reopen a heavy thread without a hang, so that long histories load paged with a visible error on failure.
  7. As a user, I can answer permission and question cards inline, so that supervised turns proceed safely.
  8. As a user, I can run a Plan turn that only allows read-only tools, so that planning never mutates files.
  9. As a user, I can run threads in two worktrees at once with isolated servers, so that parallel work never shares a backend.
  10. As a user, I can attach to an external serve URL instead of a spawned one, so that a shared backend stays shared.
  11. 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.
  12. As a user, I can open Review mid-turn for a Live view that settles on completion, so that I inspect work early.
  13. As a user, I can request automatic review and see reviewing, approved, or denied states, so that review is explicit.
  14. As a user, I see a manual-review fallback when automatic review is unavailable, so that I am never silently unreviewed.
  15. As an administrator, a managed rule requiring automatic review blocks dispatch when it cannot run, so that policy never weakens silently.
  16. As a user, I see Full Access turns carry no review label, so that bypassing review is never mislabelled.
  17. 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.
  18. As a user, I see Files, autocomplete, preview, and Review refresh after workspace changes, so that surfaces stay current.
  19. As a user, I can fork or switch a thread to or from OpenCode through handoff, so that provider choice is reversible.
  20. As a maintainer, I can see every SSE type mapped, state-only, diagnostic, or ignored-with-reason, so that coverage is auditable.
  21. As a maintainer, I can add OpenCode diff or review support through declared capabilities and conformance tests, so that core and UI stay untouched.
  22. As a verifier maintainer, I can prove OpenCode journeys across web and desktop clients, so that the provider matrix stays green.
  23. 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.
  • Render Codex protocol notices through typed ingress #1611: exhaustive notification disposition; unknown valid notices become bounded diagnostics; no raw-payload bridge.
  • 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.
  • Add Cursor and fallback parity to Last turn Review #1613: source priority native unified, then before-and-after blocks, then reconstruction, then Git; one normalized comparison; no UI provider branches.
  • 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.
  • Refresh file consumers through local workspace invalidation #1616: server-owned local invalidation to Files, autocomplete, preview, and Review; no watch or mutation methods on providers.
  • Prove provider-completeness journeys across clients and providers #1617: verifier feature map and cross-client journeys; missing access is a coverage gap, not a pass.
  • 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.

Prototype Evidence

  • Source: .dev/verification/opencode-serve-architecture.html (main worktree).
  • 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.
type OpenCodeResumeCursor = { schemaVersion: 1; sessionId: `ses_${string}` };

// SSE envelope accepted in both wrapped and flat shapes.
type OpenCodeSseEnvelope = {
  type: string;
  payload?: { type: string; properties: Record<string, unknown> };
  properties?: Record<string, unknown>;
};

// Pool isolation boundary. Never share a server across working directories.
type OpenCodePoolKey = Readonly<{ binaryPath: string; cwd: string; hostname: string }>;

// Dispatch carries resolved modes only; unresolved defaults never ship.
type OpenCodeDispatchModes = {
  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.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    ready-for-agentTriaged; ready for an autonomous coding agent to pick up

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions