# The serve-based turn path: what it was, and whether to rebuild it Written 2026-08-10, before deleting `event-mapper.ts` and the unused half of `api/chat/opencode/client.ts`. `docs/opencode-parity.md` item 7 asks for exactly this: read the dead design into a note first, because Phase 2 may rebuild it. It also answers Phase 2's blocking question, which turned out to be cheap to settle. --- ## The question Phase 2 was waiting on `docs/opencode-phase0-review.md` narrowed the fork to one thing: > Moving turns onto the serve would reintroduce exactly the single-directory coupling that caused the > original pain, **unless the serve's API can take a per-request directory**. That question — not "why > was it replaced" — is now the one to answer first. **It can. Tested against the running serve (opencode 1.17.9 on this machine):** ``` POST /session?directory=/tmp/oc-phase2-probe → { directory: "/tmp/oc-phase2-probe" } ✅ matches POST /session body { directory: … } → { directory: "" } ❌ ignored ``` `directory` is a **query parameter on every `/session*` route** — create, message, abort, fork, summarize, prompt_async, shell, all of them — alongside a `workspace` param. It is not a body field, which is why a first probe that sent it in the body appeared to disprove the whole idea. **So the coupling is gone on both paths.** `opencode run --dir` anchors a subprocess turn (verified in the phase-0 review), and `?directory=` anchors a serve turn. The single-server constraint that shelved this work does not exist in either architecture any more. That removes the reason not to move. It does not by itself decide the move — see the trade below. --- ## What the dead code actually was Two files, ~200 of ~390 platform-side lines, all reachable from nothing: **`event-mapper.ts` (146 lines)** — maps OpenCode's SSE event stream to officer's `ChatEvent`s. Handles `message.part.updated` (text deltas, tool state transitions), `message.updated`, `session.idle` and `session.error`. Its shape assumes a _streaming_ source: partial text arriving as deltas, tool calls transitioning pending → running → completed as separate events. **The SSE half of `client.ts`** — `subscribe(sessionId, listener)`, one shared `GET /event` stream per base URL demultiplexed to per-session listeners, with reconnect. Plus `createSession`, `postMessage`, `abort`, `isServerHealthy`. The live half of `client.ts` stays: `listSessions`, `getSession`, `getMessages`, `deleteSession`, `renameSession`, all used by `opencode-sessions.ts` for the chat list and transcript reads. ### Why this matters for a rebuild The dead mapper is **not** a sketch to be dusted off — it is a finished, working shape for a design that was measured against a real event stream. Two things in it are worth keeping if the serve path returns: 1. **The delta model.** `runner.ts` emits whole `text` blocks because `opencode run --format json` emits whole blocks; the mapper emits deltas because SSE emits deltas. Token streaming (parity Phase 3, item 11) is not new work on the serve path — it is this file. 2. **Tool-state transitions.** The mapper tracks a tool call across pending/running/completed. The subprocess path only ever sees the finished call. Both are recoverable from git after deletion (`5d077a4` is the last commit where the serve path was live), which is the argument for deleting rather than keeping it compiled-but-unreachable: an unused file rots silently against a moving API, and this one is already pinned to a version two minor releases behind what some machines run. --- ## The trade, now that the blocker is gone **Keep the subprocess (`opencode run --dir`)** - No rewrite; it works today. - Permanently forfeits: token streaming, mid-turn injection, background tasks, live-session enumeration, reattach-by-id, interrupt-without-teardown. Every one of those is a `stdin: 'ignore'` consequence. - One process per turn, no warm state to leak or garbage-collect. **Move turns onto the serve (`POST /session/{id}/message?directory=…`)** - Unblocks the whole of parity Phase 3 at once — those six capabilities are all downstream of a persistent, addressable session. - Re-adopts an SSE stream officer must keep alive, demultiplex and reconnect. That machinery already exists in the deleted code, so the cost is smaller than it looks. - Introduces warm sessions and therefore a lifetime question OpenCode currently does not have: idle GC, orphan adoption after a restart, the same problems the Claude path spent months getting right. - The serve becomes load-bearing rather than a convenience. Today a serve crash costs session listing; then it would cost every turn in flight. **A third option, not in the parity doc:** move only what needs the serve. Keep `opencode run` for turns and add `?directory=`-scoped serve calls for enumeration and reattach. That buys the Live panel and reattach-by-id without warm sessions or an SSE loop. It does not buy streaming or mid-turn injection, which are the two most visible gaps. ## Recommendation **Do not start the migration on this pass.** The blocker is cleared and that is the deliverable here; choosing between the three is a product call about how much OpenCode should behave like Claude, and it should be made deliberately rather than as a side effect of a cleanup branch. If it is taken: the third option first. It is incremental, it is the only one with no lifetime questions, and it makes the Live panel — which today silently omits every running OpenCode turn — tell the truth. --- ## Verified facts this note rests on | Claim | How | | ---------------------------------------------------------------------------------- | --------------------------------------------------------- | | `?directory=` on `POST /session` is honoured | Created a session, read `directory` back: matched | | A body `directory` is ignored | Same call with the field in the body: got the serve's cwd | | `directory` is on every `/session*` route | Read the serve's own `/doc` (OpenAPI) | | `event-mapper.ts` has no importers outside `client.ts` | grep | | `createSession`/`postMessage`/`abort`/`isServerHealthy` have no callers | grep for call sites | | `listSessions`/`getSession`/`getMessages`/`deleteSession`/`renameSession` are live | all from `opencode-sessions.ts` | Machine note: this server runs opencode **1.17.9**; the phase-0 review's tests ran against **1.18.11** elsewhere. The `?directory=` result above is from 1.17.9, so it holds on the older of the two.