jackwener/OpenCLI · error · Error

Page not found: ${targetId} — stale page identity

Error message

Page not found: ${targetId} — stale page identity

What it means

resolveTabId (extension/src/identity.ts:44) performs the reverse mapping: CDP targetId → internal tabId. After a cache miss it refreshes from chrome.debugger.getTargets() and throws if the targetId isn't found, deliberately never guessing. The targetId is stale — the page it identified no longer exists (closed, navigated to a new target, or the browser restarted).

Source

Thrown at extension/src/identity.ts:44

  const result = tabToTarget.get(tabId);
  if (!result) throw new Error(`No targetId for tab ${tabId} — page may have been closed`);
  return result;
}

/**
 * Resolve tabId for a given targetId.
 * Returns cached value if available; on miss, refreshes from chrome.debugger.getTargets().
 * Throws if no tabId can be found — never falls back to guessing.
 */
export async function resolveTabId(targetId: string): Promise<number> {
  const cached = targetToTab.get(targetId);
  if (cached !== undefined) return cached;

  await refreshMappings();

  const result = targetToTab.get(targetId);
  if (result === undefined) throw new Error(`Page not found: ${targetId} — stale page identity`);
  return result;
}

/**
 * Remove mappings for a closed tab.
 * Called from chrome.tabs.onRemoved listener.
 */
export function evictTab(tabId: number): void {
  const targetId = tabToTarget.get(tabId);
  if (targetId) targetToTab.delete(targetId);
  tabToTarget.delete(tabId);
}

/**
 * Full refresh of targetId ↔ tabId mappings from chrome.debugger.getTargets().
 */
async function refreshMappings(): Promise<void> {
  const targets = await chrome.debugger.getTargets();

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Catch the error and re-resolve the page identity from a durable key (URL or your own session id) instead of the stale targetId.
  2. Validate the targetId with chrome.debugger.getTargets() (or chrome.tabs.query) before calling, and remove it from your state if absent.
  3. If the page was navigated, re-acquire the target via the tab's current target rather than reusing the old UUID.
  4. Persist URLs, not targetIds, for anything surviving a browser restart.
  5. Ensure you're not passing an iframe or worker targetId — only 'page' targets are mapped.

Example fix

// before
const tabId = await resolveTabId(targetId);
// after
const exists = (await chrome.debugger.getTargets()).some(t => t.id === targetId && t.type === 'page');
const tabId = exists ? await resolveTabId(targetId) : await reopenPageAndResolve(savedUrl);
Defensive patterns

Strategy: try-catch

Validate before calling

const known = (await chrome.debugger.getTargets()).some(t => t.id === targetId && t.type === 'page');
if (!known) throw new StalePageIdentityError(targetId);

Try / catch

try {
  tabId = await resolveTabId(targetId);
} catch (err) {
  if (err instanceof Error && /Page not found/.test(err.message)) {
    return recoverPageByKey(pageKey); // re-acquire via URL/session key, never reuse the stale targetId
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling resolveTabId with a targetId captured earlier whose page has since been closed; the tab crashed and its target was replaced with a new targetId; prerendered/portal pages swapping target ids on activation; passing a targetId of a non-page target (iframe, background_page) that refreshMappings skips.

Common situations: Queue of saved targetIds processed after the user closed pages; automation resuming from disk state across a Chrome relaunch (target UUIDs are not stable across restarts); iframe targetIds mistakenly used where page targetIds are required; tab discard/hibernation extensions replacing targets.

Related errors


AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29). Data as JSON: /api/errors/ec522de637bd74c1. Report an issue: GitHub.