can1357/oh-my-pi · error · Error

No tab with id ${ref.tabId}

Error message

No tab with id ${ref.tabId}

What it means

RelayBridge's #enableSessionRuntime looks up the tab registered for a relay tab reference before enabling the CDP runtime on it. If the tab map no longer contains ref.tabId (the tab was closed, the extension service worker restarted and lost its registry, or the reference is stale), it restores the previous runtimeState and throws Error(`No tab with id <id>`).

Source

Thrown at packages/coding-agent/src/tools/browser/relay/bridge.ts:484

			this.#reply(conn, msg, {});
		} catch (err) {
			this.#replyError(conn, msg, err instanceof Error ? err.message : String(err));
		}
	}

	/**
	 * Drive the shared root `Runtime.enable` for a session and replay the live
	 * contexts to it. Rejects if the root cycle fails so every joined caller
	 * observes the failure instead of a spurious success.
	 */
	async #enableSessionRuntime(conn: CdpConnection, sessionId: string, ref: SessionRef): Promise<void> {
		const prev = ref.runtimeState;
		const epoch = ++ref.runtimeEpoch;
		ref.runtimeState = "enabled";
		const tab = this.#tabs.get(ref.tabId);
		if (!tab) {
			ref.runtimeState = prev;
			throw new Error(`No tab with id ${ref.tabId}`);
		}
		try {
			await this.#ensureRuntimeEnabled(tab);
			// A disable or newer enable may have taken ownership while the root
			// RPC was in flight; only the latest enable may replay or roll back.
			if (conn.sessions.get(sessionId) === ref && ref.runtimeEpoch === epoch && ref.runtimeState === "enabled") {
				this.#replayRuntimeContexts(conn, sessionId, ref, tab);
			}
		} catch (err) {
			if (ref.runtimeEpoch === epoch) {
				ref.runtimeState = prev;
				ref.runtimeContexts.clear();
			}
			throw err;
		}
	}

	async #ensureRuntimeEnabled(tab: TabState): Promise<void> {

View on GitHub (pinned to 9690622007)

Solutions

  1. Re-list tabs (query the relay for current tabs) and re-resolve the target tab to get a fresh, valid tabId before enabling
  2. Catch the error and treat the tab as gone: reopen or re-attach to a new tab
  3. Keep tab refs short-lived; re-acquire refs after any extension reload or Chrome restart
  4. If the service worker restarts frequently, pin the extension or check Chrome's service-worker lifetime settings

Example fix

// before
await bridge.enable(tabRef, session);
// after
try {
  await bridge.enable(tabRef, session);
} catch (err) {
  if (err.message.startsWith("No tab with id")) {
    tabRef = await bridge.resolveTab(currentTabId); // refresh stale ref
    await bridge.enable(tabRef, session);
  } else throw err;
}
Defensive patterns

Strategy: try-catch

Validate before calling

// verify the tab still exists before enabling a session on it
const currentTabs = await bridge.listTabs();
if (!currentTabs.some(t => t.id === ref.tabId)) {
  ref = await bridge.resolveTab(await pickTargetTab()); // re-acquire fresh ref
}

Type guard

function tabExists(ref: { tabId: number }, tabs: { id: number }[]): boolean {
  return tabs.some(t => t.id === ref.tabId);
}

Try / catch

try {
  await bridge.enable(ref, session);
} catch (err) {
  if (err instanceof Error && /^No tab with id \d+$/.test(err.message)) {
    ref = await refreshTabRef(); // tab closed or extension restarted
    await bridge.enable(ref, session);
  } else throw err;
}

Prevention

When it happens

Trigger: Enabling a session on a tab ref whose underlying tab was closed or navigated away before enable; Chrome extension service worker restart clearing the in-memory tab registry; a stale tabId reused after Chrome renumbered tabs.

Common situations: Agent tool call targeting a tab the user closed mid-task; relay extension reloaded (Chrome update, manual reload) invalidating previously issued tab refs; long-running sessions holding tab IDs across extension lifecycles.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/c2c1d17c89c8b7e3. Report an issue: GitHub.