From 4888bd41c506868c03958715c8cd0ecbadfe1830 Mon Sep 17 00:00:00 2001 From: George Ng Date: Tue, 7 Apr 2026 16:52:13 -0700 Subject: [PATCH 01/11] Improve cleanup logic for browserActionHandler + montageActionHandler --- .../browser/src/agent/browserActionHandler.mts | 16 ++++++++++++++++ .../browser/src/views/server/core/baseServer.ts | 15 +++++++++++++-- .../montage/src/agent/montageActionHandler.ts | 4 ++++ ts/packages/agents/montage/src/route/route.ts | 15 +++++++++++++-- 4 files changed, 46 insertions(+), 4 deletions(-) diff --git a/ts/packages/agents/browser/src/agent/browserActionHandler.mts b/ts/packages/agents/browser/src/agent/browserActionHandler.mts index c3d7803e55..37f4463ceb 100644 --- a/ts/packages/agents/browser/src/agent/browserActionHandler.mts +++ b/ts/packages/agents/browser/src/agent/browserActionHandler.mts @@ -259,6 +259,7 @@ export function instantiate(): AppAgent { return { initializeAgentContext: initializeBrowserContext, updateAgentContext: updateBrowserContext, + closeAgentContext: closeBrowserContext, executeAction: executeBrowserAction, resolveEntity, getDynamicDisplay: getDynamicDisplayImpl, @@ -540,6 +541,21 @@ async function updateBrowserContext( } } +async function closeBrowserContext( + context: SessionContext, +) { + if (context.agentContext.agentWebSocketServer) { + context.agentContext.agentWebSocketServer.stop(); + delete context.agentContext.agentWebSocketServer; + } + if (context.agentContext.browserProcess) { + context.agentContext.browserProcess.kill(); + } + if (context.agentContext.viewProcess) { + context.agentContext.viewProcess.kill(); + } +} + async function handleWebAgentRpc( method: string, params: any, diff --git a/ts/packages/agents/browser/src/views/server/core/baseServer.ts b/ts/packages/agents/browser/src/views/server/core/baseServer.ts index b009d9909a..56d122334e 100644 --- a/ts/packages/agents/browser/src/views/server/core/baseServer.ts +++ b/ts/packages/agents/browser/src/views/server/core/baseServer.ts @@ -146,14 +146,25 @@ export class BaseServer { * Start the server */ start(): Promise { - return new Promise((resolve) => { - this.app.listen(this.config.port, () => { + return new Promise((resolve, reject) => { + const server = this.app.listen(this.config.port, () => { debug(`Server running at http://localhost:${this.config.port}`); debug( `Registered features: ${Array.from(this.features.keys()).join(", ")}`, ); resolve(); }); + server.on("error", (err: NodeJS.ErrnoException) => { + if (err.code === "EADDRINUSE") { + reject( + new Error( + `Port ${this.config.port} is already in use. Is another instance already running?`, + ), + ); + } else { + reject(err); + } + }); }); } } diff --git a/ts/packages/agents/montage/src/agent/montageActionHandler.ts b/ts/packages/agents/montage/src/agent/montageActionHandler.ts index 9d91eccbd3..6a1001e057 100644 --- a/ts/packages/agents/montage/src/agent/montageActionHandler.ts +++ b/ts/packages/agents/montage/src/agent/montageActionHandler.ts @@ -230,6 +230,10 @@ async function closeMontageContext( context: SessionContext, ) { await saveMontages(context); + if (context.agentContext.viewProcess) { + context.agentContext.viewProcess.kill(); + context.agentContext.viewProcess = undefined; + } } async function updateMontageContext( diff --git a/ts/packages/agents/montage/src/route/route.ts b/ts/packages/agents/montage/src/route/route.ts index e451ff06c5..1938e84545 100644 --- a/ts/packages/agents/montage/src/route/route.ts +++ b/ts/packages/agents/montage/src/route/route.ts @@ -278,5 +278,16 @@ process.on("disconnect", () => { }); // Start the server -app.listen(port); -debug(`Montage server started on port ${port}`); +const server = app.listen(port, () => { + debug(`Montage server started on port ${port}`); +}); +server.on("error", (err: NodeJS.ErrnoException) => { + if (err.code === "EADDRINUSE") { + console.error( + `Port ${port} is already in use. Is another instance already running?`, + ); + } else { + console.error(`Server error: ${err.message}`); + } + process.exit(1); +}); From caa457361791087a9e215a98dbce6738dc1d73c2 Mon Sep 17 00:00:00 2001 From: George Ng <146492653+GeorgeNgMsft@users.noreply.github.com> Date: Wed, 8 Apr 2026 14:13:05 -0700 Subject: [PATCH 02/11] Update ts/packages/agents/montage/src/route/route.ts Set exit code instead of calling exit directly. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- ts/packages/agents/montage/src/route/route.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ts/packages/agents/montage/src/route/route.ts b/ts/packages/agents/montage/src/route/route.ts index 1938e84545..7424165b9c 100644 --- a/ts/packages/agents/montage/src/route/route.ts +++ b/ts/packages/agents/montage/src/route/route.ts @@ -289,5 +289,5 @@ server.on("error", (err: NodeJS.ErrnoException) => { } else { console.error(`Server error: ${err.message}`); } - process.exit(1); + process.exitCode = 1; }); From 1657fc70ef6fffff4133a0d138eab3faf559ce5a Mon Sep 17 00:00:00 2001 From: George Ng Date: Wed, 8 Apr 2026 15:58:38 -0700 Subject: [PATCH 03/11] Remove nodemodules/gitobjects/dist from filewatcher and search --- ts/.vscode/settings.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/ts/.vscode/settings.json b/ts/.vscode/settings.json index 2ff2419e62..d7a690c1a8 100644 --- a/ts/.vscode/settings.json +++ b/ts/.vscode/settings.json @@ -49,5 +49,14 @@ "jest.jestCommandLine": "node --no-warnings --experimental-vm-modules ./node_modules/jest/bin/jest.js", "chat.tools.terminal.autoApprove": { "ForEach-Object": true + }, + "files.watcherExclude": { + "**/node_modules/**": true, + "**/.git/objects/**": true, + "**/dist/**": true + }, + "search.exclude": { + "**/node_modules": true, + "**/dist": true } } \ No newline at end of file From c9fd046129d0b94a31066ed37af7b841b0db0f27 Mon Sep 17 00:00:00 2001 From: George Ng Date: Wed, 8 Apr 2026 16:03:59 -0700 Subject: [PATCH 04/11] Address listener comment and process cleanup --- .../agents/browser/src/agent/browserActionHandler.mts | 2 ++ .../agents/browser/src/views/server/core/baseServer.ts | 9 +++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/ts/packages/agents/browser/src/agent/browserActionHandler.mts b/ts/packages/agents/browser/src/agent/browserActionHandler.mts index 37f4463ceb..abee0a3b6b 100644 --- a/ts/packages/agents/browser/src/agent/browserActionHandler.mts +++ b/ts/packages/agents/browser/src/agent/browserActionHandler.mts @@ -550,9 +550,11 @@ async function closeBrowserContext( } if (context.agentContext.browserProcess) { context.agentContext.browserProcess.kill(); + context.agentContext.browserProcess = undefined; } if (context.agentContext.viewProcess) { context.agentContext.viewProcess.kill(); + context.agentContext.viewProcess = undefined; } } diff --git a/ts/packages/agents/browser/src/views/server/core/baseServer.ts b/ts/packages/agents/browser/src/views/server/core/baseServer.ts index 56d122334e..10acaa20d9 100644 --- a/ts/packages/agents/browser/src/views/server/core/baseServer.ts +++ b/ts/packages/agents/browser/src/views/server/core/baseServer.ts @@ -152,9 +152,13 @@ export class BaseServer { debug( `Registered features: ${Array.from(this.features.keys()).join(", ")}`, ); + server.removeListener("error", onStartupError); + server.on("error", (err: NodeJS.ErrnoException) => { + debug(`Server runtime error: ${err.message}`); + }); resolve(); }); - server.on("error", (err: NodeJS.ErrnoException) => { + const onStartupError = (err: NodeJS.ErrnoException) => { if (err.code === "EADDRINUSE") { reject( new Error( @@ -164,7 +168,8 @@ export class BaseServer { } else { reject(err); } - }); + }; + server.on("error", onStartupError); }); } } From c632b85831cfcfcf57d089322d0760d0ae8c80a5 Mon Sep 17 00:00:00 2001 From: George Ng Date: Wed, 8 Apr 2026 16:08:40 -0700 Subject: [PATCH 05/11] Change runtime error from debug message to console error as it would be a critical exception --- .../agents/browser/src/views/server/core/baseServer.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ts/packages/agents/browser/src/views/server/core/baseServer.ts b/ts/packages/agents/browser/src/views/server/core/baseServer.ts index 10acaa20d9..b2dd13ed30 100644 --- a/ts/packages/agents/browser/src/views/server/core/baseServer.ts +++ b/ts/packages/agents/browser/src/views/server/core/baseServer.ts @@ -154,7 +154,10 @@ export class BaseServer { ); server.removeListener("error", onStartupError); server.on("error", (err: NodeJS.ErrnoException) => { - debug(`Server runtime error: ${err.message}`); + console.error( + `Server runtime error on port ${this.config.port}:`, + err, + ); }); resolve(); }); From 7759eb65c7053b4dc5cc03bc3bb0bf5fd2f7d662 Mon Sep 17 00:00:00 2001 From: George Ng Date: Wed, 8 Apr 2026 16:34:33 -0700 Subject: [PATCH 06/11] Allow clients to dynamically retrieve port assignment from dispatcher, rather than naive incremental retries --- ts/packages/agentRpc/src/client.ts | 7 ++ ts/packages/agentRpc/src/server.ts | 3 + ts/packages/agentRpc/src/types.ts | 4 + ts/packages/agentSdk/src/agentInterface.ts | 3 + .../benchmark/test-webflow-grammar.mts | 1 - .../src/agent/browserActionHandler.mts | 6 +- .../src/views/server/core/baseServer.ts | 13 ++- .../browser/src/views/server/server.mts | 2 +- .../src/agent/markdownActionHandler.ts | 90 ++++++++++--------- .../agents/markdown/src/view/route/service.ts | 17 +++- .../scriptflow/benchmark/run-benchmark.mts | 1 - .../taskflow/benchmark/run-benchmark.mts | 1 - .../dispatcher/src/context/appAgentManager.ts | 14 +-- .../src/context/commandHandlerContext.ts | 6 +- .../dispatcher/src/execute/sessionContext.ts | 3 + ts/packages/shell/src/main/instance.ts | 1 - 16 files changed, 109 insertions(+), 63 deletions(-) diff --git a/ts/packages/agentRpc/src/client.ts b/ts/packages/agentRpc/src/client.ts index 8ded248adc..d8f968c4e0 100644 --- a/ts/packages/agentRpc/src/client.ts +++ b/ts/packages/agentRpc/src/client.ts @@ -277,6 +277,13 @@ export async function createAgentRpcClient( const context = contextMap.get(param.contextId); return context.getSharedLocalHostPort(param.agentName); }, + setLocalHostPort: async (param: { + contextId: number; + port: number; + }) => { + const context = contextMap.get(param.contextId); + context.setLocalHostPort(param.port); + }, indexes: async (param: { contextId: number; type: string }) => { const context = contextMap.get(param.contextId); return context.indexes(param.type as any); diff --git a/ts/packages/agentRpc/src/server.ts b/ts/packages/agentRpc/src/server.ts index d4e530f7f4..7f5811b14d 100644 --- a/ts/packages/agentRpc/src/server.ts +++ b/ts/packages/agentRpc/src/server.ts @@ -450,6 +450,9 @@ export function createAgentRpcServer( agentName, }); }, + setLocalHostPort(port: number) { + rpc.send("setLocalHostPort", { contextId, port }); + }, addDynamicAgent: async ( name: string, manifest: AppAgentManifest, diff --git a/ts/packages/agentRpc/src/types.ts b/ts/packages/agentRpc/src/types.ts index e91bf5be03..a6861efc6b 100644 --- a/ts/packages/agentRpc/src/types.ts +++ b/ts/packages/agentRpc/src/types.ts @@ -121,6 +121,10 @@ export type AgentContextInvokeFunctions = { contextId: number; agentName: string; }) => Promise; + setLocalHostPort: (param: { + contextId: number; + port: number; + }) => Promise; indexes: (param: { contextId: number; type: string }) => Promise; reloadAgentSchema: (param: { contextId: number }) => Promise; popupQuestion: (param: { diff --git a/ts/packages/agentSdk/src/agentInterface.ts b/ts/packages/agentSdk/src/agentInterface.ts index 17e4490660..c05d4a1036 100644 --- a/ts/packages/agentSdk/src/agentInterface.ts +++ b/ts/packages/agentSdk/src/agentInterface.ts @@ -227,6 +227,9 @@ export interface SessionContext { // Experimental: get the shared local host port getSharedLocalHostPort(agentName: string): Promise; + // Experimental: update this agent's bound local host port (used after OS port assignment) + setLocalHostPort(port: number): void; + // Experimental: get the available indexes indexes(type: "image" | "email" | "website" | "all"): Promise; } diff --git a/ts/packages/agents/browser/benchmark/test-webflow-grammar.mts b/ts/packages/agents/browser/benchmark/test-webflow-grammar.mts index f36fad8049..80ecb14279 100644 --- a/ts/packages/agents/browser/benchmark/test-webflow-grammar.mts +++ b/ts/packages/agents/browser/benchmark/test-webflow-grammar.mts @@ -85,7 +85,6 @@ async function main() { agents: { actions: true, commands: true }, execution: { history: false }, collectCommandResult: true, - portBase: 9400, persistDir, storageProvider: getFsStorageProvider(), }); diff --git a/ts/packages/agents/browser/src/agent/browserActionHandler.mts b/ts/packages/agents/browser/src/agent/browserActionHandler.mts index abee0a3b6b..6bfbb2c717 100644 --- a/ts/packages/agents/browser/src/agent/browserActionHandler.mts +++ b/ts/packages/agents/browser/src/agent/browserActionHandler.mts @@ -1984,8 +1984,10 @@ async function createViewServiceHost( }, }); - childProcess.on("message", function (message) { - if (message === "Success") { + childProcess.on("message", function (message: any) { + if (message?.type === "Success") { + context.agentContext.localHostPort = message.port; + context.setLocalHostPort(message.port); resolve(childProcess); } else if (message === "Failure") { resolve(undefined); diff --git a/ts/packages/agents/browser/src/views/server/core/baseServer.ts b/ts/packages/agents/browser/src/views/server/core/baseServer.ts index b2dd13ed30..9474391730 100644 --- a/ts/packages/agents/browser/src/views/server/core/baseServer.ts +++ b/ts/packages/agents/browser/src/views/server/core/baseServer.ts @@ -23,6 +23,7 @@ export class BaseServer { private sseManager: SSEManager; private features: Map = new Map(); private config: ServerConfig; + private boundPort: number | undefined; constructor(config: ServerConfig) { this.config = config; @@ -142,20 +143,28 @@ export class BaseServer { return this.app; } + get port(): number { + if (this.boundPort === undefined) { + throw new Error("Server has not been started yet."); + } + return this.boundPort; + } + /** * Start the server */ start(): Promise { return new Promise((resolve, reject) => { const server = this.app.listen(this.config.port, () => { - debug(`Server running at http://localhost:${this.config.port}`); + this.boundPort = (server.address() as { port: number }).port; + debug(`Server running at http://localhost:${this.boundPort}`); debug( `Registered features: ${Array.from(this.features.keys()).join(", ")}`, ); server.removeListener("error", onStartupError); server.on("error", (err: NodeJS.ErrnoException) => { console.error( - `Server runtime error on port ${this.config.port}:`, + `Server runtime error on port ${this.boundPort}:`, err, ); }); diff --git a/ts/packages/agents/browser/src/views/server/server.mts b/ts/packages/agents/browser/src/views/server/server.mts index f039246f19..754267b52d 100644 --- a/ts/packages/agents/browser/src/views/server/server.mts +++ b/ts/packages/agents/browser/src/views/server/server.mts @@ -34,7 +34,7 @@ async function main() { await server.start(); // Process lifecycle management - process.send?.("Success"); + process.send?.({ type: "Success", port: server.port }); process.on("message", (message: any) => { debug("Received message:", message); diff --git a/ts/packages/agents/markdown/src/agent/markdownActionHandler.ts b/ts/packages/agents/markdown/src/agent/markdownActionHandler.ts index e4345e8d04..a7f7255d32 100644 --- a/ts/packages/agents/markdown/src/agent/markdownActionHandler.ts +++ b/ts/packages/agents/markdown/src/agent/markdownActionHandler.ts @@ -297,10 +297,15 @@ async function updateMarkdownContext( const fullPath = await getFullMarkdownFilePath(fileName, storage!); if (fullPath) { process.env.MARKDOWN_FILE = fullPath; - context.agentContext.viewProcess = await createViewServiceHost( + const result = await createViewServiceHost( fullPath, context.agentContext.localHostPort, ); + if (result) { + context.agentContext.viewProcess = result.process; + context.agentContext.localHostPort = result.port; + context.setLocalHostPort(result.port); + } } } @@ -878,7 +883,10 @@ async function getDocumentContentFromView( // NOTE: Function commented out per Flow 1 consolidation // Collaboration server now managed by view process -async function createViewServiceHost(filePath: string, port: number) { +async function createViewServiceHost( + filePath: string, + port: number, +): Promise<{ process: ChildProcess; port: number } | undefined> { let timeoutHandle: NodeJS.Timeout; const timeoutPromise = new Promise((_resolve, reject) => { @@ -888,47 +896,47 @@ async function createViewServiceHost(filePath: string, port: number) { }, 10000); }); - const viewServicePromise = new Promise( - (resolve, reject) => { - try { - const expressService = fileURLToPath( - new URL( - path.join("..", "./view/route/service.js"), - import.meta.url, - ), - ); + const viewServicePromise = new Promise< + { process: ChildProcess; port: number } | undefined + >((resolve, reject) => { + try { + const expressService = fileURLToPath( + new URL( + path.join("..", "./view/route/service.js"), + import.meta.url, + ), + ); - const folderPath = path.dirname(filePath!); + const folderPath = path.dirname(filePath!); - const childProcess = fork(expressService, [port.toString()], { - env: { - ...process.env, - TYPEAGENT_MARKDOWN_ROOT: folderPath, - }, - }); - - childProcess.send({ - type: "setFile", - filePath: path.basename(filePath), - }); - - childProcess.on("message", function (message: any) { - if (message === "Success") { - resolve(childProcess); - } else if (message === "Failure") { - resolve(undefined); - } - }); - - childProcess.on("exit", (code) => { - debug("Markdown view server exited with code:", code); - }); - } catch (e: any) { - console.error(e); - resolve(undefined); - } - }, - ); + const childProcess = fork(expressService, [port.toString()], { + env: { + ...process.env, + TYPEAGENT_MARKDOWN_ROOT: folderPath, + }, + }); + + childProcess.send({ + type: "setFile", + filePath: path.basename(filePath), + }); + + childProcess.on("message", function (message: any) { + if (message?.type === "Success") { + resolve({ process: childProcess, port: message.port }); + } else if (message === "Failure") { + resolve(undefined); + } + }); + + childProcess.on("exit", (code) => { + debug("Markdown view server exited with code:", code); + }); + } catch (e: any) { + console.error(e); + resolve(undefined); + } + }); return Promise.race([viewServicePromise, timeoutPromise]).then((result) => { clearTimeout(timeoutHandle); diff --git a/ts/packages/agents/markdown/src/view/route/service.ts b/ts/packages/agents/markdown/src/view/route/service.ts index 5552d0691a..e611c764e0 100644 --- a/ts/packages/agents/markdown/src/view/route/service.ts +++ b/ts/packages/agents/markdown/src/view/route/service.ts @@ -2245,9 +2245,20 @@ debug(`[SIGNAL] Y.js WebSocket server integrated`); // Start the HTTP server (which includes WebSocket support) server.listen(port, () => { - debug(`Express server with WebSocket support listening on port ${port}`); - debug(`Y.js collaboration available at ws://localhost:${port}/`); + const boundPort = (server.address() as { port: number }).port; + debug( + `Express server with WebSocket support listening on port ${boundPort}`, + ); + debug( + `Y.js collaboration available at ws://localhost:${boundPort}/`, + ); // Send success signal to parent process AFTER server is ready to accept WebSocket connections - process.send?.("Success"); + process.send?.({ type: "Success", port: boundPort }); +}); + +server.on("error", (err: NodeJS.ErrnoException) => { + console.error("Markdown view server failed to start:", err); + process.send?.("Failure"); + process.exit(1); }); diff --git a/ts/packages/agents/scriptflow/benchmark/run-benchmark.mts b/ts/packages/agents/scriptflow/benchmark/run-benchmark.mts index a2e98605d3..6454de78af 100644 --- a/ts/packages/agents/scriptflow/benchmark/run-benchmark.mts +++ b/ts/packages/agents/scriptflow/benchmark/run-benchmark.mts @@ -135,7 +135,6 @@ async function createLiveDispatcher( agents: { actions: true, commands: true }, execution: { history: false }, collectCommandResult: true, - portBase: 9100, persistDir, storageProvider: getFsStorageProvider(), }); diff --git a/ts/packages/agents/taskflow/benchmark/run-benchmark.mts b/ts/packages/agents/taskflow/benchmark/run-benchmark.mts index 343f61c1fb..ede0cfb9f5 100644 --- a/ts/packages/agents/taskflow/benchmark/run-benchmark.mts +++ b/ts/packages/agents/taskflow/benchmark/run-benchmark.mts @@ -190,7 +190,6 @@ async function createLiveDispatcher( agents: { actions: true, commands: true }, execution: { history: false }, collectCommandResult: true, - portBase: 9200, persistDir, storageProvider: getFsStorageProvider(), clientIO, diff --git a/ts/packages/dispatcher/dispatcher/src/context/appAgentManager.ts b/ts/packages/dispatcher/dispatcher/src/context/appAgentManager.ts index e91988cabb..d4f88acab9 100644 --- a/ts/packages/dispatcher/dispatcher/src/context/appAgentManager.ts +++ b/ts/packages/dispatcher/dispatcher/src/context/appAgentManager.ts @@ -126,10 +126,8 @@ export class AppAgentManager implements ActionConfigProvider { private readyWaiters: Array<() => void> = []; private readonly actionSemanticMap?: ActionSchemaSemanticMap; private readonly actionSchemaFileCache: ActionSchemaFileCache; - private nextPortIndex = 0; public constructor( cacheDir: string | undefined, - private readonly portBase: number, private readonly allowSharedLocalView?: string[], private readonly agentInitOptions?: Record, ) { @@ -170,6 +168,12 @@ export class AppAgentManager implements ActionConfigProvider { return record.port; } + public setLocalHostPort(appAgentName: string, port: number) { + const record = this.getRecord(appAgentName); + record.port = port; + debug(`Port ${port} assigned to ${appAgentName}`); + } + public getSharedLocalHostPort(requester: string, target: string) { const record = this.agents.get(target); @@ -507,12 +511,10 @@ export class AppAgentManager implements ActionConfigProvider { } } - const port = manifest.localView - ? this.portBase + this.nextPortIndex++ - : undefined; + const port = manifest.localView ? 0 : undefined; if (port !== undefined) { - debug(`Port ${port} assigned to ${appAgentName}`); + debug(`Dynamic port (OS-assigned) reserved for ${appAgentName}`); } const record: AppAgentRecord = { diff --git a/ts/packages/dispatcher/dispatcher/src/context/commandHandlerContext.ts b/ts/packages/dispatcher/dispatcher/src/context/commandHandlerContext.ts index 5bd6e4034f..258e573de6 100644 --- a/ts/packages/dispatcher/dispatcher/src/context/commandHandlerContext.ts +++ b/ts/packages/dispatcher/dispatcher/src/context/commandHandlerContext.ts @@ -238,8 +238,9 @@ async function getAgentCache( * - persistSession: whether to save and restore session state across runs. * * Agent port assignments - for agents that host their own http server: - * - portBase: The base port to use for the agents. Default is 9001. Agents will be assigned ports starting from this value. * - allowSharedLocalView: The list of agent names that can get the ports of all other agent's port. Default is undefined. + * Ports are assigned dynamically by the OS (listen on port 0) to avoid conflicts when multiple sessions start concurrently. + * Each agent's view server reports its bound port back to the dispatcher via IPC, which stores it via setLocalHostPort(). * * Logging options: * - metrics: whether to enable collection of timing metrics. Default is false. @@ -261,7 +262,6 @@ export type DispatcherOptions = DeepPartialUndefined & { // Agent port assignments allowSharedLocalView?: string[]; // agents that can access any shared local views, default to undefined - portBase?: number; // default to 9001 // Indexing service discovery indexingServiceRegistry?: IndexingServiceRegistry; // registry for indexing service discovery @@ -567,10 +567,8 @@ export async function initializeCommandHandlerContext( if (embeddingCacheDir) { ensureDirectory(embeddingCacheDir); } - const portBase = options?.portBase ?? 9001; const agents = new AppAgentManager( cacheDir, - portBase, options?.allowSharedLocalView, options?.agentInitOptions, ); diff --git a/ts/packages/dispatcher/dispatcher/src/execute/sessionContext.ts b/ts/packages/dispatcher/dispatcher/src/execute/sessionContext.ts index 4f2f3e08c4..699d6cb6be 100644 --- a/ts/packages/dispatcher/dispatcher/src/execute/sessionContext.ts +++ b/ts/packages/dispatcher/dispatcher/src/execute/sessionContext.ts @@ -140,6 +140,9 @@ export function createSessionContext( } return localHostPort; }, + setLocalHostPort(port: number) { + context.agents.setLocalHostPort(name, port); + }, indexes(type: string): Promise { return new Promise((resolve, reject) => { const iidx: IndexData[] = diff --git a/ts/packages/shell/src/main/instance.ts b/ts/packages/shell/src/main/instance.ts index 9770c7a6bc..ca24a2c28e 100644 --- a/ts/packages/shell/src/main/instance.ts +++ b/ts/packages/shell/src/main/instance.ts @@ -176,7 +176,6 @@ async function initializeDispatcher( indexingServiceRegistry, constructionProvider: getDefaultConstructionProvider(), allowSharedLocalView: ["browser"], - portBase: isProd ? 9001 : 9050, }); } From a4c611a9b16db2e611bc094538d4e5b8b52f722b Mon Sep 17 00:00:00 2001 From: George Ng Date: Wed, 8 Apr 2026 16:34:43 -0700 Subject: [PATCH 07/11] Update documentation --- ts/docs/architecture/dispatcher.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ts/docs/architecture/dispatcher.md b/ts/docs/architecture/dispatcher.md index 57c1cd3eed..ad305e3d66 100644 --- a/ts/docs/architecture/dispatcher.md +++ b/ts/docs/architecture/dispatcher.md @@ -393,6 +393,12 @@ provider.getAppAgentNames() # Discover available agents │ │ - Load flow definitions │ │ │ └─────────────────────────────────────────────┘ │ │ │ +│ If manifest.localView = true: │ +│ - Reserve a port slot (assigned 0 = OS-chosen) │ +│ - Agent's view server spawned on first activation │ +│ - Server binds to OS-assigned port, reports back │ +│ via IPC → stored via SessionContext.setLocalHostPort│ +│ │ └───────────────────────────────────────────────────────┘ ``` From f822abc327e0fdcff24c90e1ec68dabfd941a02a Mon Sep 17 00:00:00 2001 From: George Ng Date: Wed, 8 Apr 2026 16:43:34 -0700 Subject: [PATCH 08/11] Fix rpc function call --- ts/packages/agentRpc/src/server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ts/packages/agentRpc/src/server.ts b/ts/packages/agentRpc/src/server.ts index 7f5811b14d..52a7d7d14d 100644 --- a/ts/packages/agentRpc/src/server.ts +++ b/ts/packages/agentRpc/src/server.ts @@ -451,7 +451,7 @@ export function createAgentRpcServer( }); }, setLocalHostPort(port: number) { - rpc.send("setLocalHostPort", { contextId, port }); + return rpc.invoke("setLocalHostPort", { contextId, port }); }, addDynamicAgent: async ( name: string, From e3842901a79e87118df26f5a006f10d565e483e0 Mon Sep 17 00:00:00 2001 From: George Ng Date: Wed, 8 Apr 2026 16:52:17 -0700 Subject: [PATCH 09/11] Address build errors --- ts/packages/agents/browser/src/agent/websiteMemory.mts | 1 + ts/packages/shell/src/main/instance.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/ts/packages/agents/browser/src/agent/websiteMemory.mts b/ts/packages/agents/browser/src/agent/websiteMemory.mts index a2386b7bd0..a800d20937 100644 --- a/ts/packages/agents/browser/src/agent/websiteMemory.mts +++ b/ts/packages/agents/browser/src/agent/websiteMemory.mts @@ -157,6 +157,7 @@ export async function resolveURLWithHistory( removeDynamicAgent: async () => {}, forceCleanupDynamicAgent: async () => {}, getSharedLocalHostPort: async () => 0, + setLocalHostPort: async () => {}, indexes: async () => [], reloadAgentSchema: async () => {}, }; diff --git a/ts/packages/shell/src/main/instance.ts b/ts/packages/shell/src/main/instance.ts index ca24a2c28e..2b6dbd12b2 100644 --- a/ts/packages/shell/src/main/instance.ts +++ b/ts/packages/shell/src/main/instance.ts @@ -32,7 +32,7 @@ import { import { getStatusSummary } from "agent-dispatcher/helpers/status"; import { setPendingUpdateCallback } from "./commands/update.js"; import { createClientIORpcClient } from "@typeagent/dispatcher-rpc/clientio/client"; -import { isProd, isTest } from "./index.js"; +import { isTest } from "./index.js"; import { getFsStorageProvider } from "dispatcher-node-providers"; import { ensureAndConnectDispatcher } from "@typeagent/agent-server-client"; From 4c934147362d2f2750b126142d44be17b7f96097 Mon Sep 17 00:00:00 2001 From: George Ng Date: Wed, 8 Apr 2026 17:42:37 -0700 Subject: [PATCH 10/11] Move Agent WebSocket Server into a singleton to avoid duplicate instantiations --- .../src/agent/browserActionHandler.mts | 144 +++++++++--------- 1 file changed, 73 insertions(+), 71 deletions(-) diff --git a/ts/packages/agents/browser/src/agent/browserActionHandler.mts b/ts/packages/agents/browser/src/agent/browserActionHandler.mts index 6bfbb2c717..949d27ed5d 100644 --- a/ts/packages/agents/browser/src/agent/browserActionHandler.mts +++ b/ts/packages/agents/browser/src/agent/browserActionHandler.mts @@ -28,7 +28,10 @@ import { displaySuccess, getMessage, } from "@typeagent/agent-sdk/helpers/display"; -import { BrowserClient } from "./agentWebSocketServer.mjs"; +import { + AgentWebSocketServer, + BrowserClient, +} from "./agentWebSocketServer.mjs"; import { extractPageComponent } from "./componentExtractor.mjs"; import { extractCrosswordSchema } from "./crosswordSchemaExtractor.mjs"; import { createTabTitleIndex } from "./tabTitleIndex.mjs"; @@ -135,6 +138,9 @@ const debugClientRouting = registerDebug("typeagent:browser:client-routing"); let _webFlowStore: any | undefined; let _webFlowStoreInitializing: Promise | undefined; +// Module-level singleton — one WebSocket server per agent process, shared across all session contexts. +const _agentWebSocketServer = new AgentWebSocketServer(8081); + // Track retry counts for dynamic display requests const dynamicDisplayRetryCounters = new Map(); const MAX_RETRY_CYCLES = 2; @@ -309,6 +315,7 @@ async function initializeBrowserContext( clientBrowserControl === undefined ? "extension" : "electron", index: undefined, localHostPort, + agentWebSocketServer: _agentWebSocketServer, resolverSettings: { searchResolver: true, keywordResolver: true, @@ -384,82 +391,82 @@ async function updateBrowserContext( } if (!context.agentContext.agentWebSocketServer) { - const { AgentWebSocketServer } = await import( - "./agentWebSocketServer.mjs" - ); - context.agentContext.agentWebSocketServer = - new AgentWebSocketServer(8081); - - // Register agentRpc invoke handlers for channel-multiplexed messages - context.agentContext.agentWebSocketServer.setAgentInvokeHandlers( - createAgentInvokeHandlers(context), + throw new Error( + "AgentWebSocketServer not initialized in browser context.", ); + } - context.agentContext.agentWebSocketServer.getPreferredClientType = - () => { - return context.agentContext.preferredClientType; - }; - - context.agentContext.agentWebSocketServer.onClientConnected = ( - client: BrowserClient, - ) => { - // Recreate externalBrowserControl when a new extension client connects - if (client.type === "extension") { - debug( - `Extension client connected: ${client.id}, recreating externalBrowserControl`, - ); - - // Dispose old RPC instance to prevent handler chaining - if (context.agentContext.externalBrowserControl) { - context.agentContext.externalBrowserControl.dispose(); - } + // Register agentRpc invoke handlers for channel-multiplexed messages + context.agentContext.agentWebSocketServer.setAgentInvokeHandlers( + createAgentInvokeHandlers(context), + ); - context.agentContext.externalBrowserControl = - createExternalBrowserClient( - context.agentContext.agentWebSocketServer!, - ); - } + context.agentContext.agentWebSocketServer.getPreferredClientType = + () => { + return context.agentContext.preferredClientType; }; - context.agentContext.agentWebSocketServer.onClientDisconnected = ( - client: BrowserClient, - ) => { - // Log disconnection for debugging - if (client.type === "extension") { - debug(`Extension client disconnected: ${client.id}`); + context.agentContext.agentWebSocketServer.onClientConnected = ( + client: BrowserClient, + ) => { + // Recreate externalBrowserControl when a new extension client connects + if (client.type === "extension") { + debug( + `Extension client connected: ${client.id}, recreating externalBrowserControl`, + ); + + // Dispose old RPC instance to prevent handler chaining + if (context.agentContext.externalBrowserControl) { + context.agentContext.externalBrowserControl.dispose(); } - }; - context.agentContext.agentWebSocketServer.onWebAgentMessage = - async (client: BrowserClient, data: any) => { - // Check for built-in RPC requests (crossword, commerce, etc.) - if ( - data.method === "webAgent/message" && - isBuiltInWebAgentRpcRequest(data.params) - ) { - const { id, method, params } = data.params; - const result = await handleWebAgentRpc( - method, - params, - context, - client.id, - ); + context.agentContext.externalBrowserControl = + createExternalBrowserClient( + context.agentContext.agentWebSocketServer!, + ); + } + }; - const response: BuiltInWebAgentRpcResponse = { - source: "dispatcher", - method: "webAgent/message", - type: "builtInRpcResponse", - id, - ...result, - }; + context.agentContext.agentWebSocketServer.onClientDisconnected = ( + client: BrowserClient, + ) => { + // Log disconnection for debugging + if (client.type === "extension") { + debug(`Extension client disconnected: ${client.id}`); + } + }; - client.socket.send(JSON.stringify(response)); - return; - } + context.agentContext.agentWebSocketServer.onWebAgentMessage = async ( + client: BrowserClient, + data: any, + ) => { + // Check for built-in RPC requests (crossword, commerce, etc.) + if ( + data.method === "webAgent/message" && + isBuiltInWebAgentRpcRequest(data.params) + ) { + const { id, method, params } = data.params; + const result = await handleWebAgentRpc( + method, + params, + context, + client.id, + ); - await processWebAgentMessage(data, context); + const response: BuiltInWebAgentRpcResponse = { + source: "dispatcher", + method: "webAgent/message", + type: "builtInRpcResponse", + id, + ...result, }; - } + + client.socket.send(JSON.stringify(response)); + return; + } + + await processWebAgentMessage(data, context); + }; // Initialize external browser control using the AgentWebSocketServer if ( @@ -525,11 +532,6 @@ async function updateBrowserContext( } } } else { - if (context.agentContext.agentWebSocketServer) { - context.agentContext.agentWebSocketServer.stop(); - delete context.agentContext.agentWebSocketServer; - } - // shut down service if (context.agentContext.browserProcess) { context.agentContext.browserProcess.kill(); From 065082fd15e4086eaf1c2a1f645f9c4263f86135 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Apr 2026 18:37:41 +0000 Subject: [PATCH 11/11] Add TBD comment about multi-session handler routing bug in updateBrowserContext Agent-Logs-Url: https://github.com/microsoft/TypeAgent/sessions/c3f2ef68-c2f2-4b0d-9658-63f68525eb00 Co-authored-by: GeorgeNgMsft <146492653+GeorgeNgMsft@users.noreply.github.com> --- .../agents/browser/src/agent/browserActionHandler.mts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ts/packages/agents/browser/src/agent/browserActionHandler.mts b/ts/packages/agents/browser/src/agent/browserActionHandler.mts index 949d27ed5d..3efba09c48 100644 --- a/ts/packages/agents/browser/src/agent/browserActionHandler.mts +++ b/ts/packages/agents/browser/src/agent/browserActionHandler.mts @@ -396,6 +396,13 @@ async function updateBrowserContext( ); } + // TBD: Bug - AgentWebSocketServer is a process-wide singleton but the invoke handlers, + // connection callbacks, and message callbacks below are all bound to the current session + // context. When multiple sessions are active, each call to updateBrowserContext() overwrites + // these handlers with the latest session's context, causing traffic from other sessions to + // be routed to the wrong session context. Fixing this properly requires AgentWebSocketServer + // to support per-session handler registration (e.g., a map keyed by session/client identity). + // Register agentRpc invoke handlers for channel-multiplexed messages context.agentContext.agentWebSocketServer.setAgentInvokeHandlers( createAgentInvokeHandlers(context),