## Summary
- Qualify OpenCode **2.1.4** (latest stable, 23 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 and 2.1.3 daemons stay ready and get an optional upgrade offer.
2.1.4 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.1.4
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.3 → v2.1.4, 72 commits)
- **Authenticated OpenAPI:** 121 → **124 paths**, 146 → **149
operations**. No operation changed. The changes:
- added `POST /api/experimental/client-mcp/token`;
- added `GET /api/experimental/client-mcp` (WebSocket);
- added `POST /api/debug/heap-dump`;
- `Config.InfoEncoded` gains the opt-in
`experimental.background_compaction`.
The session event (`V2EventEncoded`) and message schemas are unchanged.
None of the new routes is on the instance proxy allowlist, and the proxy
relays WebSockets only for SideCars and previews.
- **Published declarations:**
- the client adds `mcp.client.token` and `debug.heapDump`;
- the schema and plugin changes are type refactors and unused-export
removals, among them `SessionID.descending`, `SessionEvent.Source` and
`PromptInput.FileAttachment.create`; `Session.Message.ID.fromEvent` now
takes any string instead of an event ID. CodeNomad uses none of these,
and every host module its native plugin bundles import
(`@opencode/plugin`, `/effect`, `/promise/adapter`, `/rpc`,
`@opencode/schema/location`, `/session`) still exists.
- **MCP elicitation forms**
([#58184](https://github.com/anomalyco/opencode/pull/58184)): an MCP
server asking for input during a tool call now creates a global-session
Form whose `form.created` event carries the server's location.
CodeNomad's live path requires that location for global Forms and shows
them as a **Project request**. Review found that the live path dropped
the location from the queued Form, so its reply lacked
`x-opencode-directory` and failed for a worktree or subdirectory until a
pending-request rescan. **#1127** fixes that; recovery already kept it.
A native probe (minimal stdio MCP server, a real Code Mode `execute`
call, CodeNomad-style reply) gives:
| Runtime | Form listed for the location | `form.created` carries the
location | Reply completes the tool |
| --- | --- | --- | --- |
| 2.1.3 | yes | **no** | yes |
| 2.1.4 | yes | **yes** | yes (`action: accept`, `channel: stable`) |
On 2.1.3 such a request only appeared after a pending-request rescan; on
2.1.4 it appears live. The probe sends the location header itself, so it
checks the runtime contract; #1127 covers CodeNomad's live reply path.
- **Other changes** are covered by the native suites below:
- compaction (oldest-first shortening, ignored buffers, overflow
detection);
- the agent's model for sessions without one;
- V2 agent files;
- V1 global-session import attribution;
- MCP lifecycle fixes.
- **Live check:** this machine's shared daemon runs 2.1.4, and the
CodeNomad desktop session works through it.
## Isolated Windows qualification
Base: dev `9ac792fff`; Node **24.20.0**; exact official CLI **2.1.4**;
unchanged locked client/plugin **2.1.0**.
- **Build and checks:** production server/UI/native plugin builds,
server and root typechecks, native bundle guard.
- **Native runtime (9/9):**
- Code Mode job ordering, plugin runtime, automation;
- complete pruning, history, ownership, worktree and event relay;
- loaded-only pending snapshot;
- Missions contract, family control, native and Compare/Wayfinder
trajectory.
- **Sessions (15/15):** environment, fork, aside, interrupt, lifecycle,
blank cleanup, prompt skills, provider usage/accounts/selection, service
usage, web search, plugin packages, output limit, Promise-tool failure.
- **Recurring (5/5):** all **11 recurring Mission journeys**, subagent
depth, **2 MiB** compaction, permission receipts.
- **Migrations (10/10):**
- history from **2.0.3, beta-19271, 2.0.32, 2.1.0, 2.1.1, 2.1.2 and
2.1.3** to 2.1.4;
- Git degraded/init;
- temporary workspaces.
- **Installation (3/3), with the 2.1.4 recommendation:**
- fresh install of the 2.1.2 minimum;
- native **2.0.15 → 2.1.4** upgrade, after which the already-running
daemon keeps its PID and 2.0.15 version;
- explicit OpenCode **1.18.10 → 2.1.4** replacement.
- **After the recommendation change:**
- admission, transport, update, installation and instance proxy units
**116 passed, 1 POSIX-only skipped**;
- full server **2,471 passed, 8 skipped**, no failure or cancellation;
- UI units **1,699/1,699**;
- focused browser **126/126**;
- native pruning with the real rendered UI;
- the exact CI invocation of the Code Mode regression (without tsx);
- ten-locale help checks (**9/9**).
This is Windows automation evidence. In CI, `latest` resolves to 2.1.4:
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.
10 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.1.4 (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
baseUrlprefix 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
parentIDbefore forwarding (nativeparentIDignores the location). - Service lifecycle uses the selected CLI's status/start/password and the
registration fallback, never SDK
Service.ensure/stopor an assumed default port. A user'sopencode service set envvalue overrides CodeNomad's missing-daemon start variable of the same name; per-sendsession.environmentapplies 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
credentialdomain.%ProgramData%\opencodeis discovered after the global plugin root thatconfig.getconsumers take first; native fixtures redirectProgramData. - Relied-on upstream behaviors keep their regressions:
test-output-limit-native.mjs(an output-length stop settles failed/idle with one user prompt) andtest-promise-tool-failure-native.mjs(rejected Promise tools settleerror;permission.rejectedprevents 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.getglobal discovery directory, including reconnects. Replacing a connection aborts its in-flight provisioning:opencode/desktop-plugin-provisioning.tslogs 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 CLIdebug 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>.readymarker (current namespace only) once every follower of that directory in the native process has observed it while registered; routes calling bundled-plugin RPCs awaitworkspaceManager.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. 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.