Skip to content

Commit c5aa837

Browse files
committed
feat(web-server): use dynamic port and return structured handle
Replace fixed port 3000 with OS-assigned port (port 0), eliminating port conflicts between multiple bridge instances. Return a WebServerHandle struct (server, controller, wss) instead of bare http.Server so callers can shut down all components. Remove the now-unnecessary isPortAlive() port probe and unused net import. Simplify runWeb() to reuse createWebServer(). Expose ChatController.meshStore for handle cleanup. Update all other bridges (claude-code, codex, mcp, opencode) to call tryStartWebServer() without await since it is now synchronous.
1 parent 63eccc3 commit c5aa837

6 files changed

Lines changed: 42 additions & 59 deletions

File tree

src/bridges/claude-code/channel.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ export async function run(): Promise<void> {
105105
// -----------------------------------------------------------------------
106106

107107
await store.init();
108-
await tryStartWebServer();
108+
tryStartWebServer();
109109
await mcp.connect(new StdioServerTransport());
110110

111111
const reg = await ensureRegistered({

src/bridges/codex/tool.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ export async function run(): Promise<void> {
9696
// -----------------------------------------------------------------------
9797

9898
await store.init();
99-
await tryStartWebServer();
99+
tryStartWebServer();
100100
await mcp.connect(new StdioServerTransport());
101101

102102
const reg = await ensureRegistered({

src/bridges/mcp/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ export async function run(): Promise<void> {
9797
// -----------------------------------------------------------------------
9898

9999
await store.init();
100-
await tryStartWebServer();
100+
tryStartWebServer();
101101
await mcp.connect(new StdioServerTransport());
102102

103103
const reg = await ensureRegistered({

src/bridges/opencode/plugin.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export const AgentCommsPlugin = async (opts: {
5353
const client = opts.client;
5454

5555
await store.init();
56-
await tryStartWebServer();
56+
tryStartWebServer();
5757

5858
const reg = await ensureRegistered({
5959
cwd: process.cwd(),

src/bridges/user/controller.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ import type {
2626

2727
export class ChatController extends EventEmitter {
2828
private store: MeshStore;
29+
30+
/** Expose the underlying MeshStore for handle cleanup. */
31+
get meshStore(): MeshStore {
32+
return this.store;
33+
}
2934
private tool: CommsTool;
3035
private ctx!: CommsContext;
3136
private currentRoom: string | undefined;

src/bridges/user/web/server.ts

Lines changed: 33 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -17,39 +17,39 @@
1717
*/
1818

1919
import * as http from "node:http";
20-
import * as net from "node:net";
2120
import { WebSocketServer, type WebSocket } from "ws";
2221
import { ChatController } from "../controller.js";
2322
import { FRONTEND_HTML } from "./index.html.js";
2423

25-
const DEFAULT_WEB_PORT = 3000;
2624
const WEB_HOST = "127.0.0.1";
2725

26+
// ---------------------------------------------------------------------------
27+
// Types
28+
// ---------------------------------------------------------------------------
29+
30+
export interface WebServerHandle {
31+
server: http.Server;
32+
controller: ChatController;
33+
wss: WebSocketServer;
34+
}
35+
2836
// ---------------------------------------------------------------------------
2937
// Auto-start — called by every bridge after MeshStore.init()
3038
// ---------------------------------------------------------------------------
3139

3240
/**
33-
* Try to start the web UI server on the well-known port.
34-
* Returns the server if this bridge won the race, or undefined if another
35-
* bridge is already serving. The server runs independently with its own
36-
* MeshStore that syncs state from the mesh.
41+
* Start the web UI server on an OS-assigned port.
42+
* Returns the server handle, or undefined if port discovery fails.
3743
*/
38-
export async function tryStartWebServer(
39-
port = DEFAULT_WEB_PORT,
40-
): Promise<http.Server | undefined> {
41-
const alive = await isPortAlive(port, WEB_HOST);
42-
if (alive) return undefined;
43-
44-
const server = createWebServer(port);
45-
return server;
44+
export function tryStartWebServer(): WebServerHandle | undefined {
45+
return createWebServer();
4646
}
4747

4848
/**
49-
* Create and start the web server. Used by tryStartWebServer (auto-start)
50-
* and runWeb (standalone `npx agent-comms chat` mode).
49+
* Create and start the web server on a dynamic port.
50+
* Used by tryStartWebServer (auto-start) and runWeb (standalone mode).
5151
*/
52-
export function createWebServer(port = DEFAULT_WEB_PORT): http.Server {
52+
export function createWebServer(port = 0): WebServerHandle {
5353
const controller = new ChatController("Dashboard");
5454
void controller.init();
5555

@@ -63,57 +63,35 @@ export function createWebServer(port = DEFAULT_WEB_PORT): http.Server {
6363
});
6464

6565
server.listen(port, WEB_HOST, () => {
66-
console.log(`Agent Comms web UI: http://${WEB_HOST}:${String(port)}`);
66+
const addr = server.address();
67+
const actualPort = typeof addr === "object" && addr ? addr.port : port;
68+
console.log(`Agent Comms web UI: http://${WEB_HOST}:${String(actualPort)}`);
6769
});
6870

69-
return server;
70-
}
71-
72-
function isPortAlive(port: number, host: string): Promise<boolean> {
73-
return new Promise((resolve) => {
74-
const socket = new net.Socket();
75-
socket.connect(port, host, () => {
76-
socket.destroy();
77-
resolve(true);
78-
});
79-
socket.on("error", () => {
80-
socket.destroy();
81-
resolve(false);
82-
});
83-
});
71+
return { server, controller, wss };
8472
}
8573

8674
// ---------------------------------------------------------------------------
8775
// Standalone mode — `npx agent-comms chat`
8876
// ---------------------------------------------------------------------------
8977

90-
export async function runWeb(
91-
userName: string,
92-
port = DEFAULT_WEB_PORT,
93-
): Promise<void> {
94-
const controller = new ChatController(userName);
95-
await controller.init();
96-
97-
const server = http.createServer((req, res) => {
98-
handleRequest(req, res, controller);
99-
});
100-
101-
const wss = new WebSocketServer({ server });
102-
103-
wss.on("connection", (ws) => {
104-
handleWebSocket(ws, controller);
105-
});
78+
export function runWeb(userName: string, port = 0): void {
79+
const handle = createWebServer(port);
10680

107-
server.listen(port, () => {
108-
console.log(`Agent Comms web UI: http://localhost:${String(port)}`);
109-
console.log(`Connected as ${userName} (user) [${controller.agentId}]`);
81+
handle.server.on("listening", () => {
82+
const addr = handle.server.address();
83+
const actualPort = typeof addr === "object" && addr ? addr.port : port;
84+
console.log(`Agent Comms web UI: http://localhost:${String(actualPort)}`);
85+
console.log(
86+
`Connected as ${userName} (user) [${handle.controller.agentId}]`,
87+
);
11088
});
11189

11290
// Graceful shutdown
11391
const cleanup = async (): Promise<void> => {
114-
wss.close();
115-
server.close();
116-
await controller.shutdown();
92+
handle.wss.close();
93+
handle.server.close();
94+
await handle.controller.shutdown();
11795
process.exit(0);
11896
};
11997

0 commit comments

Comments
 (0)