From f77f3fb839a247c20f0ccdc6a416359c8725df9f Mon Sep 17 00:00:00 2001 From: SidQin-cyber Date: Sun, 1 Mar 2026 10:28:48 +0800 Subject: [PATCH] fix(browser): tolerate brief extension relay disconnects on attached tabs Keep extension relay tab metadata available across short extension worker drops and allow CDP clients to connect while waiting for reconnect. This prevents false "no tab connected" failures in environments where the extension worker disconnects transiently (e.g. WSLg/MV3). --- src/browser/extension-relay.test.ts | 47 +++++++++++++++++++++++++++++ src/browser/extension-relay.ts | 14 ++++----- 2 files changed, 54 insertions(+), 7 deletions(-) diff --git a/src/browser/extension-relay.test.ts b/src/browser/extension-relay.test.ts index 8725c3f33e8..6a65e592c22 100644 --- a/src/browser/extension-relay.test.ts +++ b/src/browser/extension-relay.test.ts @@ -392,6 +392,53 @@ describe("chrome extension relay server", () => { ext2.close(); }); + it("keeps /json/version websocket endpoint during short extension disconnects", async () => { + const { port, ext } = await startRelayWithExtension(); + ext.send( + JSON.stringify({ + method: "forwardCDPEvent", + params: { + method: "Target.attachedToTarget", + params: { + sessionId: "cb-tab-disconnect", + targetInfo: { + targetId: "t-disconnect", + type: "page", + title: "Disconnect test", + url: "https://example.com", + }, + waitingForDebugger: false, + }, + }, + }), + ); + + await waitForListMatch( + async () => + (await fetch(`${cdpUrl}/json/list`, { + headers: relayAuthHeaders(cdpUrl), + }).then((r) => r.json())) as Array<{ id?: string }>, + (list) => list.some((entry) => entry.id === "t-disconnect"), + ); + + const extClosed = waitForClose(ext, 2_000); + ext.close(); + await extClosed; + + const version = (await fetch(`${cdpUrl}/json/version`, { + headers: relayAuthHeaders(cdpUrl), + }).then((r) => r.json())) as { + webSocketDebuggerUrl?: string; + }; + expect(String(version.webSocketDebuggerUrl ?? "")).toContain("/cdp"); + + const cdp = new WebSocket(`ws://127.0.0.1:${port}/cdp`, { + headers: relayAuthHeaders(`ws://127.0.0.1:${port}/cdp`), + }); + await waitForOpen(cdp); + cdp.close(); + }); + it("waits briefly for extension reconnect before failing CDP commands", async () => { const { port, ext: ext1 } = await startRelayWithExtension(); const cdp = new WebSocket(`ws://127.0.0.1:${port}/cdp`, { diff --git a/src/browser/extension-relay.ts b/src/browser/extension-relay.ts index 3f5697f1d56..a6f14091f6e 100644 --- a/src/browser/extension-relay.ts +++ b/src/browser/extension-relay.ts @@ -82,7 +82,7 @@ type ConnectedTarget = { }; const RELAY_AUTH_HEADER = "x-openclaw-relay-token"; -const DEFAULT_EXTENSION_RECONNECT_GRACE_MS = 5_000; +const DEFAULT_EXTENSION_RECONNECT_GRACE_MS = 20_000; const DEFAULT_EXTENSION_COMMAND_RECONNECT_WAIT_MS = 3_000; function headerValue(value: string | string[] | undefined): string | undefined { @@ -256,6 +256,7 @@ export async function ensureChromeExtensionRelayServer(opts: { const cdpClients = new Set(); const connectedTargets = new Map(); const extensionConnected = () => extensionWs?.readyState === WebSocket.OPEN; + const hasConnectedTargets = () => connectedTargets.size > 0; let extensionDisconnectCleanupTimer: NodeJS.Timeout | null = null; const extensionReconnectWaiters = new Set<(connected: boolean) => void>(); @@ -534,8 +535,9 @@ export async function ensureChromeExtensionRelayServer(opts: { Browser: "OpenClaw/extension-relay", "Protocol-Version": "1.3", }; - // Only advertise the WS URL if a real extension is connected. - if (extensionConnected()) { + // Keep reporting CDP WS while attached targets are cached, so callers can + // reconnect through brief MV3 worker disconnects. + if (extensionConnected() || hasConnectedTargets()) { payload.webSocketDebuggerUrl = cdpWsUrl; } res.writeHead(200, { "Content-Type": "application/json" }); @@ -658,10 +660,8 @@ export async function ensureChromeExtensionRelayServer(opts: { rejectUpgrade(socket, 401, "Unauthorized"); return; } - if (!extensionConnected()) { - rejectUpgrade(socket, 503, "Extension not connected"); - return; - } + // Allow CDP clients to connect even during brief extension worker drops. + // Individual commands already wait briefly for extension reconnect. wssCdp.handleUpgrade(req, socket, head, (ws) => { wssCdp.emit("connection", ws, req); });