Skip to content

fix(daemon): fall back to ephemeral port on conflict + harden teardown#386

Merged
harshitsinghbhandari merged 1 commit into
mainfrom
fix/daemon-port-fallback-and-teardown
Jun 22, 2026
Merged

fix(daemon): fall back to ephemeral port on conflict + harden teardown#386
harshitsinghbhandari merged 1 commit into
mainfrom
fix/daemon-port-fallback-and-teardown

Conversation

@harshitsinghbhandari

Copy link
Copy Markdown
Collaborator

Fixes #385.

The packaged desktop app could get permanently stuck on "AO daemon is not ready" because the bundled daemon never reached a ready state. Two related daemon-lifecycle problems caused this.

1. Daemon exits on port conflict instead of falling back

When the configured port (default 127.0.0.1:3001) was held by a non-AO process, the daemon returned a bind error and exited. The Electron supervisor saw the child exit, so the renderer's API base URL stayed null and every request returned {"message":"AO daemon is not ready."}.

NewWithDeps (backend/internal/httpd/server.go) now falls back to an OS-assigned ephemeral port when the configured one is in use (EADDRINUSE). A genuine peer AO daemon is already ruled out upstream by the running.json + /healthz check in daemon.Run, so a conflict at this point means a foreign holder. The daemon already logs the actual bound port (msg="daemon listening" addr=...) and writes running.json, both of which the supervisor reads (frontend/src/shared/daemon-discovery.ts), so the fallback port propagates to the renderer with no UI changes.

Non-EADDRINUSE bind errors still fail fast.

2. Detached daemon orphaned on quit

The supervisor spawns the daemon detached: true. before-quit already group-kills it, but app.exit() and some shutdown routes skip that event, so the detached daemon could survive and keep holding the port, wedging the next launch against problem #1.

A synchronous Node process.on("exit") handler now also group-kills the daemon, covering the paths before-quit misses. A hard SIGKILL/crash still can't run JS, but fix #1 covers the orphan that leaves behind.

Testing

  • go build ./... and go test -race ./... (1539 tests, 72 packages) pass.
  • TestNewFailsOnPortConflict is replaced by TestNewFallsBackOnPortConflict, asserting the second bind takes a different ephemeral port instead of erroring.
  • Frontend npm run typecheck passes.

🤖 Generated with Claude Code

Fixes the packaged desktop app getting permanently stuck on "AO daemon
is not ready" (#385) via two daemon-lifecycle fixes.

1. Port conflict no longer exits the daemon. When the configured port
   (default 127.0.0.1:3001) is held by a non-AO process, NewWithDeps now
   falls back to an OS-assigned ephemeral port instead of returning a bind
   error. A genuine peer AO daemon is already ruled out upstream (the
   running.json + /healthz check in daemon.Run), so a conflict here means a
   foreign holder. The bound port is logged ("daemon listening") and written
   to running.json, both of which the supervisor reads, so the fallback
   propagates to the renderer with no UI changes.

2. Detached daemon is torn down on more exit paths. before-quit already
   group-kills the daemon, but app.exit() and some shutdown routes skip it,
   orphaning the daemon so it keeps holding the port for the next launch. A
   synchronous process 'exit' handler now also signals the daemon's process
   group. A hard SIGKILL/crash still can't run JS, but fix #1 covers the
   orphan that leaves behind.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@harshitsinghbhandari harshitsinghbhandari merged commit ea54f31 into main Jun 22, 2026
10 checks passed
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.

Daemon never becomes ready (frontend stuck on "AO daemon is not ready"): exits on port conflict + orphaned on quit

1 participant