{"record":{"id":"f7ea7ad69128b9b3","repo":"jackwener/OpenCLI","slug":"bound-tab-for-session-session-session-is-not","errorCode":null,"errorMessage":"Bound tab for session \"${session.session}\" is not debuggable (${tab.url ?? 'unknown URL'}).","messagePattern":"Bound tab for session \"(.+?)\" is not debuggable \\((.+?)\\)\\.","errorType":"error_code","errorClass":"CommandFailure","httpStatus":null,"severity":"error","filePath":"extension/src/background.ts","lineNumber":1492,"sourceCode":"type ResolvedTab = { tabId: number; tab: chrome.tabs.Tab | null };\n\n/**\n * Resolve target tab for the session lease, returning both the tabId and\n * the Tab object (when available) so callers can skip a redundant chrome.tabs.get().\n */\nasync function resolveTab(tabId: number | undefined, leaseKey: string, initialUrl?: string): Promise<ResolvedTab> {\n  const existingSession = automationSessions.get(leaseKey);\n  // Even when an explicit tabId is provided, validate it is still debuggable.\n  if (tabId !== undefined) {\n    try {\n      const tab = await chrome.tabs.get(tabId);\n      const session = existingSession;\n      const matchesSession = session\n        ? (session.preferredTabId !== null ? session.preferredTabId === tabId : tab.windowId === session.windowId)\n        : false;\n      if (isDebuggableUrl(tab.url) && matchesSession) return { tabId, tab };\n      if (session && !session.owned) {\n        throw new CommandFailure(\n          matchesSession ? 'bound_tab_not_debuggable' : 'bound_tab_mismatch',\n          matchesSession\n            ? `Bound tab for session \"${session.session}\" is not debuggable (${tab.url ?? 'unknown URL'}).`\n            : `Target tab is not the tab bound to session \"${session.session}\".`,\n          'Run \"opencli browser bind\" again on a debuggable http(s) tab.',\n        );\n      }\n      if (session && !matchesSession && session.preferredTabId === null && isDebuggableUrl(tab.url)) {\n        // Tab drifted to another window but content is still valid.\n        // Try to move it back instead of abandoning it.\n        console.warn(`[opencli] Tab ${tabId} drifted to window ${tab.windowId}, moving back to ${session.windowId}`);\n        try {\n          await chrome.tabs.move(tabId, { windowId: session.windowId, index: -1 });\n          const moved = await chrome.tabs.get(tabId);\n          if (moved.windowId === session.windowId && isDebuggableUrl(moved.url)) {\n            return { tabId, tab: moved };\n          }\n        } catch (moveErr) {","sourceCodeStart":1474,"sourceCodeEnd":1510,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/extension/src/background.ts#L1474-L1510","documentation":"resolveTab() found an existing bound (unowned) browser automation session and a candidate tab whose window matches, but the tab's URL is not debuggable (not http(s)/about:blank/data:). The library throws because CDP debugging cannot attach to such tabs (chrome://, chrome-extension://, file://, etc.), so honoring the binding would silently break later commands.","triggerScenarios":"A command resolves a tab via resolveTab while a bind-session exists with preferredTabId matching or windowId matching, and the current tab.url fails isDebuggableUrl (e.g. user navigated the bound tab to chrome://settings).","commonSituations":"User manually navigates the bound tab to a browser settings page, a new-tab page (chrome://newtab), a PDF viewer, or an extension page after running 'opencli browser bind'.","solutions":["Navigate the bound tab back to an http(s) page","Re-run 'opencli browser bind' on a debuggable http(s) tab as the remediation hint says","Bind a different tab that stays on http(s) content","Disable auto-redirects/extensions that force the tab onto chrome:// pages"],"exampleFix":"// before\ncmd opencli browser click .btn   // bound tab now on chrome://history\n// after\nopencli browser navigate https://example.com   // put bound tab back on http(s), then retry","handlingStrategy":"validation","validationCode":"const tab = await chrome.tabs.get(tabId);\nconst ok = !tab.url || tab.url.startsWith('http://') || tab.url.startsWith('https://') || tab.url === 'about:blank' || tab.url.startsWith('data:');\nif (!ok) throw new Error(`tab ${tabId} not debuggable: ${tab.url}`);","typeGuard":"function isDebuggableUrl(url?: string): boolean {\n  return !url || url.startsWith('http://') || url.startsWith('https://') || url === 'about:blank' || url.startsWith('data:');\n}","tryCatchPattern":"try {\n  await cmd();\n} catch (e) {\n  if ((e as Error).message.includes('is not debuggable')) await rebindToHttpTab();\n  else throw e;\n}","preventionTips":["Keep the bound tab on http(s) pages during automation","Re-bind after manually navigating the tab","Check tab.url before each command in a wrapper","Pin the automation tab to avoid user interference"],"tags":["chrome-extension","debugging","cdp","session-binding"],"backgroundTag":"tab-not-debuggable","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}