Closes #694 ## Status This PR contains the stable 2.0.4 client migration, authenticated V2 discovery fallback and shared cross-runtime compatibility implementation. The validation results below are local execution evidence; the newly added CI matrix must also run on the updated PR head. ## Published changes - Pin UI/server to official `@opencode/client@2.0.4` and the bundled pruning plugin to `@opencode/plugin@2.0.4`, with the corresponding lockfile migration. - Probe authenticated `/api/status` first. Fall back to earlier V2 `/api/health` **only on HTTP 404**, retaining the endpoint, credentials, 64 KiB response bound and absolute deadline. Authentication, transport, server and malformed-status failures do not trigger fallback. - Migrate the canonical UI/server contract to stable Forms, permission decisions, command names, interrupts, renaming, forks, message lookup and inbox delivery. - Update the guarded proxy for stable methods/paths, native active-session envelopes, native `cursor.next`, global Form directory headers and current resource/plugin events. - Follow the renamed upstream platform binary `bin/opencode(.exe)` while retaining public CLI aliases. Discovery success alone does not establish compatibility with an earlier runtime's conversation APIs. ## Shared cross-runtime compatibility ### Shared cross-runtime integration - Introduce one connection-scoped adapter under `packages/server/src/opencode/compatibility/`, used by both the guarded UI proxy and direct server callers, including Yolo. - Select the contract using authenticated daemon identity rather than the CLI executable's version. Recognize unknown publications through authenticated, bounded OpenAPI negotiation instead of an exact runtime-version gate. - Bind authorization, client, transport and profile to one connection generation. Fence actual mutation dispatch after asynchronous body preparation; prevent late old streams from publishing or invalidating their replacement. - Translate earlier V2 routes, methods and payloads before sending. Never retry a mutation speculatively after 400, 404, authentication or transport failures. - Normalize legacy HTTP inbox timestamps, including compaction admission, before the pinned Solid reducer. Preserve the distinct native SSE inbox schema and durable event metadata. - Restore legacy catalog/content invalidation and permission-event convergence. Preserve meaningful method/path/status/profile diagnostics and native 401 error envelopes. ### Location authority and pruning - Preserve legacy workspace identity internally through request serialization, catalog/cache keys, Forms, imports, cursors, events and exact move rollback. Modern public APIs continue to reject obsolete workspace selectors. - Authorize complete locations before translating directories or synthesizing upstream Form/location headers. Keep native cursor tokens unchanged. - Preserve session/path ownership, worktree mutation fences and the restricted proxy allowlist. Generic RPC, plugin management and destructive revert commit remain blocked. - Carry verified legacy identity through pruning membership checks, preview and the existing transactional storage/claim fence; do not relax deletion checks. - Treat native idle records as control records, retaining identity/outcome without fabricated assistant text. - Add a common multi-runtime CI matrix and the audit/evidence register in `dev-docs/OPENCODE_V2_COMPATIBILITY.md`, including preserved historical architecture findings. ## Local validation All native tests use isolated synthetic daemons, configuration and databases, never the shared daemon or user storage. | Check | Result | | --- | --- | | Same native suite on beta-19271, 2.0.3, 2.0.4 and 2.0.5 with pinned client/plugin 2.0.4 | Passed | | Full native pruning suite with non-null legacy workspace identity on beta-19271 and 2.0.3 | Passed | | Rendered UI on modern 2.0.5 and beta-19271 with non-null identity | Passed | | Native 2.0.5 suite inside Ubuntu/WSL, using a temporary Linux filesystem root | Passed | | Server tests (after CI follow-up) | 500 passed, 2 skipped | | UI store tests | 441 passed | | Electron native tests | 194 passed, 4 skipped | | UI/server/Electron typechecks | Passed | | UI/server and Electron production builds | Passed | | Windows Tauri release executable (`--no-bundle`) | Passed | | Electron/Tauri packaged-resource smoke checks | Passed | | Desktop packaging invariants | 4 passed | | `git diff --check` | Passed | The native matrix exercises the actual shared connection, generated client, UI fetch adapter and guarded proxy: voice-off/on instructions, prompts, commands, session Shell, background Shell output cursors/removal, UI/Yolo permission acceptance and rejection, busy inbox delivery/cancellation, interrupt/wait, staged revert clearing, session/global/worktree Forms, native pagination, export/import, same-directory legacy identities, scoped events, foreign authority refusal and exact rollback. Pruning coverage includes actual storage identity and execution claims, busy refusal, idempotency, competing admission, next-model payload, fork isolation, pre-compaction history, restart, backend presence disposal/reopening and crash expiry. Rendered tests cover individual/group/response/session cleanup, confirmation counts, persistence and reload. ## Autonomous gatekeeper review Review/fix/retest loops concluded with no remaining actionable findings in the reviewed transport/lifecycle/proxy, identity/UI/events/pruning/rollback, scripts/CI and rendered-fixture scopes. Corrections included dispatch-generation races, compaction admission normalization, independent negotiation cancellation, first-request profile resolution, preservation of streamed 401 envelopes and direct Shell/PTY identity propagation. A final rendered failure was traced to the fixture awaiting only the first RPC of a cross-message deletion; the fixture and regression now await every affected message. Production deletion guards were not relaxed. This scoped approval does not close the separately preserved historical architecture findings. ## Validation boundaries ### CI follow-up The first run on `a0c1c943` ([35142559235](https://github.com/NeuralNomadsAI/CodeNomad/actions/runs/35142559235)) failed in five native jobs. The local bundle tests had missed the standalone npm archive's missing sibling import, and Windows legacy jobs exposed a short-path versus canonical-path project comparison. The dependent artifact-comment job reported that same build failure. The follow-up makes the plugin's identity primitives self-contained, adds an isolated npm-file-list regression for all exported entrypoints, and corrects project identity selection with alias and nested-repository regressions. The packed-plugin failure was reproduced before correction; the corrected installed package passes the native suite on Windows 2.0.4 and Linux/WSL 2.0.4/2.0.5. The corrected 2.0.4 shipped bundle also passes rendered/native pruning. Fresh full beta-19271 and 2.0.3 `--legacy-pruning` suites pass under a reproducing Windows short-path TEMP root after the final exact-directory fix. Independent final reviews approve packaging/documentation and worktree identity with zero actionable findings. Remote checks on the new head remain the CI authority; these are local follow-up results. - Full interaction with the installed desktop application through Developer Mode was not verified because Developer Mode is inactive. The release executable and packaged-resource checks above are build/smoke evidence, not a fresh-install interactive certification. - Remote run [35146390688](https://github.com/NeuralNomadsAI/CodeNomad/actions/runs/35146390688) at `486e856d` passes all runtime-contract jobs, Windows/Linux native pruning, service compatibility, general tests and both Tauri checks. macOS passes packed-plugin loading/pruning, then times out on late bundle discovery. The fixture now canonicalizes its root before daemon startup to align `/var` configuration paths with `/private/var` FSEvents paths. Late installation remains mandatory and asserts the plugin was initially absent; failures now upload isolated diagnostic logs/inventory/captures. Actual macOS confirmation remains pending on the new head. - Interactive native TUI validation was not performed. - The passing WSL result is Linux-native execution inside WSL, not a complete Windows-host/WSL desktop traversal. With the fixture's configuration directory under `/mnt/c`, conversation/proxy checks passed but late plugin discovery timed out; the otherwise equivalent Linux-filesystem run passed. Mounted-configuration late discovery remains a documented environment limitation. - Other audited publications are declaration-reviewed, not individually native-certified. The four-runtime matrix does not imply universal V2 compatibility. - Earlier PR artifacts do not contain the completed cross-runtime implementation; use artifacts built from the updated head. Local passing results do not imply that the updated remote CI has completed.
6.9 KiB
Technical Implementation
OpenCode Dependency
Server and UI pin @opencode/client@2.0.4. Import the generated Promise client from @opencode/client and upgrade both packages and the lockfile together. The runtime CLI is managed independently; startup validates its authenticated loopback /api/status response without requiring an exact version string. Review current documentation, installed declarations, and proxy/API parity whenever the client contract changes.
Do not add @opencode-ai/sdk, old { data, error } SDK wrappers, createOpencodeClient(), or a packages/opencode-plugin package. Verify method signatures in node_modules/@opencode/client/dist/promise/.
Server Integration
OpenCodeSharedService is the sole service adapter. Host and WSL paths both execute the selected CLI's official service status, service start, and service get password commands, validate the authenticated loopback endpoint, pin its identity while active, and never stop the externally owned global daemon on backend shutdown.
OpenCode owns the daemon's standard state, database, and registration; CodeNomad has no private port, database, registration, or daemon PID. Allowed configured environment variables and the current NODE_EXTRA_CA_CERTS are passed only to service start for a missing daemon. Existing daemons are unchanged, and OPENCODE_DB/XDG_STATE_HOME are ignored. WSL requires Windows localhost forwarding and runs this lifecycle inside Linux without Windows PID operations.
Workspace creation passes a native location:
await client.location.get({ location: { directory } })
WorkspaceManager records the returned directory; public locations are directory-only and location.get().project provides project metadata. Explicit Stop Workspace evicts that location/resources and removes the logical workspace without stopping the global daemon. Ordinary tab/window close only detaches local UI state and does not call the delete/eviction path.
Native Windows And Restore State
Each channel/config profile has one native singleton process and one backend. A second launch opens another UUID window by default; Advanced settings can restore MRU focus, while --new-window always requests another window. Stable, dev, and non-default config profiles use isolated native/browser/client-state scopes.
OpenCode sessions/messages are shared service data. Each window separately persists tab membership, drafts, view state, and native bounds in the client-state V3 envelope. Snapshot V2 is a SHA-256 content-addressed partition graph. Electron and Tauri prepare immutable partitions, fence migration/root replacement on current ownership and renderer authority, atomically publish the envelope, then conservatively sweep partitions no window references.
UI Integration
packages/ui/src/lib/sdk-manager.ts constructs clients with OpenCode.make() at /workspaces/:id/instance/. Use getRootClient(instanceId) from packages/ui/src/stores/opencode-client.ts; pass native directory/location inputs when required.
Session actions use native APIs directly:
await client.session.prompt({ sessionID, text, files })
await client.session.shell({ sessionID, command })
await client.session.instructions.entry.put({ sessionID, key, value })
Shell mode and conversation instructions are upstream features and remain separate from native background Shells and interactive V2 PTYs. None requires a CodeNomad plugin.
Native background Shells are location-scoped and listed in the Status panel. packages/ui/src/stores/shell-store.ts refreshes the list on native Shell events and reconnect, exposes native metadata, and supports removal. The proxy verifies Shell cwd ownership before ID-scoped operations and forwards native output cursors unchanged. Interactive pty.* terminals remain separate.
Routing And Security
- CodeNomad operations:
packages/ui/src/lib/api-client.ts->/api/*. - OpenCode operations: generated client ->
/workspaces/:id/instance/api/*. - Browser events:
GET /api/events; heartbeat response:POST /api/client-connections/pong. - The proxy exposes only an explicit method/path allowlist, checks client-provided directories and prompt files, defaults safe requests to the workspace location, and verifies session ownership before forwarding. New OpenCode routes are unavailable until reviewed and allowlisted.
Never trust a browser-supplied worktree path. Resolve workspace/worktree ownership server-side.
Native SideCar/browser preview iframes are sandboxed without allow-same-origin; DOM comment inspection is therefore web-only.
CodeNomad-Owned Mutations
Git status/diff and mutations remain CodeNomad APIs. Stage, unstage and commit execute validated Git commands in packages/server/src/workspaces/git-mutations.ts; the UI calls /api/workspaces/:id/worktrees/:slug/git-*.
Yolo also remains CodeNomad-owned. AutoAcceptManager persists policy state, observes native permission events, replies with client.permission.reply, and publishes yolo.stateChanged/yolo.autoAccepted over /api/events.
Events
InstanceEventBridge consumes the one shared client.event.subscribe() iterable. It maps location-scoped events to workspace IDs and publishes instance.event through the CodeNomad EventBus. This stream is volatile: reconnection does not replay a guaranteed history, so UI stores refetch sessions and pending requests and other consumers must re-read authoritative file/config state.
Use current protocol names. Session events include session.created, session.renamed, session.moved, session.status, session.idle, session.execution.*, session.compaction.*, session.text.*, session.reasoning.*, and session.tool.*; background-process refresh events include shell.created, shell.exited, and shell.deleted; file and config invalidations are filesystem.changed and config.updated.
Current Structure
packages/server/src/
server/routes/ CodeNomad /api routes
workspaces/manager.ts workspace/location ownership
workspaces/opencode-service.ts
workspaces/instance-events.ts
workspaces/git-status.ts
workspaces/git-mutations.ts
permissions/ Yolo and permission policy
packages/ui/src/
lib/api-client.ts CodeNomad API and /api/events
lib/sdk-manager.ts native OpenCode Promise clients
stores/opencode-client.ts root client authority
stores/session-api.ts session queries/lifecycle
stores/session-actions.ts prompt, Shell, instructions
stores/shell-store.ts location-scoped native background Shell state/actions
Deleted packages/opencode-plugin, server plugin/background-process, and per-workspace runtime files are not architectural extension points and must not be restored.
Validation
- Run root typecheck or the relevant server/UI workspace typecheck.
- Run focused tests for service lifecycle, instance proxy, event bridge, Git mutations, or Yolo when changing those boundaries.
- Update server API types and UI consumers together.