Files
platform/docs/opencode-fork-decision.md
T
pastilhasandClaude Opus 5 41663bc207 decide the phase 2 fork: turns stay on opencode run
The serve has a second, newer API surface nobody here had looked at, and it publishes
exactly what the parity doc calls impossible under stdin ignore: delivery steer and queue
on POST /prompt, an interrupt that does not tear down, and a per-session event stream with
an after cursor — the durable-replay machinery officer hand-built for claude, as a
primitive. That would have made migrating obvious.

It does not execute. A prompt is accepted with an admittedSeq, stored, emits
prompt.admitted and prompted, and then never steps. Ruled out separately: the model, the
permissions (build is *:allow, no pending requests), the per-request location (the surface
is location-scoped via header or a deepObject query, supplied everywhere, no change), and a
config gate. The legacy POST /session/id/message?directory= generates fine in 17s, so the
serve itself works — only the new pipeline is inert. session.next.* is the tell.

And not a version problem, which is the part everything here had backwards: this Mac runs
1.18.11 and alpha runs 1.17.9, measured. The dead pipeline was tested on the NEWER binary.
The original "this server runs 1.17.9" meant alpha and was copied to a machine where it was
false; corrected in runner.ts and the test.

So building against it now would produce code that looks finished and does nothing, which
is the failure mode this project keeps rediscovering. One request reopens the question
after any upgrade, and the doc names it.

Also de-flakes the lifecycle tests: they spawn real processes, and a fixed sleep(750) went
red once on a machine busy running these probes. Presence assertions poll now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 14:15:34 +01:00

6.3 KiB

The Phase 2 fork: decided, and the probe behind it

Decision: turns stay on opencode run --dir. Do not migrate to the serve.

Taken 2026-08-10 by the agent now owning this work, after probing the serve rather than reasoning about it. docs/opencode-serve-path.md framed the three options and deferred the choice as a product call. It is no longer a product call — the option that would have justified migrating does not currently run.


What changed the analysis

The serve exposes two API surfaces, and every prior document was written against the older one:

  • /session/* — what the deleted client used. ?directory= per request.
  • /api/session/* — a newer surface nobody here had looked at.

The new one publishes, in /doc, exactly the capabilities the parity doc lists as impossible for OpenCode because of stdin: 'ignore':

Capability New API Parity doc said
Mid-turn injection POST /prompt with delivery: "steer" No — needs fork
Queue behind a turn same, delivery: "queue" No — needs fork
Interrupt without teardown POST /interrupt → 204 No — needs fork
Token streaming GET /eventtext/event-stream No — needs fork
Reconnect + replay same, ?after=<seq> — a resumable per-session cursor not considered
Compaction seams POST /compact "no signal exists"
Images prompt.files Phase 4
Interactive approvals /permission, /question + reply/reject not considered

A per-session stream with a cursor is the striking one: it is the durable-replay machinery officer had to hand-build for Claude, offered as a primitive. That would have made the migration look obvious.

Why the answer is still no

It does not execute. On the newest binary we run, a prompt to /api/session/{id}/prompt is accepted (200, with an admittedSeq), stored as a user message, emits session.next.prompt.admitted and session.next.prompted — and then nothing. No step.started, no assistant reply, indefinitely.

Ruled out, each by a separate probe:

  • Not the model. Reproduced with no model, and with an explicit {providerID: 'opencode', id: 'claude-sonnet-4-6'} accepted and echoed back by session create.
  • Not permissions. GET /permission and GET /question both return {"data":[]}, and the build agent's own config is *: allow.
  • Not the missing location. The new surface is location-scoped per request — x-opencode-directory header, or ?location[directory]= as a deepObject query (/doc confirms location is deepObject). Supplying it on every call, including the event stream, changes nothing.
  • Not a config gate. /config has no experimental key; experimental is null.
  • Not the version. See below.

The serve itself is fine. The legacy path generates normally: POST /session/{id}/message?directory= returned a complete assistant reply in 17s with cost and tokens, and honoured the directory (cwd: /private/tmp/oc-serve-probe).

So the split is precise: the serve can run turns, but only through the old endpoint, which has neither steer nor queue. The session.next.* event naming is the tell — a next-generation pipeline that accepts input and is not yet wired to a runner.

The version, which everything in this repo had backwards

runner.ts and the phase-0/phase-1 documents state 1.17.9 "here" and 1.18.11 "elsewhere". Measured:

  • This Mac: 1.18.11
  • alpha: 1.17.9

The other agent's "this server" meant alpha, and the comment was copied without re-measuring. It matters for exactly one conclusion: the dead pipeline was tested on the newer binary, so this is not "we are behind, upgrade and it works". We are ahead, and it still does not run.

What this means for each option

  • Keep the subprocess — chosen. It works; verified end to end today (session:initassistant:textresult with cost). Forfeits streaming, injection, background tasks, reattach.
  • Migrate to the serve's legacy path — would buy token streaming and reattach, at the cost of the SSE demux/reconnect machinery and warm-session lifetime questions (idle GC, orphan adoption — the problems the Claude path spent months getting right). It would not buy steer or queue, which are the two most visible gaps. Poor trade.
  • Migrate to the serve's new path — buys everything, and cannot be built against today.

Building against a preview that accepts input and never runs it is the worst of the three: the code would look finished and do nothing, which is the failure mode this project keeps rediscovering.

The trigger to reopen this

One command decides it, and it needs no design work:

POST /api/session/{id}/prompt   →   does `session.next.step.started` ever arrive?

When that produces a real turn on the installed binary, the migration becomes clearly worth doing and should target the new surface directly — not the legacy one — because steer, queue, interrupt and a resumable per-session cursor map one-to-one onto what officer already does for Claude by hand.

Re-run POST /api/session + POST /prompt after any opencode upgrade. Until then this is settled, and docs/opencode-parity.md's bucket 1 should be read as "deferred by evidence", not "pending a decision".

Findings worth keeping regardless

  • delivery defaults to "steer" when omitted — the admitted event says so.
  • New-surface responses are wrapped in {"data": …}; the legacy surface returns bare objects. Reading body.id instead of body.data.id silently yields undefined.
  • ?after= on the event stream really does replay history — verified by replaying a finished session's events after the fact.
  • The serve is unauthenticated locally (OPENCODE_SERVER_PASSWORD unset), which is how all of the above was probed with plain curl.