{"record":{"id":"8de82f056129791a","repo":"can1357/oh-my-pi","slug":"browser-open-aborted","errorCode":null,"errorMessage":"Browser open aborted","messagePattern":"Browser open aborted","errorType":"exception","errorClass":"ToolAbortError","httpStatus":null,"severity":"info","filePath":"packages/coding-agent/src/tools/browser/registry.ts","lineNumber":119,"sourceCode":"\tappArgs?: string[];\n\tsignal?: AbortSignal;\n}\n\nexport async function acquireBrowser(kind: BrowserKind, opts: AcquireBrowserOptions): Promise<BrowserHandle> {\n\tconst key = browserKey(kind);\n\tfor (;;) {\n\t\tconst existing = browsers.get(key);\n\t\tif (existing) {\n\t\t\tif (\"client\" in existing) return existing;\n\t\t\tif (existing.browser.connected) return existing;\n\t\t\tbrowsers.delete(key);\n\t\t\tawait disposeBrowserHandle(existing, { kill: false });\n\t\t\tcontinue;\n\t\t}\n\t\t// Short-circuit before launching: the tool wrapper's `untilAborted` only\n\t\t// rejects its outer promise on abort; without this check `openBrowserHandle`\n\t\t// would still fire and its result would land in `browsers` below.\n\t\tif (opts.signal?.aborted) throw new ToolAbortError(\"Browser open aborted\");\n\n\t\t// Single-flight per key: a concurrent caller already opening this browser\n\t\t// wins; everyone else waits and re-reads the registry. Without this, N\n\t\t// simultaneous opens each launch a Chromium and the last write wins,\n\t\t// leaking the rest as unreferenced process trees.\n\t\tconst pending = pendingOpens.get(key);\n\t\tif (pending) {\n\t\t\tawait pending.catch(() => undefined);\n\t\t\tcontinue;\n\t\t}\n\t\tconst open = openBrowserHandle(kind, opts).finally(() => pendingOpens.delete(key));\n\t\tpendingOpens.set(key, open);\n\t\tconst handle = await open;\n\t\t// The launch may resolve AFTER the caller has already aborted (the outer\n\t\t// `untilAborted` rejects immediately on abort but does not cancel the\n\t\t// inner promise, and `launchHeadlessBrowser` does not accept a signal).\n\t\t// Without this branch the completed handle sits in `browsers` at\n\t\t// refCount:0 forever — no tab ever takes a hold, `releaseBrowser` never","sourceCodeStart":101,"sourceCodeEnd":137,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/tools/browser/registry.ts#L101-L137","documentation":"acquireBrowser() checks opts.signal?.aborted immediately before launching a browser (and again after the launch resolves). If the caller's AbortSignal already fired, it throws ToolAbortError('Browser open aborted') so no Chromium process is ever spawned for an already-cancelled request. This is deliberate, expected cancellation flow, not a fault in the browser layer.","triggerScenarios":"The tool call's AbortSignal was cancelled (user pressed escape / request aborted) while the registry was still iterating existing handles or just before openBrowserHandle() was invoked; also thrown post-launch if the signal aborted while the launch was in flight (line 141-148), with the orphan handle disposed first.","commonSituations":"User cancels a browser_open tool call while the (slow) launch is starting; timeouts cancelling the signal during first-use Chromium download+launch; parallel tool calls aborted together.","solutions":["No fix needed — handle it as cancellation: catch ToolAbortError and return a 'cancelled' result instead of treating it as a browser failure","If aborts are spurious, check what supplies opts.signal (tool wrapper untilAborted / timeout) and raise the timeout or avoid aborting that call","Clean up is automatic; nothing to dispose manually — the registry disposes any orphaned launch itself"],"exampleFix":"// before: treating abort as a crash\ntry { await acquireBrowser(kind, { signal }); } catch (e) { logger.error(e); }\n// after\ntry { await acquireBrowser(kind, { signal }); }\ncatch (e) {\n  if (e instanceof ToolAbortError) return { status: \"cancelled\" };\n  throw e;\n}","handlingStrategy":"try-catch","validationCode":"if (signal?.aborted) {\n  // don't call acquireBrowser at all\n  return cancelledResult;\n}","typeGuard":"function isAbort(err: unknown): err is ToolAbortError {\n  return err instanceof ToolAbortError;\n}","tryCatchPattern":"try {\n  const handle = await acquireBrowser(kind, { cwd, signal });\n} catch (err) {\n  if (isAbort(err)) return { cancelled: true }; // expected cancellation, not a browser failure\n  throw err;\n}","preventionTips":["Check signal.aborted before initiating browser work to skip the call entirely","Treat ToolAbortError('Browser open aborted') as user cancellation in tool wrappers, distinct from launch failures","If aborts arrive prematurely, review the timeout/untilAborted wiring that owns the signal","Reuse a single browser per key via the registry instead of launching per call, shrinking the abort window"],"tags":["abort","cancellation","browser-launch"],"backgroundTag":"operation-aborted","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}