Move Agent Web Socket Server into singleton to avoid port collisions#2171
Move Agent Web Socket Server into singleton to avoid port collisions#2171GeorgeNgMsft wants to merge 16 commits intomainfrom
Conversation
Set exit code instead of calling exit directly. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…be a critical exception
…icrosoft/TypeAgent into dev/georgeng/fix_as_easddr_use
…, rather than naive incremental retries
…icrosoft/TypeAgent into dev/georgeng/fix_as_easddr_use
There was a problem hiding this comment.
Pull request overview
This PR aims to prevent port-collision crashes when multiple agent-cli clients connect concurrently by (1) sharing the browser agent’s AgentWebSocketServer as a singleton per agent process and (2) switching local view servers to OS-assigned ports (listen(0)) with the bound port reported back to the dispatcher via IPC.
Changes:
- Removes
portBase-based port assignment and instead reserves port0formanifest.localViewagents, with bound ports reported back viaSessionContext.setLocalHostPort(). - Promotes the browser agent’s
AgentWebSocketServer(8081)to a module-level singleton and wires it into browser session contexts. - Updates browser/markdown view server startup IPC to return the bound port (and updates callers to persist it back to the dispatcher).
Reviewed changes
Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| ts/packages/shell/src/main/instance.ts | Removes portBase option usage when creating the dispatcher. |
| ts/packages/dispatcher/dispatcher/src/execute/sessionContext.ts | Adds SessionContext.setLocalHostPort() for agents to report OS-assigned ports. |
| ts/packages/dispatcher/dispatcher/src/context/commandHandlerContext.ts | Updates docs/types to remove portBase and document OS-assigned port flow. |
| ts/packages/dispatcher/dispatcher/src/context/appAgentManager.ts | Switches localView “reserved port” to 0 and adds setLocalHostPort() storage. |
| ts/packages/agentSdk/src/agentInterface.ts | Extends SessionContext with setLocalHostPort(port). |
| ts/packages/agents/taskflow/benchmark/run-benchmark.mts | Removes portBase usage in benchmark dispatcher creation. |
| ts/packages/agents/scriptflow/benchmark/run-benchmark.mts | Removes portBase usage in benchmark dispatcher creation. |
| ts/packages/agents/montage/src/route/route.ts | Adds server error handling; still starts the HTTP server for montage local view. |
| ts/packages/agents/montage/src/agent/montageActionHandler.ts | Ensures montage view child process is killed on context close. |
| ts/packages/agents/markdown/src/view/route/service.ts | Reports OS-bound port to parent via IPC and adds startup error handling. |
| ts/packages/agents/markdown/src/agent/markdownActionHandler.ts | Consumes IPC {type:'Success', port} and persists port back via setLocalHostPort(). |
| ts/packages/agents/browser/src/views/server/server.mts | Reports OS-bound port to parent via IPC {type:'Success', port}. |
| ts/packages/agents/browser/src/views/server/core/baseServer.ts | Captures and exposes the bound port after listen, improves startup/runtime error handling. |
| ts/packages/agents/browser/src/agent/websiteMemory.mts | Updates a SessionContext stub to include setLocalHostPort (currently with a type mismatch). |
| ts/packages/agents/browser/src/agent/browserActionHandler.mts | Introduces module-level AgentWebSocketServer singleton and adds closeAgentContext. |
| ts/packages/agents/browser/benchmark/test-webflow-grammar.mts | Removes portBase usage in benchmark dispatcher creation. |
| ts/packages/agentRpc/src/types.ts | Adds setLocalHostPort RPC method to agent RPC surface. |
| ts/packages/agentRpc/src/server.ts | Exposes SessionContext.setLocalHostPort() through RPC. |
| ts/packages/agentRpc/src/client.ts | Implements setLocalHostPort invoke handler to call session context method. |
| ts/docs/architecture/dispatcher.md | Documents OS-assigned local view port flow and setLocalHostPort() reporting. |
| ts/.vscode/settings.json | Adds watcher/search excludes for node_modules and dist. |
Comments suppressed due to low confidence (1)
ts/packages/agents/browser/src/agent/browserActionHandler.mts:552
- closeBrowserContext() stops the AgentWebSocketServer, but the server is a module-level singleton shared by all session contexts. Closing any one session would shut down the shared WebSocket server and break other active sessions. Consider not stopping the singleton here (or add ref-counting / process-lifetime ownership so the server only stops when the agent process exits).
async function closeBrowserContext(
context: SessionContext<BrowserActionContext>,
) {
if (context.agentContext.agentWebSocketServer) {
context.agentContext.agentWebSocketServer.stop();
delete context.agentContext.agentWebSocketServer;
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Register agentRpc invoke handlers for channel-multiplexed messages | ||
| context.agentContext.agentWebSocketServer.setAgentInvokeHandlers( | ||
| createAgentInvokeHandlers(context), | ||
| ); | ||
|
|
There was a problem hiding this comment.
AgentWebSocketServer is now a process-wide singleton, but updateBrowserContext() sets its invoke handlers and connection/message callbacks using the current SessionContext. With multiple sessions, each enable will overwrite these callbacks, causing client traffic to be handled by the wrong session context. To support concurrent sessions, keep per-session handlers in a map keyed by session/client identity, or move these handlers into the singleton and route based on an explicit session identifier.
The browser agent's AgentWebSocketServer was being instantiated once per session context (initializeAgentContext call). When multiple agent-cli clients connected to the agent-server simultaneously, each triggered a separate initializeAgentContext RPC call on the shared browser agent process, resulting in two new AgentWebSocketServer(8081) calls and an EADDRINUSE crash on the second one.
The root cause is that the browser agent runs as a single out-of-process RPC server (agentProcess.ts) shared across all sessions, but session contexts are created independently per session — there was no deduplication of the port bind.
Fix: promote AgentWebSocketServer to a module-level singleton in browserActionHandler.mts, constructed once when the agent process loads. All session contexts reference the same instance. Construction was also moved from updateAgentContext (called once per action schema enable, raceable) to initializeAgentContext (called once per session, guarded by sessionContextP).