From fb27835ebcd52ec9a7e9765fa13f1e59569dfd33 Mon Sep 17 00:00:00 2001 From: leeguooooo Date: Fri, 12 Jun 2026 17:43:48 +0900 Subject: [PATCH] =?UTF-8?q?fix(connect):=20stable=20per-tab=20relay=20sess?= =?UTF-8?q?ion=20id=20=E2=80=94=20re-attach=20auto-recovers=20(#17)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a tab's chrome.debugger session was torn down and re-established (cross-process navigation, MV3 service-worker restart wiping the in-memory maps, DevTools stealing the debugger), the extension minted a brand-new monotonic `cb-tab-N` for the same tab. The daemon stays bound to the old id and the relay consumes attach/detach events without telling it to rebind, so the session was orphaned permanently → `stale sessionId / tab is gone`, and re-open never recovered. Derive the session id from the STABLE Chrome tabId (`cb-tab-`) instead. Any re-attach of the same tab now restores the SAME session the daemon already holds, so eval/snapshot transparently follow the new page after a navigation. Extension 0.4.3 → 0.4.4. Adds a relay unit test for the detach→reattach-same- session recovery contract. --- cli/src/native/relay.rs | 39 +++++++++++++++++++++++++++++ extensions/ab-connect/background.js | 12 +++++++-- extensions/ab-connect/manifest.json | 2 +- 3 files changed, 50 insertions(+), 3 deletions(-) diff --git a/cli/src/native/relay.rs b/cli/src/native/relay.rs index 8536415..65b887d 100644 --- a/cli/src/native/relay.rs +++ b/cli/src/native/relay.rs @@ -330,6 +330,45 @@ mod tests { } } + #[test] + fn reattach_with_same_session_restores_target() { + // Issue #17 recovery contract. A tab's chrome.debugger session is torn + // down (cross-process nav, SW restart, …) then re-attached. The fix has + // the extension reuse the SAME `cb-tab-` id across that churn, so + // after detach+reattach the relay must expose the NEW target under the + // SAME session — which is exactly the session the daemon is still bound + // to, so its eval/snapshot auto-follow the new page instead of going stale. + let mut s = RelayState::new(); + s.handle_ext_message(&attached_event("T_old", "cb-tab-42"), "tok"); + s.handle_ext_message( + &json!({ + "method": "forwardCDPEvent", + "params": { "method": "Target.detachedFromTarget", "params": { "sessionId": "cb-tab-42" } } + }), + "tok", + ); + s.handle_ext_message(&attached_event("T_new", "cb-tab-42"), "tok"); + + let route = s.route_client_command(1, &json!({ "id": 1, "method": "Target.getTargets" })); + match route { + ClientRoute::Local(v) => { + let infos = v["result"]["targetInfos"].as_array().unwrap(); + assert_eq!(infos.len(), 1, "only the new target should remain"); + assert_eq!(infos[0]["targetId"], "T_new"); + } + _ => panic!("getTargets must be local"), + } + // The daemon's existing session id still resolves — to the new target. + let route = s.route_client_command( + 1, + &json!({ "id": 2, "method": "Target.attachToTarget", "params": { "targetId": "T_new" } }), + ); + assert_eq!( + route, + ClientRoute::Local(json!({ "id": 2, "result": { "sessionId": "cb-tab-42" } })) + ); + } + #[test] fn browser_get_version_is_answered_locally() { // Liveness probe must NOT be forwarded (the extension can't do diff --git a/extensions/ab-connect/background.js b/extensions/ab-connect/background.js index 8531f79..1c5dbfd 100644 --- a/extensions/ab-connect/background.js +++ b/extensions/ab-connect/background.js @@ -24,7 +24,6 @@ let port = null /** Whether the native-messaging host (the local chrome-use CLI) is linked. * Read by the popup status page. */ let hostConnected = false -let nextSession = 1 /** tabId -> { sessionId, targetId } */ const tabs = new Map() /** sessionId -> tabId (main session per tab) */ @@ -261,7 +260,16 @@ async function attachTab(tabId) { const targetInfo = info?.targetInfo const targetId = String(targetInfo?.targetId || '') if (!targetId) throw new Error('attachTab: no targetId') - const sessionId = `cb-tab-${nextSession++}` + // Derive the session id from the STABLE Chrome tabId, not a monotonic counter + // (issue #17). A tab's chrome.debugger session can be torn down and + // re-established — cross-process navigation, a service-worker restart wiping + // these in-memory maps, DevTools stealing the debugger — and each time the tab + // re-attaches. With a counter, re-attach minted a BRAND-NEW `cb-tab-N`, which + // orphaned the daemon's binding (it's still pinned to the old id and the relay + // never tells it to rebind) → permanent "stale sessionId / tab is gone". The + // tabId is stable across all of that, so `cb-tab-` restores the SAME + // session the daemon already holds → eval/snapshot auto-follow the new page. + const sessionId = `cb-tab-${tabId}` const entry = { sessionId, targetId } tabs.set(tabId, entry) sessionToTab.set(sessionId, tabId) diff --git a/extensions/ab-connect/manifest.json b/extensions/ab-connect/manifest.json index 1e85e57..9f97558 100644 --- a/extensions/ab-connect/manifest.json +++ b/extensions/ab-connect/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "chrome-use", - "version": "0.4.3", + "version": "0.4.4", "description": "Let chrome-use drive your logged-in Chrome \u2014 install once, no token, no per-use confirmation.", "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6vQIyscGIPYPZdSpPwPL0+0gxUROyRgCpmvCSDoc8XUm4qm97VbKnD9Ijc1lV22lNWZtE78gaRjt6BeSfuMgnBymnhLKjN1gU6AI5QUU0mrJyeHdWKvrKQR5FmsM2A7Xr1ykE2SiiS8zNUS3Y/6O5l+Nva7wrVy6E4a2dkBVQkOsu+DV+nEZvhIyuDY5D5SPXqNwUTWTaglwj5mjvHz36xSwCWlPmrtJ+ED0AUyrb2z4GIOmvk4kqtBVrh/UD058klLo4CkYOnIybB5aV6WYuwarfPY4bF/dLggPem+ewLNTUNBuwrxj/A4nUv0LJTuRO8rR7f8WR9qnRCY0Ic5saQIDAQAB", "icons": {