The shared OpenCode connection is rechecked by the first acquisition more than 30 seconds after its last check, by rerunning CLI discovery (`service status`, `service get password` and an authenticated status probe, bounded by the caller's deadline). When that discovery failed or reported no service, the healthy connection was invalidated. A slow daemon, or slow CLI spawns on Windows, could therefore tear down a working connection. Every event stream on it then broke and the relay reconnected in the middle of streaming: the reconnection the transcript now survives (#1088), triggered without any real failure. Discovery failure and absence are now inconclusive. The working connection is kept and the next recheck waits for the following interval, so a slow service is not re-probed on every request. A discovered daemon that differs in URL, PID, version or credentials still replaces the connection immediately. A dead connection is still retired by failed location validation (`withClient`), proxy transport or authentication failures and upstream event-stream EOF/errors, after which the next acquisition discovers or starts the service as before. An inconclusive recheck never starts another service. A frozen daemon that accepts TCP but never answers is now detected through those paths, bounded by Undici's inactivity timeouts, rather than by the periodic recheck. Validation: two new `opencode-service` tests fail on the previous code. One keeps the connection through a timed-out and an empty recheck without rediscovering before the next interval or starting a service. The other keeps a live event stream delivering events while another request's recheck fails. The shared-service, connection generation, setup reconnect, event relay/routing/ownership, subscriber lifetime, pending discovery, plugin lifecycle and proxy suites pass (82/82), the wider workspace manager, CLI/WSL lifecycle, native registration and opencode-update suites pass (227/227), and the server typecheck is clean.
9.5 KiB
CodeNomad Architecture
Overview
CodeNomad is a SolidJS UI and Fastify server hosted by Electron or Tauri. The official @opencode/client dependency pins live in the server and UI manifests; the runtime CLI is independently managed.
Desktop host -> CodeNomad server -> one shared OpenCode service
^ |
| +-> CodeNomad /api/* and /api/events
+------ UI clients through /workspaces/:id/instance/api/*
CodeNomad's bundled native plugins (automation, see BROWSER_AUTOMATION.md; session pruning; Missions) load through native V2 discovery; none owns the daemon.
Shared Service And Locations
packages/server/src/workspaces/opencode-service.ts runs the selected host or WSL CLI's official service status, service start, and service get password lifecycle, validates the authenticated loopback endpoint, and pins that identity while active. It connects to one externally owned global daemon and never stops it on backend shutdown. An acquisition more than 30 seconds after the last check repeats discovery: a different discovered daemon (URL, PID, version or credentials) replaces the connection, while a discovery that fails or finds no service keeps it until the next interval. Failed location validation, proxy transport or authentication failures and upstream event-stream EOF/errors retire the connection.
OpenCode owns the daemon's standard state and database. Configured allowed environment variables and NODE_EXTRA_CA_CERTS apply only if CodeNomad starts a missing daemon; a running daemon keeps its environment. OPENCODE_DB and XDG_STATE_HOME never reach the daemon or session environments (workspaces/host-opencode-service.ts, workspaces/session-environment.ts). WSL support requires Windows localhost forwarding and executes the Linux CLI lifecycle inside the selected distribution without cross-namespace PID operations.
packages/server/src/workspaces/manager.ts treats selected folders as native OpenCode locations:
- Validate the directory with
client.location.get. - Store the returned
LocationRefand publish the logical workspace. - Reuse the shared service for every additional directory.
- Closing a project tab deletes that occurrence's logical workspace record (
DELETE /api/workspaces/:id; see PROJECTS.md).
Native Profiles, Windows, And Client State
Electron and Tauri run one native singleton process and one CodeNomad backend per data profile/config pair (DESKTOP_DATA_PROFILES.md), isolating singleton identity, backend/browser storage and client state. A second launch opens another UUID-backed window by default; Advanced settings can restore most-recent-window focus, while --new-window always creates another window.
OpenCode sessions and messages are shared through the global daemon. Window membership, tabs, drafts, view state, and native bounds are local to each UUID window. Client-state V3 is a per-window envelope over the V2 SHA-256 content-addressed partition graph: immutable partitions are prepared before atomic root publication, writes and migrations are fenced by current ownership and renderer authority, and garbage collection runs after publication while retaining every partition referenced by any window.
Previews use unguessable capabilities for HTTP and WebSocket traffic. Electron and Windows Tauri local windows open HTTP(S) pages in hardened native child webviews sharing one preview profile kept separate from the app; other clients use the capability-scoped iframe proxy. Browser preview iframes are opaque-origin sandboxes without allow-same-origin; preview element comments use a source-checked message bridge instead of parent DOM access. SideCar iframes add allow-same-origin by design: a SideCar is a trusted extension of CodeNomad, and its scripts run with the application's origin and access. /sidecars/* authenticates with the SameSite=Lax session cookie and rejects Origin: null, which an opaque frame cannot satisfy.
API Boundaries
CodeNomad control APIs live under /api/*. Important routes include:
/api/workspacesand/api/workspaces/:id/worktrees/*/api/workspaces/:id/worktrees/:slug/git-status|git-diff|git-stage|git-unstage|git-commit/api/eventsand/api/client-connections/pong/api/storage,/api/settings,/api/filesystem,/api/speech/api/opencode-plugin/automation, authenticated by a per-process loopback token and restricted to CodeNomad-owned locations/api/remote-control/*, managed from the host only;POST /api/remote-control/pairand/remote-pairanswer only through Remote Control (REMOTE_CONTROL.md)
Native OpenCode requests use /workspaces/:id/instance/api/*. The Fastify proxy exposes an explicit method/path allowlist, adds shared-service authorization, validates prompt files, defaults safe requests to the workspace location, and rejects locations/directories outside the selected workspace or its worktrees. Session routes also verify session.location.directory. Never trust a browser-supplied worktree path: resolve ownership server-side. Upstream additions are exposed only after an explicit proxy review.
Yolo state endpoints live at /workspaces/:id/yolo/sessions/:sessionId; Yolo notifications use /api/events.
Client And Events
packages/ui/src/lib/sdk-manager.ts uses OpenCode.make() and caches generated Promise clients by instance proxy path. packages/ui/src/stores/opencode-client.ts is the root-client authority; native directory/location fields select the worktree.
The server holds one client.event.subscribe() stream. InstanceEventBridge maps native location events to CodeNomad instance.event records, and /api/events multiplexes them with workspace and Yolo events for the browser; heartbeats use POST /api/client-connections/pong. The stream is volatile and has no replay guarantee: reconnect must reconcile sessions and pending requests and reread authoritative file/config state. Pending recovery uses the loaded-only broker, never worktree-wide ordinary lists; see compatibility, whose Agent conventions also specify relay ordering and ownership fences.
Native events include session lifecycle/output events (session.created, session.renamed, session.moved, session.execution.*, session.retry.scheduled, session.step.*, session.compaction.*, session.text.*, session.reasoning.*, session.tool.*), background Shell refresh via shell.created, shell.exited, shell.deleted, file invalidation via filesystem.changed, and configuration invalidation via config.updated. Each busy period settles with exactly one session.execution.succeeded/failed/interrupted; derive session activity from these events, never from the declared but unpublished session.status/session.idle.
Feature Ownership
| Feature | Owner |
|---|---|
| Sessions, messages, permission/question APIs | OpenCode V2 |
| Shell mode | client.session.shell |
| Conversation instructions | client.session.instructions.entry |
| Background Shell management | Location-scoped OpenCode V2 shell.* API through the ownership-checking proxy; Status panel UI |
| Interactive PTY management | Separate OpenCode V2 pty.* API |
| Workspace lifecycle and directory authorization | CodeNomad |
| Git status/diff/stage/unstage/commit | CodeNomad server |
| Yolo state, persistence and auto-accept | CodeNomad server |
| Browser SSE multiplexing | CodeNomad server |
| Desktop inspection, CDP feedback and browser previews | CodeNomad desktop host/browser controllers and the bundled automation plugin |
| Remote Control transport, pairing and admission | OpenTunnel SDK route plus packages/server/src/remote-control/ (REMOTE_CONTROL.md) |
Session Shell and conversation instructions are native features. Session Shell is separate from background Shell and PTY management. The Status panel lists location-scoped native background Shells, refreshes on Shell events/reconnect, displays native metadata, and allows removal. The proxy verifies Shell cwd ownership before ID-scoped operations and preserves native output cursor pagination.
Persistence
CodeNomad configuration resolves through packages/server/src/config/location.ts: config.yaml, state.yaml, remote-control-devices.json, and instances/ under ~/.config/codenomad/. config.json is read only as migration input.
Implementation Map
Server entrypoints are packages/server/src/index.ts and server/http-server.ts; shared-service authority lives in workspaces/opencode-service.ts and workspaces/manager.ts, event routing in workspaces/instance-events.ts, and bundled automation provisioning in opencode/automation-plugin.ts. UI native calls use stores/opencode-client.ts, session-api.ts and session-actions.ts; CodeNomad routes use lib/api-client.ts. Current integration conventions are in AGENTS.md and the area documents it indexes.
Agent conventions
- SideCar proxy cookies use
sidecars/proxy-cookies.ts: authenticate locally first, then remove the actual auth cookie, thecodenomad_sessiondesktop namespace and paired-device cookie upstream; filter the same names from HTTP and WebSocket response cookies, preserving service cookies. Handshakes go through Node's bounded HTTP parser inserver/proxy-websocket.ts, including non-101 responses and coalesced binary data. Same-origin SideCar scripts keep full application access: SideCars are trusted extensions by design.