can1357/oh-my-pi · error · ToolError
Tab ${JSON.stringify(name)} is busy
Error message
Tab ${JSON.stringify(name)} is busy What it means
Each tab serializes its work: runInTabWithSnapshot throws ToolError if the tab's pending set is non-empty, meaning another run is already in flight on that tab. This prevents interleaved scripts from racing on the same page.
Source
Thrown at packages/coding-agent/src/tools/browser/tab-supervisor.ts:520
},
);
}
async function runInTabWithSnapshot(
name: string,
opts: { code: string; timeoutMs: number; signal?: AbortSignal; session?: ToolSession },
snapshot: SessionSnapshot,
): Promise<RunResultOk> {
const tab = tabs.get(name);
if (!tab || tab.state === "dead") {
const killed = killedTabs.get(name);
throw new ToolError(
killed
? `Tab ${JSON.stringify(name)} was killed: ${killed}. Reopen it.`
: `Tab ${JSON.stringify(name)} is not alive. Open it first with action:"open".`,
);
}
if (tab.pending.size > 0) throw new ToolError(`Tab ${JSON.stringify(name)} is busy`);
const id = Snowflake.next();
const { promise, resolve, reject } = Promise.withResolvers<RunResultOk>();
// `releaseTab` calls `pending.reject(closeError)` when the tab dies
// out from under an in-flight run (sibling `browser close --all`,
// session-scoped reap, etc.). Both backends below MUST end up awaiting
// this same `promise` so:
// 1. The caller sees `Tab ... was closed` immediately instead of
// blocking to the run's timeout, and
// 2. `reject(...)` always has an attached handler — a zero-consumer
// rejection would fire `unhandledRejection` and the CLI's
// top-level handler would tear the whole session down, killing
// every other tab and subagent sharing the process (issue #4499).
// The cmux branch also composes `closeAc.signal` into the run's abort
// signal so `wait(...)`, cmux socket calls, and the facade proxies
// unwind promptly when the tab is closed — otherwise a `wait(60_000)`
// with no in-flight socket request would keep `runCmuxCode` blocked
// until timeout even after the tab is gone.
const closeAc = new AbortController();View on GitHub (pinned to 9690622007)
Solutions
- Await the in-flight run before issuing the next one — serialize calls per tab.
- Open additional tabs and spread concurrent work across distinct tab names.
- Abort/kill the stuck run (or the tab) if the previous operation is hung, then retry.
- Reduce per-call timeoutMs so hung scripts fail fast and free the tab.
Example fix
// before
await Promise.all([
browserTool({ action: "eval", name: "docs", code: a }),
browserTool({ action: "eval", name: "docs", code: b }), // busy
]);
// after
await browserTool({ action: "eval", name: "docs", code: a });
await browserTool({ action: "eval", name: "docs", code: b }); Defensive patterns
Strategy: validation
Validate before calling
const tab = getTab(name);
if (tab && tab.pending.size > 0) {
await waitForTabIdle(name); // or use a different tab
} Type guard
function isTabIdle(t: ReturnType<typeof getTab>): boolean {
return !!t && t.state !== "dead" && t.pending.size === 0;
} Try / catch
try {
await runInTab(name, code);
} catch (e) {
if (e instanceof ToolError && e.message.endsWith("is busy")) {
await Bun.sleep(250);
return runInTab(name, code); // bounded retry after prior run drains
}
throw e;
} Prevention
- Never issue parallel runs against the same tab name; serialize or use multiple tabs
- Set per-call timeoutMs so stuck scripts free the tab
- Implement a small retry-with-backoff around 'is busy' errors
- Abort hung runs explicitly instead of letting them hold the tab
When it happens
Trigger: Issuing a second eval/navigate on the same tab name while a previous run (long script, slow navigation, hung wait) has not finished; concurrent tool calls targeting one tab.
Common situations: An agent issuing parallel tool calls that all target the same tab; a previous script stuck on a never-resolving waitFor; retry logic firing while the original run is still pending.
Related errors
- native spelling thread stopped
- Agent is already processing. Use steer() or followUp() to qu
- Failed to open auth database at '${dbPath}' after ${maxAttem
- OAuth refresh ownership was lost before persistence
- No credential with id=${id}
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/5178e0ad5e9468ce.
Report an issue: GitHub.