Skip to content

cli: background service startup reports generic timeout, hiding port-in-use root cause #49009

Closed as duplicate

Description

@sghng

Summary

When the managed background service cannot bind its port, every spawned serve --service contender fails with a clear, actionable error ("port already in use by another process") — but the client only surfaces Error: Timed out waiting for the background service to start. The actual cause is only visible by digging through the log file.

Environment

  • opencode version: 2.0.3
  • OS: Darwin 27.0.0 (macOS, arm64)
  • Terminal: ghostty (TERM=xterm-ghostty, COLORTERM=truecolor)
  • Shell: fish
  • Install/channel: latest (bun global install)
  • Active plugins: none found in server config (oh-my-openagent:tui and which-key TUI plugins enabled in client state)

Reproduction

  1. Occupy the managed service port with any process that prevents bind() — e.g. python3 -m http.server <port> or, as in my real case, any lingering ESTABLISHED connection whose local port matches.
  2. Run opencode service start (or plain opencode, which auto-starts the service).
  3. Wait through the retry loop (~2 minutes of silent respawns).

Observed on macOS where a stale Tailscale-loopback connection (127.0.0.1:49383 -> 127.0.0.1:49374, ESTABLISHED, no LISTEN socket anywhere) made the port unbindable even with SO_REUSEADDR — verified with a manual Python bind(). Nothing showed up under lsof -sTCP:LISTEN, so even the standard "find what listens on the port" advice failed; only the opencode log revealed the truth.

Expected Behavior

The final error surfaces the underlying cause from the spawn attempts, e.g.:

Error: Timed out waiting for the background service to start
  cause: Managed service port 49374 on 127.0.0.1 is already in use by another process.
         Configure another port with `opencode service set port <port>` and start the service again.

Actual Behavior

Only this is shown:

Starting background server...
ERROR (#1): Error: Timed out waiting for the background service to start

Meanwhile the log accumulates dozens of entries with the real, actionable cause:

level=ERROR message="cli process failed" cause="Cause([Fail(Error: Managed service port 49374 on 127.0.0.1 is already in use by another process. Configure another port with `opencode service set port <port>` and start the service again. ...)])" args="[\"serve\",\"--service\"]" role=cli

Root Cause (code pointers)

  • packages/cli/src/server-process.ts:149 — the spawned serve --service process already detects EADDRINUSE and produces the helpful message, including the service set port hint.
  • packages/client/src/effect/service.ts — in ensure, a finished contender's failure is only returned when contenders.size === 0 (line 122). Because the loop keeps one candidate plus one lock probe in flight, each failure arrives while another contender is still pending, so the specific error is dropped. Once the attempt schedule is exhausted, line 138 throws the generic Timed out waiting for the background service to start with no cause.
  • Same pattern in packages/client/src/promise/service.ts:59.

Additional Context

  • 100% reproducible while the managed port is unbindable; resolves immediately once the port is freed.
  • Workarounds: opencode --standalone, or opencode service set port <free-port> then opencode service start.
  • Suggested fix: track the most recent contender failure in the ensure loop and attach it as cause to the final timeout error.
  • Secondary consideration: the managed service binds a fixed port inside the OS ephemeral range (49374 on macOS), so ordinary outbound connections from unrelated apps can coincidentally squat on it (as happened here). Detecting "port busy with no recognizable incumbent" and auto-selecting a replacement port — or at least hinting at it in the error — would make this self-healing.

Activity

  1. github-actions commented on Sep 14, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of existing issues. Please check:

    If your issue is distinct (e.g. the specific client-side ensure loop dropping contender errors), please clarify the difference from #41696 so maintainers can track separately.

  2. sghng commented on Sep 14, 2026

    @sghng
    Author

    Duplicate of #41696. Copied the release-channel (v2.0.3) reproduction, code-pointers, and stale-connection diagnostics into a comment there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions