From f67bb44b7eb92057dbe951033eac12e9b2b60ba1 Mon Sep 17 00:00:00 2001 From: Andre Padez Date: Tue, 11 Aug 2026 04:21:17 +0100 Subject: [PATCH] fold the upstream source reading into the assessment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first pass was written from the running server's own OpenAPI document and live probes. This adds what the source at tag v1.18.16 says, which changes three things. The names are transitional at BOTH ends. session.next.* is the event family of the rewritten event-sourced engine, landed in 1.15.0 (PR #27415); on the v2 branch all 36 events have already dropped the .next. and some are renamed outright — agent.switched becomes agent.selected, prompted becomes prompt.promoted. Those renames are v2-branch only and the 1.x line we run still emits the old names, so the guidance is to code against them but keep one mapping table. The schema package's own AGENTS.md says the V2 suffix is going too. Upstream calls the /api surface EXPERIMENTAL in its own title — "Experimental HttpApi surface for selected instance routes", version 0.0.1 — while /session/* is what the public docs document and is not deprecated. Worth writing down plainly: the internal direction is unambiguous, the external commitment is nil, and we would be building on a surface its authors have not committed to. The SDK is generated from the exact document we probed: the build script runs opencode's own generate and feeds it to hey-api, and @opencode-ai/sdk/v2 exposes the whole /api surface, takes a directory and injects it as both the header and the location query param. That is our hand-rolled SSE reader, both envelope unwrappers, three type sets and the model-id splitting, deleted. Also corrected by reading rather than guessing: permissions v2 is a real contract change (rules, requests and the reply all change shape, and free-text replies are gone) while questions v2 is a pure re-homing with identical fields — so they are not one piece of work. And the durable cursor's replay-then-live is gap-free by construction: it re-reads the database on every wake instead of draining a buffer, with the prompt response's admittedSeq as the first cursor. Co-Authored-By: Claude Opus 5 --- docs/opencode-api-2-assessment.md | 128 +++++++++++++++++++++++++++--- 1 file changed, 116 insertions(+), 12 deletions(-) diff --git a/docs/opencode-api-2-assessment.md b/docs/opencode-api-2-assessment.md index b2d60c48..d04fe3ac 100644 --- a/docs/opencode-api-2-assessment.md +++ b/docs/opencode-api-2-assessment.md @@ -72,6 +72,26 @@ There is no version string "2.0" in the running server. `GET /doc` self-reports So "API 2.0" most likely means **the `/api/*` surface — which we already run on for turns**. Its operation ids are literally `v2.*`. It is not something to adopt; it is something to *finish*. +Two qualifications, both from the source at tag `v1.18.16`: + +- **Upstream calls it experimental.** `packages/protocol/src/api.ts` titles it `"opencode HttpApi"`, + version `"0.0.1"`, described as *"Experimental HttpApi surface for selected instance routes"*, with + every group annotated the same way. Meanwhile `/session/*` is the surface the public docs actually + document, and it is not deprecated. The internal direction is unambiguous; the external commitment is + nil. +- **`session.next` is the event family of that rewritten engine, and the name is already dead + upstream.** It arrived in **1.15.0** (PR #27415, "Add Effect-native core event system", merged + 2026-05-15) as an interim prefix. On the `v2` branch all 36 session events have dropped `.next.` — + `session.step.started`, `session.text.delta` — along with renames: `agent.switched` → + `agent.selected`, `model.switched` → `model.selected`, `prompted` → `prompt.promoted`. Those renames + are **v2-branch only**; the 1.x line we run still emits `session.next.*`. Code against + `session.next.*` today, but put the names behind one mapping table, because they are scheduled to + change wholesale. + +Same for the `v2` suffix itself. `packages/schema/AGENTS.md`: *"V1 coexistence is temporary… delete the +V1 subtree when the legacy runtime is retired"* and *"Do not preserve `V2` as the permanent name for the +replacement architecture."* Both halves of today's naming are transitional. + **OpenCode 2.0 the product is a different question**, and the answer tonight is not yet: the beta docs carry the banner *"we may wipe your data, things may break, and APIs, configuration, and plugin APIs may change"*, releases ship ~6/day, and the migration guide states three intentional breaking changes @@ -162,6 +182,21 @@ for more. Every durable event carries `{aggregateID, seq: integer, version}`, so integer. This is the documented, working answer to the gap Phase B left open and `docs/opencode-testing-checklist.md` calls the most likely thing to be broken. +The upstream implementation (`packages/core/src/event.ts`, `durable()`) makes three things explicit +that matter for building on it: + +- `after` is an **exclusive** lower bound on the durable seq, and the aggregate is the session. + Omitting it replays the session from 0. +- **Replay-then-live is gap-free by construction**: it reads `WHERE seq > after ORDER BY seq ASC`, + advances its cursor to the last row, and on every wake re-reads *the database* rather than draining a + pubsub buffer. Sequences are strictly monotonic and contiguous per session, enforced with explicit + `Sequence mismatch` / `Replay diverged` errors. +- **The first cursor is free.** `POST …/prompt` returns `{admittedSeq, id, sessionID, prompt, delivery, + timeCreated, promotedSeq?}` — measured at 22 ms — and `admittedSeq` feeds straight back as `after`. + +Note the two cursor kinds are unrelated: the session *list* uses an opaque base64url cursor +(`cursor.previous` / `cursor.next`), this one is a plain integer. + **But the two streams are not interchangeable, and the schema says why.** `SessionDurableEvent` is a `oneOf` of exactly 28 members, and the five it omits are `text.delta`, `tool.input.delta`, `reasoning.delta`, `compaction.delta` and the retry error. **Deltas are live-only by design; the @@ -192,6 +227,23 @@ GET /api/session/{id}/question POST …/question/{requestID}/r Plus `permission.v2.asked` / `question.v2.asked` events (the v1 families still exist alongside; the only `deprecated: true` operation in the entire document is `POST /session/{id}/permissions/{id}`). +The two "v2"s are not the same kind of change, which matters if we implement one of them: + +- **Permissions v2 is a real contract change.** A rule goes from `{permission, pattern, action}` to + `{action, resource, effect}`; a request from `{permission, patterns[], metadata, always[], tool?}` to + `{action, resources[], save?[], metadata?, source?}`, with the tool linkage becoming a tagged union + `source: {type:"tool", messageID, callID}`; and the reply loses its free-text `message`. The public + V2 docs say the same in config terms: *"Do not use `permission`, `bash`, or `task` in V2 + configuration."* +- **Questions v2 is a re-homing.** Field shapes are byte-identical to v1 — `questions[]` of + `{question, header, options[], multiple?, custom?}`, answers as `string[][]`. Only the namespace and + event names changed. + +Which family a 1.18.16 agent actually emits is worth measuring before building UI: the manifest the +`/api` protocol is *built* from excludes the v1 families, but the server wires the **full** manifest +(`makeApi({definitions: EventManifest.Latest.values()})`), which is why both appear in the `/api/event` +union on our own `/doc`. + An opencode agent that wants consent, or that asks a question mid-turn, gets no answer from officer. We don't subscribe to those events and have no route to reply on. Claude's harness runs `--dangerously-skip-permissions`, so this has never been modelled for either harness. Largest single @@ -271,9 +323,8 @@ with no model, against a serve with no configured default, hangs silently.** Wor comments, no `retry:`, no multi-line frames, fixed 1 s reconnect with no backoff. A cursored stream must resume at `?after=`, not restart. - **Two envelope unwrappers and three hand-written type sets** (`serve-runner.ts:161`, `client.ts:38`; - types pinned by comment to two different opencode versions, 1.17.9 and 1.18.16). `@opencode-ai/sdk/v2` - would replace most of this — a dependency decision, and note our install is frozen, so it is a - deliberate lockfile change. + types pinned by comment to two different opencode versions, 1.17.9 and 1.18.16). This is the part a + dependency would delete outright — see below. - **Stale comments in at least nine files** still describe the deleted `opencode run` subprocess path (`protocol.ts:198`, `serve-events.ts:5`, `connect-credential.ts:24`, `index.ts:137`, `websocket.ts:454`, `chat.ts:127`, `list-models.ts:75`, `sidecar-server.ts:8`, `send-opencode.ts:7`). @@ -286,6 +337,40 @@ Unrelated but found while inventorying: the settings UI writes provider keys to --- +## 7b. The SDK is generated from the document we have been reading by hand + +`@opencode-ai/sdk@1.18.16` (published 2026-08-10, versioned in lockstep with the CLI) is built by +`packages/sdk/js/script/build.ts`, which runs opencode's own `generate` to produce the OpenAPI document +and feeds it to `@hey-api/openapi-ts`. **It is generated from the same `/doc` we probed**, which is +about as good a guarantee of shape-agreement as exists. + +It ships two clients. The default export is the legacy surface. `@opencode-ai/sdk/v2` is ours: + +```ts +import { createOpencodeClient } from '@opencode-ai/sdk/v2'; + +const client = createOpencodeClient({ baseUrl }); +const admitted = await client.v2.session.prompt({ sessionID, prompt: { text }, delivery: 'steer' }); +const events = await client.v2.session.events({ sessionID, after: admitted.data.admittedSeq }); +for await (const ev of events.stream) { /* ev.type, ev.durable.seq */ } +``` + +`client.v2.session.*` covers list/create/active/get/switchAgent/switchModel/prompt/compact/wait/ +context/history/events/interrupt/message(s); there is also `client.v2.event.subscribe`, +`client.v2.permission.*`, `client.v2.question.*`, `fs`, `model`, `provider`, `agent`, `skill`, `pty`. +`createOpencodeClient` takes `directory` and injects it as both the `x-opencode-*` headers and the +`location[directory]` query param — the thing we hand-roll in two places. + +That would delete: our hand-rolled SSE reader, both envelope unwrappers, three hand-written type sets, +and the model-id string splitting. It is a dependency change, and installs here are frozen, so it is a +deliberate `bun install --no-frozen-lockfile` plus a read of the lockfile diff. Worth noting the +package's only dependency is `cross-spawn`. + +Not to be confused with two siblings the v2 docs mention: `@opencode-ai/sdk-next` is marked private and +is not on npm, and `@opencode-ai/client` is a private generation target for the beta line. + +--- + ## 8. What we'd gain immediately Ordered by value over effort. 1–3 are bug fixes, not features. @@ -326,16 +411,27 @@ Then the two that are real features needing UI: **permissions/questions** (§4d) 1. Is there a field that says which engine owns a session? Tonight's only discriminator is behavioural (legacy returns `[]`). If not, we need our own record — we already store `sessionKey → ses_…` in `opencode/state.ts` and could record the surface with it. -2. Do the `v2` permission/question events replace the v1 ones or run alongside? Both are emitted; - which one a 1.18.16 agent actually uses is unconfirmed. +2. Which permission/question family does a 1.18.16 agent actually emit? Both are declared and both + appear in our `/doc`, because the server wires the full manifest. Measure before building UI. 3. What is `/api/*`'s auth story? The spec declares no `securitySchemes` yet every route declares a `401`. The v1 docs describe HTTP Basic via `OPENCODE_SERVER_PASSWORD`; we run with none, on loopback. In the 2.0 beta this is formalised as basic auth read from `~/.local/state/opencode/service.json`. -4. Does `@opencode-ai/sdk/v2` work against 1.18.16 exactly? Generated from the same surface, but - unverified by running it. -5. When did `/api/*` first appear in the 1.x line, and when does the legacy surface actually go? Both - UNKNOWN — the published changelog names no `/api/` additions at all. +4. Does `@opencode-ai/sdk/v2` work against 1.18.16 exactly? It is generated from this exact server's + OpenAPI output and versioned in lockstep, so it should — but nobody here has run it. +5. When did `/api/*` first appear in the 1.x line? UNKNOWN; the changelog names no `/api/` additions. + The event family underneath it landed in 1.15.0. And no dated removal plan exists for `/session/*`, + `permission.asked/replied` or `question.*` — only the undated internal intent quoted in §2. +6. Will the `v2`-branch event renames reach the 1.x line, or only ship with OpenCode 2.0? No merge + found, no statement either way. This decides whether the mapping table in §2 is a one-off or a + permanent seam. +7. Is a durable `seq` stable across a server restart or a session move? It is a database column, so it + should be, but no durability guarantee is documented and we have not tested it. Item 6 in §8 depends + on the answer. + +Version state at the time of writing: **1.18.16 is the newest release** (2026-08-10) and contains +nothing API-facing. The active stream is the 2.0 beta, cutting releases continuously — the most recent +was published hours before this file was written. --- @@ -345,9 +441,17 @@ Then the two that are real features needing UI: **permissions/questions** (§4d) above against the local sidecar's serve on port 49698. - Code: `src/servers/sidecar/opencode/*`, `src/servers/api/chat/opencode/*`, `opencode-sessions.ts`, `list-models.ts`, `send-opencode.ts`. -- Upstream: opencode.ai/v2/docs/migrate-v1, opencode.ai/v2/docs, opencode.ai/docs/server, - github.com/anomalyco/opencode (formerly sst/opencode), npm `@opencode-ai/sdk` 1.18.16, - `@opencode-ai/client@next`. +- Upstream, docs: opencode.ai/v2/docs/migrate-v1, opencode.ai/v2/docs, opencode.ai/v2/docs/permissions, + opencode.ai/docs/server, opencode.ai/changelog. +- Upstream, source at tag `v1.18.16` in **github.com/anomalyco/opencode** (formerly `sst/opencode`, + which 301s): `packages/protocol/src/api.ts` and `groups/session.ts` (the surface's own "experimental" + self-description, the `after` parameter), `packages/schema/src/session-event.ts` (`DurableDefinitions` + vs `Definitions` — the delta exclusion), `packages/schema/src/{permission,question}.ts` and their + `v1/` counterparts, `packages/schema/src/session-input.ts` (`admittedSeq`), `packages/schema/AGENTS.md` + (the V1/V2 naming intent), `packages/core/src/event.ts` (replay-then-live), `packages/sdk/js/script/ + build.ts` and `src/v2/client.ts`. PRs #27415 (the engine landing in 1.15.0), #33993, #35217, #35229 + (the renames). +- npm: `@opencode-ai/sdk` 1.18.16, `@opencode-ai/client@next`. - Prior art in this repo: `docs/opencode-parity.md`, `-fork-decision.md`, `-serve-migration-plan.md`, `-serve-path.md`, `-testing-checklist.md`, `-phase0-review.md`, `-phase1-report.md`, `-phase1-review.md`.