Skip to content

fix(daemon): let opencode choose its own port when spawning runtimes#28

Merged
chenxin-yan merged 2 commits into
mainfrom
port-fix
Apr 22, 2026
Merged

fix(daemon): let opencode choose its own port when spawning runtimes#28
chenxin-yan merged 2 commits into
mainfrom
port-fix

Conversation

@Aleexc12
Copy link
Copy Markdown
Collaborator

@Aleexc12 Aleexc12 commented Apr 21, 2026

Summary

The daemon could register N instances but only ever run one at a time. Starting a second instance failed with:

Server exited with code 1
Warning: OPENCODE_SERVER_PASSWORD is not set; server is unsecured.
Failed to start server on port 4096

The TUI hit this when switching models, because that path triggers a fresh opencode spawn while the existing one is still holding port 4096.

Root cause

OpencodeRegistry.ensureStarted in packages/daemon/src/opencode.ts called createOpencode() with no options. @opencode-ai/sdk@1.3.13's createOpencodeServer then filled in its hardcoded default port:

// @opencode-ai/sdk v2 — createOpencodeServer
options = Object.assign(
  { hostname: "127.0.0.1", port: 4096, timeout: 5000 },
  options ?? {},
)
const args = [`serve`, `--hostname=${options.hostname}`, `--port=${options.port}`]

So every spawn asked the opencode binary to bind exactly 127.0.0.1:4096. The second concurrent spawn lost the race and exited with EADDRINUSE.

The fix

One change: createOpencode()createOpencode({ port: 0 }).

  • --port=<nonzero> — binds exactly that port; exits 1 on EADDRINUSE (no fallback).
  • --port=0 — tries 4096 first, then falls back to an OS ephemeral port if 4096 is busy. Supports arbitrarily many concurrent instances.

Verification

Reproduced against @opencode-ai/sdk@1.3.13 and the bundled opencode binary:

  1. With the old code (createOpencode()): Started two registered instances via ralph daemon instance start. First bound to 4096; second failed with the exact error above.
  2. With the fix (createOpencode({ port: 0 })): Same two-instance drive. First bound to 4096; second bound to an ephemeral port (61264 in one run, 53157 in another). Both instances ran concurrently with no collision.
  3. Sanity check on the opencode binary directly: opencode serve --port=0 alone binds 4096; running a second one concurrently lands on an ephemeral port. Confirms the fallback is inside the opencode binary, not the SDK.
  4. Manual TUI validation: opening the model picker, switching models mid-conversation, and driving two real-workspace instances in parallel all succeed end-to-end.

Test plan

  • Repro original failure with createOpencode() → "Failed to start server on port 4096" on second instance
  • Confirm fix with createOpencode({ port: 0 }) → two instances run concurrently on distinct ports
  • Confirm via direct opencode serve --port=0 invocation that fallback lives in the opencode binary
  • Exercise the TUI model-switching path end-to-end (the originally reported trigger)

Files touched

  • packages/daemon/src/opencode.tsOpencodeRegistry.ensureStarted now passes { port: 0 } to createOpencode (+ a biome-driven reformat of the surrounding .then callback).

@Aleexc12 Aleexc12 requested a review from chenxin-yan April 21, 2026 21:45
Pass `port: 0` to `createOpencode` in `OpencodeRegistry.ensureStarted`
so multiple concurrent instances stop colliding on 4096.
@chenxin-yan chenxin-yan merged commit 04eae39 into main Apr 22, 2026
1 check passed
@chenxin-yan chenxin-yan deleted the port-fix branch April 22, 2026 22:53
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.

2 participants