# 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<string, unknown>
  client?: Record<string, unknown>
}
```

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=<hostname>
--port=<port>
```

If `config.logLevel` exists, append:

```text
--log-level=<config.logLevel>
```

Environment should merge `process.env` with:

```text
OPENCODE_CONFIG_CONTENT=<json string>
```

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 <binary>
```

### 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<typeof createOpencodeClient>
  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
