# Spawning OpenCode ## Status: Draft ## Date: 2026-04-13 ## Purpose This document describes the local helper we need for starting OpenCode when the new server ships with a bundled OpenCode binary. The key problem is: - `@opencode-ai/sdk` provides a `createOpencode()` helper - that helper expects to launch `opencode` from `PATH` - our distribution plan wants the OpenWork server to ship its own bundled OpenCode binary - therefore we need a local helper that mimics the useful startup behavior while allowing an explicit binary path ## Design Goal Build a small local helper that: - starts OpenCode directly with Bun runtime process APIs - accepts an explicit binary path - waits until OpenCode is actually ready - accepts a custom config object generated by the OpenWork server - returns an OpenCode HTTP client from `@opencode-ai/sdk` - does not modify the upstream SDK The SDK should still be used for the HTTP client. The SDK should not be used for server startup. ## Why We Need This In the target Server V2 distribution model: - `openwork-server-v2` is the primary executable - it embeds `opencode` - it extracts `opencode` into a managed runtime directory - it launches that exact binary path So the normal case is not: - look on `PATH` for `opencode` The normal case is: - use the exact extracted OpenCode binary path managed by the server runtime That means relying on `PATH` is the wrong startup model. We need deterministic control over: - which `opencode` binary is launched - which hostname and port it binds to - which config blob it receives - how readiness and failure are detected That config blob should come from the OpenWork server's own database-backed config model, not from ad hoc client state. ## Proposed Helper Location Suggested local helper location: ```text apps/server-v2/src/adapters/opencode/local.ts ``` This should remain app-local/server-local code, not a patch to `node_modules`. ## Proposed Public API Use one exported function: ```ts createLocalOpencode(opts?) ``` Suggested signature: ```ts type CreateLocalOpencodeOptions = { binary?: string hostname?: string port?: number timeout?: number signal?: AbortSignal config?: Record client?: Record } ``` Suggested defaults: - `binary`: omitted only if the helper can resolve the extracted bundled runtime binary path from server-managed runtime state; otherwise startup should fail - `hostname`: `"127.0.0.1"` - `port`: `4096` - `timeout`: `5000` Important rule: - the helper should not rely on `PATH` - the helper should always launch the bundled OpenCode binary extracted by the server runtime - if no explicit bundled binary path is available, that should be treated as a startup error, not as a signal to fall back to a system binary ## Runtime Behavior The helper should: 1. resolve the binary path 2. spawn OpenCode with `serve` 3. watch stdout/stderr for readiness 4. parse the listening URL 5. create an OpenCode HTTP client using `createOpencodeClient` 6. return both the client and server process handle ## Spawn Contract Spawn should use: ```text binary serve --hostname= --port= ``` If `config.logLevel` exists, append: ```text --log-level= ``` Environment should merge `process.env` with: ```text OPENCODE_CONFIG_CONTENT= ``` Where: ```ts JSON.stringify(config ?? {}) ``` ## Config Source Of Truth The config object passed into this helper should be generated by the OpenWork server. That means: - the server reads its sqlite state - the server resolves the effective config for the relevant runtime or workspace - the server materializes an OpenCode config object - the helper passes that object through `OPENCODE_CONFIG_CONTENT` The helper should not decide config policy. Its job is to: - receive the already-computed config object - serialize it - launch OpenCode with it This keeps the ownership boundary clean: - server DB and services own config state - the spawn helper owns process startup only ## SDK Usage Use the SDK only for the HTTP client: ```ts import { createOpencodeClient } from "@opencode-ai/sdk" ``` The helper should create the client after readiness: ```ts createOpencodeClient({ baseUrl: url, ...clientOpts, }) ``` ## Readiness Detection The helper should detect readiness by watching stdout and stderr for a line like: ```text opencode server listening on http://... ``` It should: - capture stdout and stderr incrementally - parse the URL from the first matching line - treat that as the canonical base URL Suggested parser behavior: - scan each emitted line - match `http://...` or `https://...` - store the parsed URL exactly as reported ## Failure Handling The helper should fail clearly in three main cases. ### 1. Missing binary / `ENOENT` If the binary does not exist or cannot be launched: - reject immediately - include the binary path/name in the error - include a clear explanation that the executable was not found Suggested error shape: ```text Failed to start OpenCode: executable not found at ``` ### 2. Timeout before readiness If startup exceeds the timeout: - kill the child process - reject with a timeout error - include collected stdout/stderr in the error message Suggested error shape: ```text OpenCode did not become ready within 5000ms. Collected output: ... ``` ### 3. Early exit before ready If the child exits before a readiness line is seen: - reject with an early-exit error - include exit code or signal - include collected stdout/stderr Suggested error shape: ```text OpenCode exited before becoming ready (exit code 1). Collected output: ... ``` ## Runtime Exit And Crash Handling Startup success is not enough. The server also needs to handle the case where OpenCode exits or crashes after readiness. The helper and the surrounding runtime supervisor should support: - detecting unexpected child exit after readiness - surfacing a clear runtime state change to the rest of the server - exposing crash/error state to clients - controlled restart behavior ### Required runtime behavior Once OpenCode is ready, the server should continue monitoring the process. If the child exits unexpectedly: - mark OpenCode as unhealthy or offline in runtime state - capture the exit code or signal - capture recent stdout/stderr for diagnostics - make that status visible through server APIs - either restart automatically or leave the process down based on the server's restart policy ### Restart policy The exact restart policy is still a design decision, but the system should be built so it can support: - no automatic restart - bounded restart attempts with backoff - explicit manual restart via server control surface The important point is that OpenCode process failure must be a first-class runtime state, not an invisible child-process problem. ## Abort Support If `signal` is provided: - attach an abort listener - kill the spawned child on abort if startup is still in progress - reject with an abort-related error The same signal can also be used later by the caller to coordinate shutdown policy if desired. ## Returned Shape Suggested return shape: ```ts type LocalOpencodeHandle = { client: ReturnType server: { url: string close: () => void proc: Bun.Subprocess } } ``` Notes: - `client` is the upstream SDK HTTP client - `server.url` is the parsed listening URL - `server.close()` should terminate the spawned process - `server.proc` is the Bun subprocess handle for diagnostics and advanced lifecycle management The process handle should also make it possible for the server's runtime supervisor to observe post-start exit/crash behavior. ## Suggested Implementation Notes Use: ```ts import { createOpencodeClient } from "@opencode-ai/sdk" ``` Preferred runtime API inside `apps/server-v2`: ```ts Bun.spawn(...) ``` Reasoning: - the new server is Bun-based - Bun is the runtime we are standardizing on for `apps/server-v2` - using Bun-native process APIs keeps the implementation aligned with the rest of the server runtime Compatibility note: - `node:child_process.spawn` would also work under Bun for many cases - but it should not be the preferred framing for this helper in the design docs - the helper should be documented as a Bun-based local runtime helper Implementation guidance: - use TypeScript - avoid `any` - collect output safely with bounded buffers if needed - make timeout cleanup and process cleanup deterministic - remove abort hooks, timers, and background readers after resolve/reject - only resolve once - continue observing the process after readiness so unexpected exit/crash can be surfaced to the runtime supervisor ## Suggested Example Usage ```ts const opencode = await createLocalOpencode({ binary: "/absolute/path/to/opencode", config: { model: "anthropic/claude-3-5-sonnet-20241022", }, }) console.log(opencode.server.url) opencode.server.close() ``` ## Tiny Usage Example ```ts import { createLocalOpencode } from "./adapters/opencode/local" const opencode = await createLocalOpencode({ binary: "/runtime/opencode", hostname: "127.0.0.1", port: 4096, }) const projects = await opencode.client.project.list() console.log(projects) opencode.server.close() ``` ## Expected Implementation Skeleton Illustrative shape only: ```ts import { createOpencodeClient } from "@opencode-ai/sdk" export async function createLocalOpencode(opts: CreateLocalOpencodeOptions = {}) { // resolve defaults, including extracted bundled binary path when present // spawn process with Bun.spawn // capture stdout/stderr // wait for readiness line // enforce timeout // reject on early exit // create client with parsed URL // return { client, server } } ``` ## Relationship To The Distribution Plan This helper is important because it connects the runtime distribution plan to the actual server implementation. In the target distribution model: - `openwork-server-v2` embeds `opencode` - extracts it to a managed runtime directory - then uses this helper to launch the extracted binary by absolute path That makes startup deterministic and independent from whatever happens to be on `PATH`. ## OpenCode Health Endpoint The server should expose an endpoint that allows clients to check OpenCode health. At minimum, clients should be able to ask: - is OpenCode running? - what version is running? - what URL is it bound to? - when did it last start or fail? Suggested shape: ```text GET /system/opencode/health ``` Example response shape: ```json { "running": true, "version": "1.2.27", "baseUrl": "http://127.0.0.1:4096", "lastStartedAt": "2026-04-13T12:00:00Z", "lastExit": null } ``` If OpenCode crashed, the server should be able to report something like: ```json { "running": false, "version": "1.2.27", "baseUrl": "http://127.0.0.1:4096", "lastStartedAt": "2026-04-13T12:00:00Z", "lastExit": { "code": 1, "signal": null, "at": "2026-04-13T12:05:00Z" } } ``` This endpoint is important because the UI should not infer OpenCode health from indirect failures alone. The server should make runtime state explicit. ## Recommendation Implement this helper locally in the new server codebase and treat it as the canonical OpenCode startup wrapper. It should be designed around the Bun-based runtime model of `apps/server-v2`, not around a generic Node-only process model. It should also be designed as one piece of a larger runtime supervision system, not just a fire-and-forget spawn helper. It should become the place where we later add: - version validation - health probes beyond stdout readiness - structured logs - cleanup/restart semantics - possibly router-adjacent startup coordination if needed