Skip to content
Draft
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 73 additions & 71 deletions ts/packages/agents/browser/src/agent/browserActionHandler.mts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -135,6 +138,9 @@ const debugClientRouting = registerDebug("typeagent:browser:client-routing");
let _webFlowStore: any | undefined;
let _webFlowStoreInitializing: Promise<void> | 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<string, number>();
const MAX_RETRY_CYCLES = 2;
Expand Down Expand Up @@ -309,6 +315,7 @@ async function initializeBrowserContext(
clientBrowserControl === undefined ? "extension" : "electron",
index: undefined,
localHostPort,
agentWebSocketServer: _agentWebSocketServer,
resolverSettings: {
searchResolver: true,
keywordResolver: true,
Expand Down Expand Up @@ -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),
);

Comment on lines +399 to 403
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
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 (
Expand Down Expand Up @@ -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();
Expand Down
Loading