CodeNomad/dev-docs/OPENCODE_COMPATIBILITY.md
Pascal André ca514ed2ea
chore(opencode): qualify and recommend runtime 2.2.1 (#1188)
## Summary

- Qualify OpenCode **2.2.1** (latest stable, 29 October) with the
existing locked **2.1.0 client/plugin** dependencies, and **recommend**
it.
- Keep the technical **minimum at 2.1.2**: no consumed contract changed,
so the minimum has no new reason to move.
- 2.1.2, 2.1.4 and 2.2.0 daemons stay ready and get an optional upgrade
offer. 2.2.1 is the `tested` version. Newer and custom labels keep
authenticated contract recognition.
- The reviewed-contract fast path lists the 2.1.2 minimum and the 2.2.1
target.
- The recommended version is updated in the help (all ten locales), both
READMEs, `CONTRIBUTING.md`, `dev-docs/OPENCODE_COMPATIBILITY.md` and the
architecture skill.
## Contract review (v2.1.4 → v2.2.0 → v2.2.1; 661 + 134 commits)

Reviewed from the exact tags `v2.1.4` (`68bc26b7da`), `v2.2.0`
(`1097e2668b`) and `v2.2.1` (`3ebcf9a32c`), the published npm packages,
and an authenticated OpenAPI dump from each official CLI running in its
own sandbox. The [changelog](https://opencode.ai/changelog) covers
2.2.0. It had no 2.2.1 entry at review time (29 October), so 2.2.1 was
reviewed from its commit log.

- **Authenticated OpenAPI:** 124 → 130 → **134 paths**, 149 → 156 →
**162 operations**. Each dump matches the tagged
`packages/protocol/openapi.json`.
- **2.2.0 adds** `GET /api/plugin/gui`; `POST
/api/experimental/generate/{text,image,decisions}`; `POST`/`DELETE
/api/experimental/fs/watch`; `GET /api/experimental/media/{hash}`; and
`GET /api/experimental/usage/limits`.
- **2.2.0 removes** `POST
/api/experimental/voice/session/{sessionID}/instructions`.
- **2.2.1 adds** six `/api/experimental/schedule` operations (list,
create, get, delete, pause, resume), gated by `experimental.schedules`.
  - **Changed operations:**
- `GET /api/model` gains an optional `types` filter. Without it the
route lists chat models only. Image and decision models are new in
2.2.0, so the default listing matches 2.1.4.
    - `GET /api/event` gains an optional `types` filter.
    - `GET /api/vcs/diff` gains an optional `untracked` parameter.
- `POST /api/experimental/generate` becomes the deprecated alias of
`/generate/text`.
- Nine session, project and saved-permission routes drop
`LocationNotFoundError` from their 404 responses: they no longer boot a
Location ([#58997](https://github.com/anomalyco/opencode/pull/58997)).
- `GET /api/agent` can now answer 503 while plugins settle
([#59082](https://github.com/anomalyco/opencode/pull/59082)).
- In 2.2.1, the session log and persistent-PTY operation IDs move under
`experimental.*`. Their paths are unchanged.
- **Schemas:** all 103 consumed-family schemas are byte-identical from
2.1.4 to 2.2.1. These are `V2EventEncoded`, `Session.*` (messages,
parts, inbox, info), `Form.*`, `Permission.*`, `Prompt*`, `Event.*`,
`Location.*` and PTY/Shell. The session prompt, environment, message,
form/permission reply and RPC operations are unchanged too. The only
`Config.InfoEncoded` changes are additive opt-ins:
`experimental.media_blob`, `experimental.image_generate`,
`experimental.schedules` and `timezone`. Model `type`, prompt-cache
`cache` settings, `FileDiff.Info.more`, `Mcp.Server.title` and
`ServerInfo.hostname` are additive fields.
- **Admission:** every predicate CodeNomad's
`compatibility/negotiate.ts` checks holds on 2.2.0 and 2.2.1. That
covers the canonical API, `session_environment` and the 2.1 replay shape
(`native` without `state`, compaction `metadata` without `recent`).
- **Instance proxy:** none of the new routes is on the allowlist. That
includes the GUI plugin bundle, generation, file watch, media blobs,
usage limits and schedules. The removed voice route was never exposed.
The routes CodeNomad does proxy kept their paths.
- **Published declarations** (2.1.0 pins → 2.1.4 → 2.2.1):
- **Client:** 143 → 147 → 185 methods. 2.2.1 mirrors the experimental
methods under `client.experimental.*`. The only removal is
`voice.instructions`, which CodeNomad doesn't use. 2.2.1 doesn't send
the `experimental-media-uri` capability, so media reads stay inline.
- **Schema:** 2.2.0 adds media refs (`MediaRef`,
`Session.Message.MediaUser`, inbox `MediaUserPayload`), `generate`,
`usage-limits` and `Model.Type`. 2.2.1 adds `goal` and `schedule`. The
one removal since 2.1.0 is `SessionEvent.Source` (2.1.4).
- **Plugin:** 2.2.0 adds the `gui/*` SDK (excluded from the host
runtime) and `Host.packageID`. 2.2.1 adds
`SessionIdle`/`SessionIdleBackground`. The event `subscribe` domain
gains an optional `types` filter; CodeNomad's plugins don't subscribe
through it.
- **Typecheck:** the server and UI typecheck with **0 errors** against
the 2.1.4 and 2.2.1 client/schema/plugin declarations, through scratch
`paths` overrides. The pinned 2.1.0 control is also clean.
- **Host modules:** CodeNomad's three daemon-loaded bundles resolve
`@opencode/plugin` (`Plugin`), `/effect` (`Plugin`, `Rpc`, `Location`),
`/promise/adapter` (`fromPromise`), `/rpc` (`Rpc`), `effect` and
`effect/encoding` (`Base64Url`) from the host. `@opencode/schema` and
`@opencode/client` are bundled from the 2.1.0 pins, and
`@opencode/schema/location` imports are type-only. A probe plugin loaded
in an isolated **2.2.1** daemon resolves every one of these modules and
bindings, as on 2.1.4. The host Effect stays **4.0.1**, with the same
lockfile hash.
- **Private native services:** the pending-request reader and Missions
recurrence use keyed native services: `LocationServiceMap`, `Form`,
`Permission`, `Location`, `Job`, `Database`, `Session` and `Bus`. Their
keys are unchanged at v2.2.1, and the members CodeNomad calls are
unchanged. The changes are additive: `Permission.assertNotDenied`,
`Session.*WithRefs`, an `image` Job kind, and
`LocationServiceMap.canonical` normalizing paths.
- **Storage** (migrations from 2.1.4 to 2.2.1): `blob`/`blob_ref`
(always created; written only with `experimental.media_blob`) and
`schedule` are new. `session_pending` is dropped; CodeNomad never reads
it. A `session_v2.allowance` column is added and dropped within 2.2.0.
- The tables CodeNomad reads directly (`session_message`, `session_v2`,
`session_inbox`, `event`, `event_sequence`, `kv`) keep their shape.
- The pruning plugin's `media-blob.ts` already projects `media:<sha256>`
refs.
- The pruning fixture enables `media_blob` explicitly, so the
always-present `blob` table doesn't change its expectation.
- Missions reads `session_inbox` only by count and ID in production
code.
- **Behavior changes checked:**
- **Code Mode:** 2.2.1 moves programs onto one job queue
([#57976](https://github.com/anomalyco/opencode/pull/57976)). The
run-to-completion regression behind the 2.1.2 minimum passes on 2.1.4,
2.2.0 and 2.2.1: `{ total: 2000, beforeAwait: 1000, count: 1000 }`.
- **Session form polling** returns `[]` without booting a cold Location
([#59083](https://github.com/anomalyco/opencode/pull/59083)). Forms are
in memory, so nothing pending is hidden. This matches CodeNomad's
existing-only pending reader.
- **Location keys and `session.create` directories** are canonical on
POSIX too: trailing slash removed, `posix.normalize`
([#59327](https://github.com/anomalyco/opencode/pull/59327)). Windows
already normalized. This is low risk; it was reviewed in source only,
since the location and ownership fixtures run on Windows and the POSIX
certification covers migration, Code Mode and Git-degraded.
- **Location keep-alive:** the changelog line "keep project services
alive … with a 24-hour inactivity cap"
([#57274](https://github.com/anomalyco/opencode/pull/57274)) doesn't
match the shipped tree. That commit was superseded before 2.1.4, and no
cap exists at v2.2.0 or v2.2.1. From 2.1.4 to 2.2.1,
`location-activity.ts` only canonicalizes paths and generalizes Job
kinds. Long-waiting Missions sessions keep their Location as on 2.1.4.
- **Plugin readiness:** each runner step and tool call now waits for
plugin activation
([#51133](https://github.com/anomalyco/opencode/pull/51133)), and
agent/provider lists wait for plugins. Presence leases stay outside the
discovery root, so this adds no reload.
- **Service lifecycle:** registration and config renames are retried on
Windows. The managed service ignores a saved `XDG_STATE_HOME`
([#58815](https://github.com/anomalyco/opencode/pull/58815)); CodeNomad
never sets it. The `OPENCODE_SERVER_USERNAME` change was added and
reverted within 2.2.0. Detached POSIX children are killed when OpenCode
exits; service start uses its own spawn path.
  - **2.2.1 opt-ins:**
- schedules, both the API and the agent tool, need
`experimental.schedules`;
    - `image_generate` needs `experimental.image_generate`;
    - `/goal` runs only when invoked;
- the subagent tool description now invites follow-ups to *running*
subagents, with the same mechanics as 2.1.4.
- **Other changes:** compaction sizing from the provider's count, MCP
malformed-tool tolerance, OAuth refresh coordination
([#54023](https://github.com/anomalyco/opencode/pull/54023)) and
webfetch redirect rules. They touch no consumed contract and were
reviewed in source; of these, the native suites exercise compaction
only.
- **Upstream #52922** (Bus routing before Location fanout) is still
open: keep the discovery and event-relay mitigations.

**Verdict:** no consumed contract changed from 2.1.4 to 2.2.1.

- The **2.1.0 client/plugin pins can stay**.
- The **2.1.2 minimum has no reason to move**.
- 2.2.1 is admitted through the existing contract recognition, and
nothing found blocks recommending it once the native suites pass on it.
- No CodeNomad bug was found.

Bumping the pins is optional. It would bring the 2.2 client's
discard-tool-result-payload option and its "register running sessions
missed while disconnected" fix
([#53012](https://github.com/anomalyco/opencode/pull/53012)). It would
also move the replay reducer, so it belongs in its own PR. The UI and
server already typecheck against 2.2.1.

**Not verified here:**
- the full native suites against 2.2.0/2.2.1 (only the Code Mode
regression and the host-module probe ran);
- the TUI pruning companion under OpenTUI 0.7. It uses only
`Plugin.define`, `ctx.data.session` and `ctx.data.listen`, so the 0.7
renderable changes don't apply, but this wasn't run;
- POSIX canonicalization on a real Linux/macOS runtime;
- the 2.2.1 release notes, which were unpublished at review time.
## Isolated Windows qualification

Base: dev `157766206`; Node **25.2.1** (local; CI runs the pinned
24.20.0); exact official CLI **2.2.1**; unchanged locked client/plugin
**2.1.0**.

- **Build and checks:** production server and native plugin builds
(pruning, automation, Missions), root typecheck.
- **Native runtime:** Code Mode job ordering (`{ total: 2000,
beforeAwait: 1000, count: 1000 }` on 2.2.1), plugin runtime, automation;
complete pruning, history, proxy, navigation and media; Location,
worktree families/management and event relay; permission receipts; Git
init/degraded.
- **Missions:** contract, family control, native and trajectory; **all
11 recurring Mission journeys** (`ABCDGELFWQN`).
- **Sessions:** environment, fork, aside, interrupt, lifecycle, blank
cleanup (`--conditions=browser`, as in CI), compaction isolation, prompt
skills, provider usage/accounts/selection, service usage, web search,
plugin packages, output limit, Promise-tool failure, subagent depth,
temporary workspaces.
- **Migrations:** history from **2.1.0, 2.1.2, 2.1.4 and 2.2.0** to
2.2.1.
- **Installation:** fresh install, native upgrade and legacy replacement
fixtures.
- **Units after the recommendation change:**
compatibility/transport/update **35/35**; full server **2,628 passed, 8
skipped, 1 cancelled** (`index.test.ts` shutdown test timed out at 5 s
under full-suite load and passes **4/4** alone); ten-locale help checks
pass.

In CI, `latest` resolves to 2.2.1: the compatibility job runs the native
suites against the minimum and latest on Windows, and the migration and
Git-degraded certification runs them on Ubuntu, Windows and macOS. WSL,
packaged desktop, installers/updater end-to-end and real mobile devices
keep their separate release gates.
2026-10-29 15:04:29 +01:00

11 KiB

OpenCode compatibility and runtime qualification

Qualification policy

Qualify the pinned client/plugin dependencies (moved together) against the technical minimum and @opencode/cli@latest before publishing; CI records the resolved runtime. Review a release from its exact tags, published npm packages and an authenticated isolated OpenAPI dump (the tagged packages/protocol/openapi.json can lag the runtime), never GitHub's compare view; release notes are at opencode.ai/changelog (GitHub Releases cover V1 only). Keep results in the PR and CI logs, never as per-version sections here.

Minimum runtime 2.1.2, recommended 2.2.1 (runtime-support.ts), with 2.1.0 client/plugin pins. Earlier native Code Mode breaks JavaScript run-to-completion (lost shared updates, async caller resumed before its first await); scripts/test-codemode-jobs-native.mjs keeps the red/green regression, run in CI against minimum and latest. The pinned 2.1 reducer consumes the replay contract (below), so 2.0.x daemons are refused with native_contract. Plugins share the host Effect 4.0.1/Schema runtime. Unlisted/prerelease/custom/future labels undergo authenticated contract recognition; missing canonical APIs, session environment or the replay shape give a concrete reason. OpenCode 2.1 reads older stored data.

Native Codex subscription usage

Beyond the usage rules in PROVIDER_ACCOUNTS.md: Codex uses only the owned session's integration's first connection and its matching active ChatGPT OAuth credential (chatgpt-browser/chatgpt-headless), never Codex CLI credentials, SQLite or another saved account; WSL uses the selected daemon's credentials. Snapshots are keyed by native connection, instance, session, directory, provider and credential identity and revalidate native selection before publication; the proxy keeps blocking browser credential export and generic RPC. Fixture: node scripts/test-provider-usage-native.mjs <cli> (synthetic credentials and mocked quota HTTP only).

Side questions (/btw)

/btw calls only native session.generate({ sessionID, prompt }) through the ownership-checked proxy and shows its single text answer; the session's environment, transcript and inbox stay untouched, and closing aborts only that request. scripts/test-session-aside-native.mjs (private daemon, local provider) runs in the minimum/latest CI matrix.

Contract notes

  • Generated client URLs keep their baseUrl prefix and pass through unchanged; scheduling classifies API paths relative to the proxy prefix.
  • The proxy allowlist stays closed to pairing (/api/pair, /auth/connect/*), credential export/creation (/api/credential) and experimental voice (/api/experimental/voice/session); a runtime update never widens it or retries a mutation under another contract (instance-proxy.test.ts).
  • Session creation needs an inspectable JSON object; the proxy rejects any parentID before forwarding (native parentID ignores the location).
  • Service lifecycle uses the selected CLI's status/start/password and the registration fallback, never SDK Service.ensure/stop or an assumed default port. A user's opencode service set env value overrides CodeNomad's missing-daemon start variable of the same name; per-send session.environment applies independently.
  • Plugin bundles externalize Effect and the plugin SDK because two Effect runtimes cannot share one native fiber and host Schema APIs differ across releases. Test hosts that spread the Promise plugin context must declare the credential domain. %ProgramData%\opencode is discovered after the global plugin root that config.get consumers take first; native fixtures redirect ProgramData.
  • Relied-on upstream behaviors keep their regressions: test-output-limit-native.mjs (an output-length stop settles failed/idle with one user prompt) and test-promise-tool-failure-native.mjs (rejected Promise tools settle error; permission.rejected prevents the side effect).
  • Upstream #52922 (Bus routing before Location fanout) is open: keep the discovery and event-relay mitigations.

Replay contract

Text/reasoning/tool/step provider data is one native field (on a key shared by a tool call and its result, the result wins). Summary compactions reference metadata.keepID, with kept text in metadata.recent; provider compactions replay from native.messages. On read, the SessionLegacy codec (@opencode/schema) upgrades stored rows/events carrying state, providerState, providerResultState or recent without rewriting bytes, and drops a compaction's providerContext.version/provenance and a providerState stored beside a providerContext. The UI reduces events with the pinned createData (stores/opencode-data.ts); mismatched pins change reasoning identity and stores/session-actions.ts refuses pruning (session-pruning-projection.test.ts). Admission therefore requires Session.Message.Assistant.Text.native without state and Session.Message.Compaction.Completed with metadata but no recent (compatibility/negotiate.ts). planner.ts hashes the SessionLegacy projection and writes surviving stored parts back unchanged (pruning.test.ts). CodeNomad never archives (session.archived/unarchived only invalidate); the UI compaction part keeps native (normalizers.ts). The packed pruning plugin declares every bare import, including @opencode/schema (package.test.ts).

Capability-checked loaded-only pending recovery

Rules: NATIVE_INTERRUPTION_UX.md. Pruning/history methods have their own support and write checks.

The reader uses private native LocationServiceMap, Location, Form and Permission services, scoped existing-only leases and repeated inventory/object-identity checks. It preserves global/idle Forms and native numeric JSON codecs, returning complete coverage (including empty cold directories) with the caller's actual originDirectory. Bounds: 64 requested directories/selected placements, 2048 loaded keys, 1024 Forms and Permissions per placement/directory, 4 MiB UTF-8 output, a two-second reader deadline and the broker's 30-second overall deadline (including Git ownership revalidation, eight directories at a time through the Git worker). Overflow or unsafe serialization fails rather than truncates.

The UI sends at most eight directories per request within the escaped URL budget, under its own ten-second deadline; recovering a whole history on a slow filesystem can take arbitrarily long. The broker rejects overflowing raw JSON numbers (including unknown metadata) before coverage/capability admission; native numeric codec strings pass through unchanged.

GET /api/workspaces/:id/pending-requests keeps its UI wire shape and calls only POST /api/rpc/codenomad.pending-requests/snapshot?location[directory]=<root> with { "input": { "directories": [...] } }. Legacy workspace identities are rejected and never erased; workspace/connection replacement, root or candidate deletion and changed authority fence dispatch and publication. Cold UI coverage synthesizes a location from the validated native entry directory, never the browser's alias. Only HTTP 400 with a strict bounded RpcError envelope (_tag: "RpcError", type rpc.unavailable or rpc.method_not_found, message of at most 4,096 characters) means unsupported, and only after origin and candidate revalidation. Plain 404s, other errors, malformed output, authentication failures, reader errors, timeouts and overflow return non-authoritative 503s. An unverified capability is deliberately never probed during an observed compaction.

Candidate traversal constructs no cold Locations; native HTTP middleware may warm or rebuild the single bootstrap root, and root eviction or a config rebuild constructs it again. Snapshots can go stale after leases release, so per-kind mutation fences and settlement tombstones stay essential.

Agent conventions

  • Desktop automation and pruning provisioning follows the authenticated OpenCode connection and its config.get global discovery directory, including reconnects. Replacing a connection aborts its in-flight provisioning: opencode/desktop-plugin-provisioning.ts logs that at debug as superseded (the new connection provisions again) and keeps errors for failures on the current connection. Never derive a running daemon's roots from backend/startup environment or CLI debug paths. OpenCode watches the entire discovery root recursively: keep changing presence leases outside it, in the sibling .codenomad/<root-hash>/ namespace. Retain existing outside-root storage; migrate inside-root storage while reading older backends' leases without writing there. WSL translates the daemon-reported paths through the selected distro only for filesystem access. Verify heartbeat stability with the isolated native automation fixture.
  • Plugins register only at their next presence check, so a backend's first plugin RPC could arrive before registration and be refused. Followers acknowledge a lease with a <lease>.ready marker (current namespace only) once every follower of that directory in the native process has observed it while registered; routes calling bundled-plugin RPCs await workspaceManager.desktopPluginsReady() before reading or authorizing the session, so ownership checks and deletion-fence entry stay adjacent (bounded to 5 s, never rejects). The bound is paid in full only when no marker can arrive: no Location loaded yet, or pre-marker plugin copies still loaded after an upgrade. Do not replace this ordering with retries of refused reads.
  • The shared native event relay consumes upstream events before slow routing I/O. Preserve per-session/PTY/Shell and per-recipient FIFO, validated full-location ownership, invalidation/connection/workspace fences and bounded-backlog reconnect recovery. When an ownership lookup still fails after its retry, the relay evicts that cache entry and pulses that recipient's status alone (error with event routing failed, then connected), so its clients run their reconnect reconciliation (one pass per instance, with later requests coalesced into one trailing pass, ui/src/lib/trailing-resync.ts). Pulses are leading and trailing, at most every 5 s, doubling to 60 s while failures persist. Never fail the shared subscription for one recipient's lookup: that resyncs every instance. A slow recipient must not block another or the shared SDK subscriber. Validate with the relay regressions and isolated native location fixture.
  • Native OpenCode plugins share packages/server/scripts/native-plugin-build.mjs: never embed Effect or the public plugin SDK into daemon-loaded bundles (version boundaries: Qualification policy above). Keep backend-side OpenTunnel isolation separate, and run the bundle regression plus isolated plugin-runtime/pruning/automation/Missions fixtures when changing this boundary.