can1357/oh-my-pi · error · ToolError

Tab ${JSON.stringify(name)} is bound to a different browser

Error message

Tab ${JSON.stringify(name)} is bound to a different browser (${describeKind(existing.browser.kind)}). Close it first.

What it means

Named tabs are bound to a specific browser kind (e.g. managed CDP vs remote). If tab.open is called with a name that already exists on a different browser kind, the tool fails fast with this ToolError rather than silently migrating the tab; the caller must close the existing tab first.

Source

Thrown at packages/coding-agent/src/tools/browser.ts:270

			}
			throw error;
		}
	}

	async #open(
		name: string,
		params: BrowserParams,
		details: BrowserToolDetails,
		timeoutMs: number,
		signal?: AbortSignal,
	): Promise<AgentToolResult<BrowserToolDetails>> {
		const kind = resolveBrowserKind(params, this.session);
		details.browser = kind.kind;

		// If a tab with this name already exists on a different browser kind, fail fast — caller must close first.
		const existing = getTab(name);
		if (existing && !sameBrowserKind(existing.browser.kind, kind)) {
			throw new ToolError(
				`Tab ${JSON.stringify(name)} is bound to a different browser (${describeKind(existing.browser.kind)}). Close it first.`,
			);
		}

		// The requested timeout must cover the *entire* open — browser
		// acquisition (CDP discovery/connect), queued tab acquisition, worker
		// creation, and navigation — not only `acquireTab`. Compose one deadline
		// from the caller signal and `params.timeout` and thread it through both
		// stages so a stalled acquisition rejects at the requested boundary.
		// Capture the deadline start as well: `acquireTab` counts its
		// worker-init time against this same budget via `deadlineStartMs`
		// instead of restarting the clock after acquisition.
		const deadlineStart = performance.now();
		const timeoutSignal = AbortSignal.timeout(timeoutMs);
		const openSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
		try {
			const browser = await untilAborted(openSignal, () =>
				acquireBrowser(kind, {

View on GitHub (pinned to 9690622007)

Solutions

  1. Close the existing tab with the same name first, then reopen with the new browser kind
  2. Use a different tab name for the other browser kind
  3. Make the browser kind explicit and consistent (same params/env) for a given tab name
  4. Check resolveBrowserKind inputs (params.browser, session config) to see why the kind differs

Example fix

// before
await browser.run({ action: 'open', name: 'main', url }) // kind switched
// after
await browser.run({ action: 'close', name: 'main' });
await browser.run({ action: 'open', name: 'main', url });
Defensive patterns

Strategy: validation

Validate before calling

const existing = getTab?.(name); // or track locally
if (existing && existing.browserKind !== requestedKind) {
	await browser.run({ action: 'close', name });
}

Try / catch

try {
	await browser.run({ action: 'open', name, url });
} catch (err) {
	if (err instanceof ToolError && /bound to a different browser/.test(err.message)) {
		await browser.run({ action: 'close', name });
		await browser.run({ action: 'open', name, url });
	} else throw err;
}

Prevention

When it happens

Trigger: Calling browser open with an existing tab name while switching the browser kind (params or session default changed); reusing a well-known tab name (e.g. 'main') after the environment switched between local and remote browsers; concurrent sessions sharing tab registry names with different kinds.

Common situations: Config change mid-session (CDP_ENDPOINT added/removed) altering resolveBrowserKind's result; tests reusing fixed tab names across suites with different browser setups; plugin/agent switching browser preference without closing tabs.

Related errors


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