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
- Close the existing tab with the same name first, then reopen with the new browser kind
- Use a different tab name for the other browser kind
- Make the browser kind explicit and consistent (same params/env) for a given tab name
- 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
- Keep a fixed browser kind per tab name
- Close old tabs before switching browser configuration
- Make the browser kind explicit in params instead of relying on ambient config
- Avoid shared fixed tab names across sessions with different browser setups
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
- Debug session ${root.id} is still active. Terminate it befor
- Marketplace "${catalog.name}" already exists
- Marketplace catalog name changed from "${name}" to "${catalo
- Daemon ${spec.name} is already starting
- Daemon ${spec.name} is already ${existing.snapshot.state}
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/5c7b8035dbb65dfe.
Report an issue: GitHub.