From bb41c24c0856e886f2746360a2e67c5a785d8ee5 Mon Sep 17 00:00:00 2001 From: leeguooooo Date: Wed, 10 Jun 2026 01:38:18 +0900 Subject: [PATCH] fix(connect): relay answers Browser.getVersion locally (stops reconnect storm) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ROOT CAUSE of per-session command drift on the extension path: the daemon's liveness check (`is_connection_alive` → `Browser.getVersion`) is a BROWSER-level command. The relay only answered Target.* locally and forwarded the rest, so Browser.getVersion went to the extension, which can only do per-tab chrome.debugger → it errored → CdpClient saw TransportError → connection deemed DEAD → the daemon closed + reconnected + re-ran discover_and_attach_targets on EVERY command. Each re-discover rebuilds pages from the relay's minimal targetInfo and resets active_page_index=0, so eval/get-title/screenshot drifted to the first tab (about:blank / a foreign focused tab). Reproduced locally (throwaway Chrome + Extensions.loadUnpacked + fork.24 nm-host): trace showed discover_and_attach_targets running on every command (pages before=0) and [ev] active_idx reset to 0. Fix: relay answers Browser.getVersion locally with a stub version (like getTargets), so the liveness probe succeeds → connection stays alive → no reconnect/re-discover → the session's active tab is preserved. Pairs with fork.24's add_background_page. relay.rs: 10 unit tests. --- cli/src/native/relay.rs | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/cli/src/native/relay.rs b/cli/src/native/relay.rs index 7e058b3..80475ee 100644 --- a/cli/src/native/relay.rs +++ b/cli/src/native/relay.rs @@ -107,6 +107,22 @@ impl RelayState { let session_id = raw.get("sessionId").and_then(|s| s.as_str()); match method { + // Browser-level command the daemon uses as its liveness probe + // (`is_connection_alive` → `Browser.getVersion`). The extension only + // speaks per-tab `chrome.debugger`, so forwarding it errors → the + // daemon would deem the connection dead and reconnect+re-discover on + // EVERY command, resetting the active tab (eval/screenshot drift). + // Answer it locally so the relay connection reads as alive. + "Browser.getVersion" => ClientRoute::Local(json!({ + "id": id, + "result": { + "protocolVersion": "1.3", + "product": "Chrome/ab-connect-relay", + "revision": "", + "userAgent": "", + "jsVersion": "" + } + })), // Discovery is best-effort and event-driven in real CDP; abs only // reads the getTargets result, so an empty ack is enough here. "Target.setDiscoverTargets" | "Target.setAutoAttach" => { @@ -302,6 +318,21 @@ mod tests { } } + #[test] + fn browser_get_version_is_answered_locally() { + // Liveness probe must NOT be forwarded (the extension can't do + // browser-level commands) — else the daemon reconnects on every command. + let mut s = RelayState::new(); + let route = s.route_client_command(1, &json!({ "id": 7, "method": "Browser.getVersion" })); + match route { + ClientRoute::Local(v) => { + assert_eq!(v["id"], 7); + assert!(v["result"]["protocolVersion"].is_string()); + } + _ => panic!("Browser.getVersion must be answered locally"), + } + } + #[test] fn attach_to_target_returns_known_session() { let mut s = RelayState::new();