Skip to content

Daemon binds a different port on every config reload; reload rebinds even when the listener is unaffected #44

Description

@vnovick

Summary

With server.port unset (the default), every WORKFLOW.md change makes the daemon bind a different port. Open dashboards break, bookmarks go stale, and the Vite dev proxy points at nothing. Editing several settings in sequence used to compound it: each save triggered its own reload.

Observed on v0.2.0 (6d1c67c).

Root cause

cmd/itervox/main.go:649-656:

port := 0
if cfg.Server.Port != nil {
    port = *cfg.Server.Port
}
srvListener, actualAddr, err = listenStrict(cfg.Server.Host, port)

server.port is *int (internal/config/config.go:313) with no default, so when it is unset port stays 0 and net.Listen picks an ephemeral port.

That binding lives inside run(), and main.go:484 re-invokes run() on every reload:

runDone := make(chan error, 1)
go func() {
    runDone <- run(runCtx, cancel, cfg, *workflowPath, ...)
}()

So a reload does not merely rebind the listener — it re-rolls the port. The listener, the HTTP server and the dashboard URL file are all torn down and recreated.

Two separate problems

1. The port is not stable across reloads. Ephemeral-by-default means "different port every reload", which is a surprising default for a long-running local daemon whose whole interface is a browser tab.

2. A reload rebinds even when nothing about the server changed. Editing agent.max_turns has no bearing on the listener, but the listener goes down and back up regardless, dropping in-flight requests and SSE streams.

Proposed fix

  • Hoist the listener above the reload loop. Bind once at startup; on reload, reuse the existing listener unless the resolved port actually changed. Keeps the port stable and stops dropping connections on unrelated config edits.
  • Give server.port a fixed default instead of ephemeral, so a fresh install is predictable.

Note this interacts with an existing documented workflow. listenStrict's own error hint (main.go:658-659) currently says:

to run two itervox daemons in parallel, set server.port: 0 in WORKFLOW.md to let the OS pick a free port, or set a distinct explicit port per repo

A fixed default makes the multi-daemon case require an explicit per-repo server.port (or an explicit 0). That is a real ergonomic cost for anyone running itervox against several repositories, so the address-in-use error must stay loud and keep pointing at that knob.

Partially addressed already

The compounding half is fixed: internal/workflow/watcher.go now debounces, waiting for WORKFLOW.md to stop changing for 2s before firing onChange, so a burst of saves coalesces into one reload instead of one per save. Each further change restarts the quiet period.

That reduces the frequency but not the underlying behaviour — a single genuine config change still re-rolls the port. This issue tracks the remaining half.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions