fix(ab-connect): recover a churned-tabId session by stable CDP targetId (0.4.9, #24)
Live-reproduced #24 on 0.4.8 driving the Mercari signin token-exchange hop (login.jp.mercari.com): the cross-process nav gives the tab a NEW Chrome tabId while the CDP targetId stays the same. So cb-tab-<oldTabId> can't be recovered — recoverSessionTab parsed the old tabId, chrome.tabs.get(oldTabId) failed (gone), and it gave up → permanent 'stale sessionId ... its tab is gone' until the page settled ~6s later and something re-attached. current/tab <targetId>/daemon restart all failed because the relay still mapped the targetId to the dead session. Fix: remember each session's targetId across detach (sessionTargets map). When recoverSessionTab can't recover by the encoded tabId, fall back to the STABLE targetId — chrome.debugger.getTargets() to find the tab now hosting that target, attach it, and ALIAS the dead cb-tab-<oldTabId> session to the live tab so the daemon's session id keeps resolving. Longer retry window (~6s) since this hop takes seconds to settle. Builds on 0.4.6/0.4.8 reattach; covers the tabId-churn case those missed. Needs dogfood on the real Mercari flow (can't repro the tabId churn synthetically).
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -30,6 +30,16 @@ const tabs = new Map()
|
|||||||
const sessionToTab = new Map()
|
const sessionToTab = new Map()
|
||||||
/** child (OOPIF/worker) sessionId -> tabId */
|
/** child (OOPIF/worker) sessionId -> tabId */
|
||||||
const childSessionToTab = new Map()
|
const childSessionToTab = new Map()
|
||||||
|
/** sessionId -> CDP targetId, kept ACROSS detach so a dead `cb-tab-<oldTabId>`
|
||||||
|
* session can be recovered by its stable targetId when the cross-process nav
|
||||||
|
* gave the tab a new Chrome tabId (issue #24). Capped to bound memory. */
|
||||||
|
const sessionTargets = new Map()
|
||||||
|
function rememberSessionTarget(sessionId, targetId) {
|
||||||
|
if (!sessionId || !targetId) return
|
||||||
|
sessionTargets.delete(sessionId)
|
||||||
|
sessionTargets.set(sessionId, targetId)
|
||||||
|
if (sessionTargets.size > 256) sessionTargets.delete(sessionTargets.keys().next().value)
|
||||||
|
}
|
||||||
/** tab-group name -> chrome tabGroups id (best-effort cache) */
|
/** tab-group name -> chrome tabGroups id (best-effort cache) */
|
||||||
const groupIdByName = new Map()
|
const groupIdByName = new Map()
|
||||||
|
|
||||||
@@ -172,18 +182,50 @@ function tabIdFromSession(sessionId) {
|
|||||||
// (closed / restricted). (issues #20.1, #23)
|
// (closed / restricted). (issues #20.1, #23)
|
||||||
async function recoverSessionTab(sessionId) {
|
async function recoverSessionTab(sessionId) {
|
||||||
const tabId = tabIdFromSession(sessionId)
|
const tabId = tabIdFromSession(sessionId)
|
||||||
if (tabId == null) return null
|
// 1) Fast path: the encoded Chrome tabId still exists — re-attach it (covers
|
||||||
|
// the common renderer-process swap where the tabId is preserved, #23).
|
||||||
|
if (tabId != null) {
|
||||||
for (let i = 0; i < 3; i++) {
|
for (let i = 0; i < 3; i++) {
|
||||||
const tab = await chrome.tabs.get(tabId).catch(() => null)
|
const tab = await chrome.tabs.get(tabId).catch(() => null)
|
||||||
if (!eligible(tab)) return null
|
if (!eligible(tab)) break // tabId is gone — fall through to targetId recovery
|
||||||
try {
|
try {
|
||||||
await attachTab(tabId)
|
await attachTab(tabId)
|
||||||
if (tabs.has(tabId)) return tabId
|
if (tabs.has(tabId)) return tabId
|
||||||
} catch {
|
} catch {
|
||||||
// mid-swap: the tab exists but isn't attachable yet — back off and retry.
|
// mid-swap: tab exists but isn't attachable yet — back off and retry.
|
||||||
}
|
}
|
||||||
await new Promise((r) => setTimeout(r, 120 + i * 150))
|
await new Promise((r) => setTimeout(r, 120 + i * 150))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
// 2) The Chrome tabId is gone, but the CDP targetId is STABLE across the nav.
|
||||||
|
// Some cross-process hops (Mercari's signin token exchange) give the tab a
|
||||||
|
// NEW tabId while keeping the same target, so `cb-tab-<oldTabId>` can't be
|
||||||
|
// recovered by tabId. Find the tab now hosting our remembered targetId via
|
||||||
|
// chrome.debugger.getTargets(), attach it, and ALIAS the dead session to it
|
||||||
|
// so the daemon's session id keeps resolving. Longer window: this hop can
|
||||||
|
// take several seconds to settle (issue #24).
|
||||||
|
const targetId = sessionTargets.get(sessionId)
|
||||||
|
if (targetId) {
|
||||||
|
for (let i = 0; i < 6; i++) {
|
||||||
|
const targets = await chrome.debugger.getTargets().catch(() => null)
|
||||||
|
const t = targets && targets.find((x) => x.id === targetId && x.tabId != null)
|
||||||
|
if (t && t.tabId != null) {
|
||||||
|
const tab = await chrome.tabs.get(t.tabId).catch(() => null)
|
||||||
|
if (eligible(tab)) {
|
||||||
|
try {
|
||||||
|
await attachTab(t.tabId)
|
||||||
|
if (tabs.has(t.tabId)) {
|
||||||
|
sessionToTab.set(sessionId, t.tabId) // alias dead session -> live tab
|
||||||
|
return t.tabId
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// not attachable yet — keep waiting for the swap to settle.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await new Promise((r) => setTimeout(r, 300 + i * 300))
|
||||||
|
}
|
||||||
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -343,6 +385,7 @@ async function attachTab(tabId) {
|
|||||||
const entry = { sessionId, targetId }
|
const entry = { sessionId, targetId }
|
||||||
tabs.set(tabId, entry)
|
tabs.set(tabId, entry)
|
||||||
sessionToTab.set(sessionId, tabId)
|
sessionToTab.set(sessionId, tabId)
|
||||||
|
rememberSessionTarget(sessionId, targetId)
|
||||||
setBadge(tabId, port ? 'on' : 'connecting')
|
setBadge(tabId, port ? 'on' : 'connecting')
|
||||||
postToHost({
|
postToHost({
|
||||||
method: 'forwardCDPEvent',
|
method: 'forwardCDPEvent',
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "chrome-use",
|
"name": "chrome-use",
|
||||||
"version": "0.4.8",
|
"version": "0.4.9",
|
||||||
"description": "Let chrome-use drive your logged-in Chrome \u2014 install once, no token, no per-use confirmation.",
|
"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",
|
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6vQIyscGIPYPZdSpPwPL0+0gxUROyRgCpmvCSDoc8XUm4qm97VbKnD9Ijc1lV22lNWZtE78gaRjt6BeSfuMgnBymnhLKjN1gU6AI5QUU0mrJyeHdWKvrKQR5FmsM2A7Xr1ykE2SiiS8zNUS3Y/6O5l+Nva7wrVy6E4a2dkBVQkOsu+DV+nEZvhIyuDY5D5SPXqNwUTWTaglwj5mjvHz36xSwCWlPmrtJ+ED0AUyrb2z4GIOmvk4kqtBVrh/UD058klLo4CkYOnIybB5aV6WYuwarfPY4bF/dLggPem+ewLNTUNBuwrxj/A4nUv0LJTuRO8rR7f8WR9qnRCY0Ic5saQIDAQAB",
|
||||||
"icons": {
|
"icons": {
|
||||||
|
|||||||
Reference in New Issue
Block a user