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:
@@ -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
|
||||
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.
|
||||
**Decision: the fork is worth taking. Not started, and gated on one open question.**
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
- `/api/session/*` — a newer surface nobody here had looked at.
|
||||
I probed it, saw prompts accepted and never executed, and concluded it was an unfinished pipeline
|
||||
("`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
|
||||
OpenCode because of `stdin: 'ignore'`:
|
||||
**That model silently does not run on the new pipeline.** No error, no event, no assistant message — the
|
||||
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 |
|
||||
| ----------------------------- | ----------------------------------------------------------- | ---------------------- |
|
||||
| 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 |
|
||||
So I had one broken variable in every experiment and read the result as a property of the system.
|
||||
|
||||
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.
|
||||
## What is actually true, measured on 1.18.16 (both machines upgraded 2026-08-10)
|
||||
|
||||
## 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
|
||||
(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.
|
||||
Steer and queue are exactly the two features we hand-built for Claude and that Andre called a game
|
||||
changer. Having them as primitives, plus a resumable per-session cursor that mirrors officer's durable
|
||||
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:
|
||||
'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.
|
||||
**Why does `claude-sonnet-4-6` run under `opencode run --model` and silently die under the new API?**
|
||||
|
||||
**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`).
|
||||
Until that is answered, the migration cannot be adopted: model choice is a user-facing control, and a
|
||||
harness where picking the wrong model produces silence rather than an error is worse than one without
|
||||
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
|
||||
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.
|
||||
1. `variant` on `ModelRef` — it is the one field of the three we never set.
|
||||
2. Compare `/config/providers` against what `run` resolves, to see whether the ids differ in form.
|
||||
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**
|
||||
- **alpha: 1.17.9**
|
||||
Until then `opencode run --dir` stays, and it is verified working: `session:init` → `assistant:text` →
|
||||
`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
|
||||
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.
|
||||
## The lesson, which is the reason this file keeps its history
|
||||
|
||||
## 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` →
|
||||
`assistant:text` → `result` 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.
|
||||
- `directory` in the body of `POST /session` — accepted, echoed, ignored. Produced a confident wrong
|
||||
answer about per-request directories.
|
||||
- `location.directory` in the parity doc — a field that did not exist on that surface, which would have
|
||||
compiled and silently produced `''`.
|
||||
- `model` on `POST /api/session` — accepted, echoed back in the response, and fatal to execution.
|
||||
|
||||
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.
|
||||
**OpenCode's API accepts input it does not honour, and says nothing.** So a probe that changes one thing
|
||||
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:
|
||||
|
||||
```
|
||||
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
|
||||
- `delivery` defaults to `"steer"` when omitted.
|
||||
- New-surface responses wrap in `{"data": …}`; the legacy surface returns bare objects, so 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`.
|
||||
- The new surface is location-scoped per request: `x-opencode-directory` header, or
|
||||
`?location[directory]=` as a `deepObject` query.
|
||||
- `?after=` genuinely replays a finished session's events.
|
||||
- **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
@@ -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
|
||||
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
|
||||
newer `/api/session/*` surface offering exactly those capabilities natively (`delivery: "steer" |
|
||||
"queue"`, `/interrupt`, a resumable per-session event stream), and on the newest binary we run it accepts
|
||||
prompts and never executes them. `docs/opencode-fork-decision.md` has the probe, what was ruled out, and
|
||||
the single request that reopens the question after an upgrade. Bucket 1 below is therefore **deferred by
|
||||
evidence**, not pending a decision.
|
||||
**The fork is REOPENED and worth taking.** The serve publishes a newer `/api/session/*` surface offering
|
||||
those capabilities natively, and on 1.18.16 **`delivery: "steer"` and `delivery: "queue"` are both
|
||||
verified working** — mid-turn injection and queueing, as primitives, plus `/interrupt` and a resumable
|
||||
per-session event stream. One blocker remains: `claude-sonnet-4-6` silently does not run on that surface
|
||||
(it runs fine under `opencode run`). `docs/opencode-fork-decision.md` has the evidence, the open
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user