diff --git a/src/core/mesh-store.ts b/src/core/mesh-store.ts index 2fdb52c8..a5e01eb6 100644 --- a/src/core/mesh-store.ts +++ b/src/core/mesh-store.ts @@ -341,6 +341,7 @@ export class MeshStore implements CommsStore { ); connected = true; } catch { + let eaddrinuseMessage: string | undefined; try { await this.requireTransport().becomeCoordinator( COORDINATOR_HOST, @@ -354,18 +355,21 @@ export class MeshStore implements CommsStore { if (!msg.includes("EADDRINUSE")) { throw coordErr; } - // EADDRINUSE — port held by an unresponsive process. Degrade. + // EADDRINUSE — connectToCoordinator already failed above, so whatever holds this port never answered as a reachable coordinator either. Degrade, naming the actual mismatch rather than a generic "unavailable". + eaddrinuseMessage = msg; } - } - if (!connected) { - this.events.onError?.( - new Error( - `MeshStore: could not join or create mesh on port ${String(this.coordinatorPort)}. ` + - "Running without mesh — agent-comms will be unavailable.", - ), - ); - return; + if (!connected) { + this.events.onError?.( + new Error( + `MeshStore: could not join or create a mesh on port ${String(this.coordinatorPort)}. ` + + `port ${String(this.coordinatorPort)} is already in use by something that never answered as a reachable coordinator -- a stale process from a previous run, or an incompatible agent-comms version. ` + + "agent-comms will run without mesh connectivity until this is resolved. " + + `(${eaddrinuseMessage ?? "unknown reason"})`, + ), + ); + return; + } } this.requireTransport().unref(); diff --git a/src/test/mesh-store-orchestration.test.ts b/src/test/mesh-store-orchestration.test.ts index e1f8a058..fb81c801 100644 --- a/src/test/mesh-store-orchestration.test.ts +++ b/src/test/mesh-store-orchestration.test.ts @@ -229,12 +229,15 @@ describe("MeshStore — init()", () => { await expect(store.init()).resolves.toBeUndefined(); expect(onError).toHaveBeenCalledTimes(1); - expect(onError.mock.calls[0]?.[0]?.message).toContain( - "could not join or create mesh", - ); - expect(onError.mock.calls[0]?.[0]?.message).toContain( - "Running without mesh — agent-comms will be unavailable.", + const message = onError.mock.calls[0]?.[0]?.message ?? ""; + expect(message).toContain("could not join or create a mesh"); + // The message must name the actual mismatch, not just say the mesh is unavailable: something already holds the port but never answered as a reachable coordinator (connectToCoordinator already failed first), which is a stale process or an incompatible agent-comms version -- not a generic catch-all. + expect(message).toContain( + "port 19876 is already in use by something that never answered as a reachable coordinator", ); + expect(message).toContain("stale process"); + expect(message).toContain("incompatible agent-comms version"); + expect(message).toContain("listen EADDRINUSE: address already in use"); expect(transport.unref).not.toHaveBeenCalled(); });