{"record":{"id":"5178e0ad5e9468ce","repo":"can1357/oh-my-pi","slug":"tab-json-stringify-name-is-busy","errorCode":null,"errorMessage":"Tab ${JSON.stringify(name)} is busy","messagePattern":"Tab (.+?) is busy","errorType":"exception","errorClass":"ToolError","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/tools/browser/tab-supervisor.ts","lineNumber":520,"sourceCode":"\t\t},\n\t);\n}\n\nasync function runInTabWithSnapshot(\n\tname: string,\n\topts: { code: string; timeoutMs: number; signal?: AbortSignal; session?: ToolSession },\n\tsnapshot: SessionSnapshot,\n): Promise<RunResultOk> {\n\tconst tab = tabs.get(name);\n\tif (!tab || tab.state === \"dead\") {\n\t\tconst killed = killedTabs.get(name);\n\t\tthrow new ToolError(\n\t\t\tkilled\n\t\t\t\t? `Tab ${JSON.stringify(name)} was killed: ${killed}. Reopen it.`\n\t\t\t\t: `Tab ${JSON.stringify(name)} is not alive. Open it first with action:\"open\".`,\n\t\t);\n\t}\n\tif (tab.pending.size > 0) throw new ToolError(`Tab ${JSON.stringify(name)} is busy`);\n\tconst id = Snowflake.next();\n\tconst { promise, resolve, reject } = Promise.withResolvers<RunResultOk>();\n\t// `releaseTab` calls `pending.reject(closeError)` when the tab dies\n\t// out from under an in-flight run (sibling `browser close --all`,\n\t// session-scoped reap, etc.). Both backends below MUST end up awaiting\n\t// this same `promise` so:\n\t//   1. The caller sees `Tab ... was closed` immediately instead of\n\t//      blocking to the run's timeout, and\n\t//   2. `reject(...)` always has an attached handler — a zero-consumer\n\t//      rejection would fire `unhandledRejection` and the CLI's\n\t//      top-level handler would tear the whole session down, killing\n\t//      every other tab and subagent sharing the process (issue #4499).\n\t// The cmux branch also composes `closeAc.signal` into the run's abort\n\t// signal so `wait(...)`, cmux socket calls, and the facade proxies\n\t// unwind promptly when the tab is closed — otherwise a `wait(60_000)`\n\t// with no in-flight socket request would keep `runCmuxCode` blocked\n\t// until timeout even after the tab is gone.\n\tconst closeAc = new AbortController();","sourceCodeStart":502,"sourceCodeEnd":538,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/tools/browser/tab-supervisor.ts#L502-L538","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nawait Promise.all([\n  browserTool({ action: \"eval\", name: \"docs\", code: a }),\n  browserTool({ action: \"eval\", name: \"docs\", code: b }), // busy\n]);\n// after\nawait browserTool({ action: \"eval\", name: \"docs\", code: a });\nawait browserTool({ action: \"eval\", name: \"docs\", code: b });","handlingStrategy":"validation","validationCode":"const tab = getTab(name);\nif (tab && tab.pending.size > 0) {\n  await waitForTabIdle(name); // or use a different tab\n}","typeGuard":"function isTabIdle(t: ReturnType<typeof getTab>): boolean {\n  return !!t && t.state !== \"dead\" && t.pending.size === 0;\n}","tryCatchPattern":"try {\n  await runInTab(name, code);\n} catch (e) {\n  if (e instanceof ToolError && e.message.endsWith(\"is busy\")) {\n    await Bun.sleep(250);\n    return runInTab(name, code); // bounded retry after prior run drains\n  }\n  throw e;\n}","preventionTips":["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"],"tags":["concurrency","browser","serialization"],"backgroundTag":"resource-busy","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}