{"record":{"id":"656447a4ab1d3422","repo":"stablyai/orca","slug":"browser-tab-not-found-656447","errorCode":"browser_tab_not_found","errorMessage":"Tab index ${index} is out of range. ${liveEntries.length} tab(s) open.","messagePattern":"Tab index (.+?) is out of range\\. (.+?) tab\\(s\\) open\\.","errorType":"exception","errorClass":"BrowserError","httpStatus":null,"severity":"warning","filePath":"src/main/browser/cdp-bridge.ts","lineNumber":986,"sourceCode":"        index,\n        url: guest.getURL(),\n        title: guest.getTitle(),\n        active: wcId === this.activeWebContentsId\n      })\n      index++\n    }\n\n    return { tabs }\n  }\n\n  async tabSwitch(index: number): Promise<BrowserTabSwitchResult> {\n    // Why: filter to live tabs so indices match tabList(), skipping destroyed-but-uncleaned entries.\n    const liveEntries = [...this.getRegisteredTabs()].filter(([_, wcId]) => {\n      const guest = webContents.fromId(wcId)\n      return guest && !guest.isDestroyed()\n    })\n    if (index < 0 || index >= liveEntries.length) {\n      throw new BrowserError(\n        'browser_tab_not_found',\n        `Tab index ${index} is out of range. ${liveEntries.length} tab(s) open.`\n      )\n    }\n\n    const [tabId, wcId] = liveEntries[index]\n    if (this.activeWebContentsId !== null) {\n      this.invalidateRefMap(this.activeWebContentsId)\n    }\n    this.activeWebContentsId = wcId\n\n    return { switched: index, browserPageId: tabId }\n  }\n\n  onTabClosed(webContentsId: number): void {\n    if (this.activeWebContentsId === webContentsId) {\n      this.activeWebContentsId = null\n    }","sourceCodeStart":968,"sourceCodeEnd":1004,"githubUrl":"https://github.com/stablyai/orca/blob/1136503c6a231a16dce8f921f6fadb63d181e8db/src/main/browser/cdp-bridge.ts#L968-L1004","documentation":"tabSwitch(index) resolves the target tab by its zero-based position among live tabs. The live count is computed by filtering the registry for WebContents that still exist and are not destroyed, so stale entries do not inflate the count. If index is negative or >= liveEntries.length, the switch is rejected. The indices shown here match those returned by tabList(), which applies the same filter.","triggerScenarios":"Passing an out-of-range index to tabSwitch; the number of live tabs changed between tabList() and tabSwitch() because a tab was closed in the interim.","commonSituations":"Hardcoding an index that becomes invalid when a tab closes; calling tabSwitch with an index from a stale tabList; off-by-one when iterating tabs.","solutions":["Call tabList() immediately before tabSwitch() to get the current live-tab indices.","Validate the index against the returned tab count before passing it.","Do not cache indices across operations that may open or close tabs."],"exampleFix":"// before\nawait bridge.tabSwitch(5)\n\n// after\nconst { tabs } = bridge.tabList()\nif (index < 0 || index >= tabs.length) {\n  throw new Error(`Index ${index} out of range (0..${tabs.length - 1})`)\n}\nawait bridge.tabSwitch(index)","handlingStrategy":"validation","validationCode":"const { tabs } = bridge.tabList()\nif (index < 0 || index >= tabs.length) {\n  throw new Error(`Tab index ${index} out of range (0..${tabs.length - 1})`)\n}\nawait bridge.tabSwitch(index)","typeGuard":"function isValidTabIndex(index: number, tabCount: number): boolean {\n  return Number.isInteger(index) && index >= 0 && index < tabCount\n}","tryCatchPattern":null,"preventionTips":["Call tabList() immediately before tabSwitch() — indices reflect only live tabs and can change when tabs close.","Never cache tab indices across operations that may open or close tabs.","Validate the index against the current live-tab count before passing it."],"tags":["tabs","browser","index","lifecycle"],"backgroundTag":null,"analyzedSha":"1136503c6a231a16dce8f921f6fadb63d181e8db","analyzedAt":"2026-08-12T23:15:58.167Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}