Files
platform/docs/opencode-fork-decision.md
T
pastilhasandClaude Opus 5 a41abb4b0f characterise the fork blocker: only free models run
Not sonnet, and not variant. Swept models through the new pipeline: every -free model runs,
every paid one silently does not — haiku, sonnet and codex-mini all never start.

Ruled out: variant (sonnet advertises low/medium/high/max and echoes back an invalid
"default", which looked like the answer and was not — setting high explicitly also never
ran); credentials (zen key in auth.json plus ANTHROPIC_API_KEY); and the sidecar environment,
since the same process runs sonnet fine through opencode run.

So the new pipeline does not resolve paid-model credentials and says nothing, while run and
the legacy path authenticate fine. Upstream bug in an in-progress pipeline, not our config.

The fork stays blocked, but precisely: steer and queue are proven, and the day a paid model
runs there the migration is worth doing immediately. Re-run the sweep after each upgrade.

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

6.7 KiB

The Phase 2 fork: reopened, and why the first answer was wrong

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.

Decision: the fork is worth taking. Not started, and gated on one open question.


What actually happened

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.

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'}.

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.

So I had one broken variable in every experiment and read the result as a property of the system.

What is actually true, measured on 1.18.16 (both machines upgraded 2026-08-10)

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 YesPOST /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

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.

The blocker, now characterised: only free models run

It is not sonnet, and it is not variant. Swept four models through POST /api/session/{id}/model followed by a prompt:

Model New pipeline
longcat-2.0-free ran
ling-3.0-tiny-free ran
claude-haiku-4-5 never ran
claude-sonnet-4-6 never ran
gpt-5.1-codex-mini never ran

Every -free model runs; every paid model silently does not. Ruled out along the way:

  • Not variant. claude-sonnet-4-6 advertises ["low","medium","high","max"] and session create echoes back variant: "default", which is not among them — a promising theory that turned out to be wrong: setting variant: "high" explicitly also never ran. Tested rather than assumed, which is the whole lesson of this file.
  • Not missing credentials. opencode auth list shows an OpenCode Zen API key in ~/.local/share/opencode/auth.json plus ANTHROPIC_API_KEY in the environment.
  • Not the sidecar's environment. The same sidecar process runs claude-sonnet-4-6 correctly through opencode run --model, verified end to end. Same user, same home, same auth file.

So the new pipeline does not resolve paid-model credentials, and fails silently rather than reporting it — while the legacy path and run both authenticate fine. Note it can surface provider auth errors when it reaches that far: alpha's default (nano-gpt) returned a clean 401 missing_api_key. The silence is specific to opencode-zen paid models.

This reads as an upstream bug in an in-progress pipeline, not something configurable on our side.

Consequence for the fork: blocked, but precisely. Officer's users pick real models; a harness that works only on free tiers is not adoptable. Re-run the sweep above after each opencode upgrade — the day a paid model runs there, the migration is unblocked and worth doing immediately, because steer and queue are already proven.

Revised recommendation

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.

Until then opencode run --dir stays, and it is verified working: session:initassistant:textresult with cost, on 1.18.16, after the upgrade.

The lesson, which is the reason this file keeps its history

This project has now hit the same trap three times, each time in a different costume:

  • 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.

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.

Findings worth keeping

  • 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.
  • 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.