reverse the fork decision: the pipeline works, my probe did not

I concluded a few commits ago that the serve new /api/session pipeline accepts prompts and
never executes them, and kept turns on opencode run. Wrong. Every probe behind that passed
an explicit model claude-sonnet-4-6, and THAT model silently does not run on the new
surface — no error, no event, no assistant message. Drop the field and the same request
completes.

One broken variable in every experiment, read as a property of the system.

Measured on 1.18.16, both machines upgraded today: delivery steer injects into a running
turn (verified, output changed to order), delivery queue runs after it (verified, ONE then
TWO, zero errors), and model selection works via POST /model — just not with sonnet.

So the fork is reopened and worth taking, targeting the new surface rather than the legacy
message path, which generates fine but has neither steer nor queue. Blocked only on why
sonnet dies there while working under opencode run.

Third time this project has hit the same trap: opencode accepts input it does not honour
and says nothing — directory in the body, location.directory that never existed, now model.
A probe that changes one thing and sees nothing has not learned the feature is missing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-10 14:42:08 +01:00
co-authored by Claude Opus 5
parent 0c90216c7a
commit 539aeca7ec
2 changed files with 76 additions and 88 deletions
+66 -82
View File
@@ -1,108 +1,92 @@
# The Phase 2 fork: decided, and the probe behind it # The Phase 2 fork: reopened, and why the first answer was wrong
**Decision: turns stay on `opencode run --dir`. Do not migrate to the serve.** > **CORRECTION, same day.** An earlier version of this file concluded "the new pipeline does not
> execute, keep the subprocess". **That was wrong, and wrong for an embarrassing reason: my probe.**
> The conclusion is reversed below. The mistake is written up rather than deleted, because the shape of
> it is the useful part.
Taken 2026-08-10 by the agent now owning this work, after probing the serve rather than reasoning about **Decision: the fork is worth taking. Not started, and gated on one open question.**
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 ## What actually happened
The serve exposes **two** API surfaces, and every prior document was written against the older one: The serve exposes a newer `/api/session/*` surface, alongside the `/session/*` one every prior document
was written against. It offers, natively, what the parity doc lists as impossible under `stdin: 'ignore'`:
`delivery: "steer" | "queue"` on `POST /prompt`, `/interrupt`, a per-session `text/event-stream` with an
`?after=` cursor, `/compact`, `prompt.files`, and `/permission` + `/question`.
- `/session/*` — what the deleted client used. `?directory=` per request. I probed it, saw prompts accepted and never executed, and concluded it was an unfinished pipeline
- `/api/session/*` — a newer surface nobody here had looked at. ("`session.next.*` is the tell"). Every one of those probes passed an explicit
`model: {providerID: 'opencode', id: 'claude-sonnet-4-6'}`.
The new one publishes, in `/doc`, exactly the capabilities the parity doc lists as impossible for **That model silently does not run on the new pipeline.** No error, no event, no assistant message — the
OpenCode because of `stdin: 'ignore'`: prompt is admitted, stored, `prompt.admitted` and `prompted` fire, and nothing else ever happens. Drop
the model field and the identical request completes normally.
| Capability | New API | Parity doc said | So I had one broken variable in every experiment and read the result as a property of the system.
| ----------------------------- | ----------------------------------------------------------- | ---------------------- |
| 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 /event``text/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 ## What is actually true, measured on 1.18.16 (both machines upgraded 2026-08-10)
to hand-build for Claude, offered as a primitive. That would have made the migration look obvious.
## Why the answer is still no | Claim | Verdict |
| ----------------------------------------- | -------------------------------------------------------------------- |
| The new pipeline executes turns | **Yes** — replies normally when no model is forced |
| `delivery: "steer"` injects mid-turn | **Yes, verified** — steered a running turn, output changed to order |
| `delivery: "queue"` runs after | **Yes, verified** — two replies, "ONE" then "TWO", zero errors |
| Model selection works at all | **Yes**`POST /api/session/{id}/model` → 204, then runs on it |
| `claude-sonnet-4-6` works there | **No** — silent no-op, at create *and* via the model route |
| `claude-sonnet-4-6` works via `run --dir` | **Yes** — verified end to end the same day |
**It does not execute.** On the newest binary we run, a prompt to `/api/session/{id}/prompt` is accepted Steer and queue are exactly the two features we hand-built for Claude and that Andre called a game
(200, with an `admittedSeq`), stored as a user message, emits `session.next.prompt.admitted` and changer. Having them as primitives, plus a resumable per-session cursor that mirrors officer's durable
`session.next.prompted` — and then nothing. No `step.started`, no assistant reply, indefinitely. replay, is a strong argument for migrating.
Ruled out, each by a separate probe: ## The open question, which is now the only blocker
- **Not the model.** Reproduced with no model, and with an explicit `{providerID: 'opencode', id: **Why does `claude-sonnet-4-6` run under `opencode run --model` and silently die under the new API?**
'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=` Until that is answered, the migration cannot be adopted: model choice is a user-facing control, and a
returned a complete assistant reply in 17s with cost and tokens, and honoured the directory harness where picking the wrong model produces silence rather than an error is worse than one without
(`cwd: /private/tmp/oc-serve-probe`). streaming. Things to try, cheapest first:
So the split is precise: **the serve can run turns, but only through the old endpoint, which has neither 1. `variant` on `ModelRef` — it is the one field of the three we never set.
steer nor queue.** The `session.next.*` event naming is the tell — a next-generation pipeline that 2. Compare `/config/providers` against what `run` resolves, to see whether the ids differ in form.
accepts input and is not yet wired to a runner. 3. Whether it is auth-scoped: alpha's default model failed `401 missing_api_key` on a *different*
provider (`nano-gpt`), which proves the new pipeline surfaces provider auth errors properly when it
gets that far — so sonnet's silence is not a generic auth failure.
### The version, which everything in this repo had backwards ## Revised recommendation
`runner.ts` and the phase-0/phase-1 documents state 1.17.9 "here" and 1.18.11 "elsewhere". Measured: **Take the fork, targeting the new surface, once the model question is answered.** Not the legacy
`/session/{id}/message` path — that generates fine but has neither steer nor queue, so it buys streaming
at the cost of SSE demux and warm-session lifetime for the two least interesting gaps.
- **This Mac: 1.18.11** Until then `opencode run --dir` stays, and it is verified working: `session:init``assistant:text`
- **alpha: 1.17.9** `result` with cost, on 1.18.16, after the upgrade.
The other agent's "this server" meant alpha, and the comment was copied without re-measuring. It matters ## The lesson, which is the reason this file keeps its history
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 This project has now hit the same trap three times, each time in a different costume:
- **Keep the subprocess** — chosen. It works; verified end to end today (`session:init` → - `directory` in the body of `POST /session` — accepted, echoed, ignored. Produced a confident wrong
`assistant:text` → `result` with cost). Forfeits streaming, injection, background tasks, reattach. answer about per-request directories.
- **Migrate to the serve's legacy path** — would buy token streaming and reattach, at the cost of the - `location.directory` in the parity doc — a field that did not exist on that surface, which would have
SSE demux/reconnect machinery and warm-session lifetime questions (idle GC, orphan adoption — the compiled and silently produced `''`.
problems the Claude path spent months getting right). It would **not** buy steer or queue, which are - `model` on `POST /api/session` — accepted, echoed back in the response, and fatal to execution.
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 **OpenCode's API accepts input it does not honour, and says nothing.** So a probe that changes one thing
would look finished and do nothing, which is the failure mode this project keeps rediscovering. and sees nothing happen has not learned that the feature is missing; it has learned that *something* is
wrong, and the next step is to remove variables, not to conclude. The corrected probes here each changed
exactly one field.
## The trigger to reopen this ## Findings worth keeping
One command decides it, and it needs no design work: - `delivery` defaults to `"steer"` when omitted.
- New-surface responses wrap in `{"data": …}`; the legacy surface returns bare objects, so reading
```
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`. `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 - The new surface is location-scoped per request: `x-opencode-directory` header, or
events after the fact. `?location[directory]=` as a `deepObject` query.
- The serve is unauthenticated locally (`OPENCODE_SERVER_PASSWORD` unset), which is how all of the above - `?after=` genuinely replays a finished session's events.
was probed with plain `curl`. - **opencode versions: both machines on 1.18.16** as of 2026-08-10 (Mac was 1.18.11, alpha 1.17.9).
Several comments recorded these backwards; corrected. Policy from Andre: **write against the latest
version regardless of what alpha happens to run.**
+10 -6
View File
@@ -91,12 +91,16 @@ is gone. What remains is bucket 1 — capabilities Claude has and OpenCode does
visible ones (token streaming, mid-turn injection, background tasks, interrupt-without-teardown) are visible ones (token streaming, mid-turn injection, background tasks, interrupt-without-teardown) are
downstream of `stdin: 'ignore'` and therefore of the Phase 2 fork. downstream of `stdin: 'ignore'` and therefore of the Phase 2 fork.
**The fork is decided: turns stay on `opencode run --dir`.** Not as a preference — the serve publishes a **The fork is REOPENED and worth taking.** The serve publishes a newer `/api/session/*` surface offering
newer `/api/session/*` surface offering exactly those capabilities natively (`delivery: "steer" | those capabilities natively, and on 1.18.16 **`delivery: "steer"` and `delivery: "queue"` are both
"queue"`, `/interrupt`, a resumable per-session event stream), and on the newest binary we run it accepts verified working** — mid-turn injection and queueing, as primitives, plus `/interrupt` and a resumable
prompts and never executes them. `docs/opencode-fork-decision.md` has the probe, what was ruled out, and per-session event stream. One blocker remains: `claude-sonnet-4-6` silently does not run on that surface
the single request that reopens the question after an upgrade. Bucket 1 below is therefore **deferred by (it runs fine under `opencode run`). `docs/opencode-fork-decision.md` has the evidence, the open
evidence**, not pending a decision. question, and a correction — an earlier version of that file concluded the opposite because every probe
passed that one model.
Until the model question is answered, turns stay on `opencode run --dir`, which is verified working on
1.18.16.
--- ---