diff --git a/docs/opencode-fork-decision.md b/docs/opencode-fork-decision.md index 5a666aa5..58f250a6 100644 --- a/docs/opencode-fork-decision.md +++ b/docs/opencode-fork-decision.md @@ -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=` — 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.** diff --git a/docs/opencode-parity.md b/docs/opencode-parity.md index 2f2f6c82..948f396c 100644 --- a/docs/opencode-parity.md +++ b/docs/opencode-parity.md @@ -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. ---