# OpenCode — Deep Architecture Analysis (Phase 0 Research) **Reference:** `references/opencode` — snapshot at commit `0bff28de09105088ff5bdefab91413d55c28dff1` (2026-08-09), repo `github.com/anomalyco/opencode`, version `1.18.x`. **Method:** real code-path tracing (imports, agent loop, tool registry, render tree, permission checks, persistence), not README reading. All paths are relative to the OpenCode repo root. Key symbols are cited as evidence. **Important context:** this snapshot is the *post-rewrite* OpenCode. The old Go/bubbletea TUI is gone; the entire product is now **TypeScript on Bun**, with a SolidJS-driven terminal renderer (`@opentui/*`). The codebase also visibly contains **two coexisting generations**: the live "v1" runtime (`packages/opencode/src/**`, schemas in `packages/core/src/v1/**`) and an in-progress event-sourced "v2" layer (`packages/core/src/{session,agent,permission,config,snapshot,tool}`). Both write to the same SQLite database. This mid-migration state is itself an architectural lesson (see "What OpenCode does poorly"). --- ## 1. Monorepo and technology stack - Bun workspace monorepo (`package.json` at root, `packageManager: bun@1.3.14`, turbo for typecheck, oxlint). ~30 packages under `packages/`: the ones that matter for a terminal agent are: - `packages/opencode` — the CLI + engine (agent loop, tools, sessions, server routes). - `packages/core` — shared services: database, event system, filesystem, ripgrep, permission/agent/config schemas (v1 + v2), snapshots, shell, PTY. - `packages/tui` — the terminal UI (SolidJS + `@opentui/solid`). - `packages/sdk` — `openapi.json` + generated JS client (`packages/sdk/js/src/gen/{client.gen.ts,sdk.gen.ts,types.gen.ts}`). - `packages/schema` — Effect Schema definitions shared by everything (`session.ts` v1, `session-message.ts` v2, `permission`, `revert`, identifiers). - Plus web/app/desktop/console/enterprise packages that are irrelevant to the terminal product but bloat the repo. - Core libraries: **Effect 4** (services, layers, streams, structured concurrency) everywhere in the engine; **SolidJS 1.9** + **`@opentui/core|solid|keymap` 0.4.5** for the TUI; **`ai` SDK 6** for provider streaming; **drizzle-orm + SQLite** for persistence; **web-tree-sitter** (bash/powershell WASM grammars) for command parsing; **fuzzysort** for fuzzy matching; **ripgrep** (bundled binary service, `packages/core/src/ripgrep`). - The engine is written in "Effect service" style: every subsystem is a `Context.Service` with a `Layer` and explicit dependency list (e.g. `SessionProcessor.node = LayerNode.make({ service, layer, deps: [Session.node, Config.node, Snapshot.node, ...] })` in `packages/opencode/src/session/processor.ts:699`). This gives explicit wiring and testability at the cost of a steep idiom (generators, `Effect.gen`, `Deferred`, `PubSub` everywhere). --- ## 2. Client/server architecture ### 2.1 Process model — one process, virtual HTTP The headline finding: **OpenCode is architected as client/server but usually runs as a single process.** - The server is an **Effect HttpApi** app (not raw hono in this generation): `packages/opencode/src/server/server.ts` builds `HttpApiApp.webHandler()` and exposes both `fetch(request)` and an **in-memory `request(input, init)`** entry (`Server.Default`, `server.ts:57-66`). `openapi()` derives the public OpenAPI document from `PublicApi`. - The default `opencode` command (`packages/opencode/src/cli/cmd/tui.ts`, `TuiThreadCommand`) does **not** open a socket. It spawns the engine in a **Bun Worker** (`packages/opencode/src/cli/tui/worker.ts`) and gives the TUI: - `createWorkerFetch(client)` — a `fetch` implementation that serializes `{url, method, headers, body}` over an RPC channel; the worker replays it against `Server.Default().app.fetch(request)` (`worker.ts:31-49`). The base URL is a placeholder (`http://opencode.internal`). - `createEventSource(client)` — subscribes to `"global.event"` RPC events instead of SSE (`cli/cmd/tui.ts:42-49`); the worker forwards every `GlobalBus` event over RPC (`worker.ts:24-26`). - The same TUI can run against a **real** HTTP server: `packages/cli/src/tui.ts` (`runTui({url, headers})`) uses plain HTTP + SSE with exponential backoff. `opencode serve` / `opencode attach` / `opencode web` (`packages/opencode/src/cli/cmd/{serve,attach,web}.ts`) expose the same API over the network; `server.ts` supports mDNS advertisement (`MDNS`) and CORS. **Consequence:** the client is written 100% against the SDK/API surface, but the common path pays no network tax and needs no daemon lifecycle management. UI ↔ engine isolation also means a render-thread stall never blocks tool execution and vice versa. ### 2.2 API surface and SDK - Route groups: `packages/opencode/src/server/routes/instance/httpapi/groups/{session,permission,question,provider,config,file,event,project,workspace,pty,mcp,tui,global,...}.ts`. - The JS SDK is **generated from `packages/sdk/openapi.json` with `@hey-api/openapi-ts`** (`packages/sdk/js/script/build.ts`; ~162 paths / 188 operations into `client.gen.ts` / `sdk.gen.ts` / `types.gen.ts`), so client and server cannot drift silently. The TUI consumes it via `createOpencodeClient({baseUrl, fetch, directory, headers})` (`packages/tui/src/context/sdk.tsx`). A `packages/sdk-next` variant embeds the engine in-process for SDK consumers. - Events reach clients through SSE handlers — per-instance `/event` and server-wide `/global/event` (`server/routes/instance/httpapi/handlers/{event,global}.ts`): `text/event-stream`, listener registered into an unbounded queue *before* the stream body starts ("Listener registration is eager, so events published after this point cannot be lost"), filtered by instance directory/workspace, prefixed with `server.connected`, merged with a 10s `server.heartbeat`. Response gzip middleware explicitly bypasses SSE paths. - Multiple clients (TUI + web + IDE + ACP) can attach to one engine; the server is **multi-tenant across project directories via an `x-opencode-directory` header**, with per-directory engine state held in a `ScopedCache` keyed on `ctx.directory` (`packages/opencode/src/effect/instance-state.ts`) and instance boot deduplicated through `Deferred`s (`packages/opencode/src/project/instance-store.ts`). The share feature and control-plane routes build on the same event stream. ### 2.3 Event system — the engine's spine Three layers (all evidence in `packages/opencode/src/bus/global.ts`, `packages/core/src/event.ts`, `packages/opencode/src/event-v2-bridge.ts`): 1. **`GlobalBus`** — a plain in-memory `EventEmitter` for process-level fan-out to SSE/RPC clients. 2. **`EventV2`** — typed pub/sub on Effect `PubSub`, with a critical property: event definitions can be **durable** (`options: {durable: {aggregate: "sessionID", version: 1}}` in `packages/schema/src/v1/session.ts`). Publishing a durable event transactionally appends it to the `event` table (per-aggregate `seq` from `event_sequence`) **and runs registered projectors in the same transaction** ("Local operational projection committed atomically with a new durable event"). 3. **`EventV2Bridge`** — attaches location info (directory/workspace/project) to every publish and re-emits onto `GlobalBus`, including a second `sync` envelope for durable events (replication/share). **Persistence is a projection of events.** `packages/core/src/session/projector.ts` maps `SessionV1.Event.{Created,Updated,MessageUpdated,PartUpdated,...}` onto the `session` / `message` / `part` tables; session cost and token counters are maintained *incrementally* (`sql\`${col} + ${delta}\`` applied from `step-finish` parts, with negative deltas on part replacement/removal). `packages/opencode/src/session/session.ts` `updateMessage`/`updatePart` never touch the DB directly — they only publish events. The UI, the DB, and remote clients are all consumers of the same stream. This is the single most elegant structural decision in the codebase. --- ## 3. TUI framework ### 3.1 Stack and rendering model - **SolidJS 1.9.10 (patched) + `@opentui/core` + `@opentui/solid` + `@opentui/keymap`** — opentui is the team's own terminal UI engine: a **retained-mode renderable tree with Yoga flexbox layout painted into an optimized cell buffer at a target FPS**, driven declaratively through Solid's fine-grained reactivity. JSX intrinsics are terminal renderables: ``, ``, ``, ``, `