Skip to content

Support opencode 2.x servers alongside 1.x (per-connection API detection) - #51

Merged
kjeldahl merged 8 commits into
mainfrom
forge/v2-adapter
Nov 12, 2026
Merged

kjeldahl merged 8 commits into
mainfrom
forge/v2-adapter

Conversation

@kjeldahl

Copy link
Copy Markdown
Owner

What

Lets the app talk to opencode 2.x servers as well as 1.x, transparently. Each connection probes GET /api/info and routes every call to whichever implementation answered, so the 1.x path is unchanged and no screen or store needs to know about versions.

How it works

  • Detection (src/lib/server-version.ts): /api/info answered as JSON with a 2.x version → 2.x; anything else → 1.x. Decided by content type, never status code — both majors serve their web app with 200 for routes they don't implement. A workspace that is only waking gets no cached verdict, and the SSE reconnect re-probes so an in-place server upgrade is picked up without restarting.
  • Adapter (src/lib/sdk-v2.ts): maps /api/*, {data} envelopes, location[directory] scoping, cursor-paginated newest-first transcripts and the granular session.* event stream onto the 1.x shapes the stores already consume. Tool names are renamed (shell→bash, subagent→task, patch→apply_patch) and part IDs agree between the stream and a GET, so a refresh never duplicates a part.
  • Pending revert: 2.x commits a staged revert itself when the re-sent prompt lands, and says so only in the stream. Mapped, so the "resend to confirm" banner clears and the re-sent message is not hidden by the transcript's revert filter.

Found while building it

  • GET /message paging combined order with cursor, which 2.x rejects — any transcript spanning more than one page failed to load.
  • A sleeping forge workspace made Test connection show a diagnostic report that said the connection works. It's now treated as a state (retry inside the same budget), not a failure.
  • Both majors answer /global/health with 200 + HTML, so 200 OK is not evidence of a working API; diagnostics probe the API route for the detected major and classify an HTML answer as a failure.

Verification

  • 460 unit/integration tests, including a 2.x mock that 400s on order+cursor like the real server.
  • Real 2.0.29 server: detection, session CRUD, paging, streaming deltas, tool parts, permission round trip (incl. follow-up asks), question round trip, interrupt, and the edit-message flow.
  • Real 1.18.32 server: probes as 1.x, existing flows unchanged.
  • Live scripts kept under scripts/v2-live-*.ts.

Not covered

  • The app UI itself against a 2.x connection (needs a device; CI has no emulator).
  • ~ expansion in the directory picker on 2.x (no server home is exposed).
  • CUA smoke test has not been run — there is no emulator in this environment. Per AGENTS.md it should run before merge.

…dapter

Adds GET /api/info-based server detection (JSON {version:2.x} = v2, HTML = 1.x)
cached per server and re-probed on SSE reconnect, plus src/lib/sdk-v2.ts which
maps the 2.x API onto the existing Client surface: /api/* routes, {data}
envelopes, location[directory] scoping, paginated newest-first message lists,
session/revert/interrupt, permission and form (question) round trips, and the
granular session.* event stream accumulated back into message.part.updated
parts. Screens and stores are unchanged.
…mples

Adds tests/fixtures/mock-opencode-server-v2.ts (a 2.x-protocol stub behind
--v2: /api/* + {data} + location[directory] + desc/cursor paging + granular
session.* events, plus scripted permission/form asks) and sdk-v2.test.ts:
message mapping straight from sample-messages.json, event mapping replaying
sample-events.sse, part-id agreement between streaming and refetch, detection
against JSON/HTML/404 servers, and full permission and question round trips.

Also records real 2.0.29 permission and form events under
docs/opencode-v2-port/sample-{permission,form}-events.sse.
scripts/v2-live-{detect,check,permission,question,interrupt}.ts drive the
adapter against a real 2.0.29 server (both majors for the probe), covering
detection, session list/create, paging, streaming deltas, tool parts, the
permission and question round trips, and interrupt.
…without order+cursor

Editing a sent message left the transcript frozen on a 2.x server: the app
hides everything after the pending revert point, but 2.x commits that revert
itself when the re-sent prompt lands and never tells the app over an API call
— only in the stream (session.revert.staged/committed/cleared). Mapping those
onto a session.revert event clears the banner and lifts the filter, and the
commit's first dropped id prunes the pre-edit exchange the server removed.

Also fixes GET /message paging: a 2.x cursor must not be combined with order
(InvalidCursorError), which made any transcript that spanned more than one page
fail to load.
@kjeldahl
kjeldahl merged commit caec0d9 into main Nov 12, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant