diff --git a/packages/coding-agent/src/tools/browser/cmux/cmux-tab.ts b/packages/coding-agent/src/tools/browser/cmux/cmux-tab.ts index 85e5e3619..f307cae69 100644 --- a/packages/coding-agent/src/tools/browser/cmux/cmux-tab.ts +++ b/packages/coding-agent/src/tools/browser/cmux/cmux-tab.ts @@ -10,7 +10,7 @@ import { resolveToCwd } from "../../path-utils"; import { formatScreenshot } from "../../render-utils"; import { bindRunFacade, - isBrowserRunRejection, + isBrowserRunOwnedRejection, markBrowserRunRejection, observeBrowserRunPromise, resolvePredicateTimeout, @@ -1409,9 +1409,7 @@ export async function runCmuxCode(tab: CmuxTab, opts: RunCmuxCodeOptions): Promi rejectFloatingFailure(error); }; const uninstallRejectionInterceptor = postmortem.interceptUnhandledRejections(reason => { - const browserRunRejection = isBrowserRunRejection(reason, rejectionOwner); - const stack = reason instanceof Error && typeof reason.stack === "string" ? reason.stack : undefined; - if (!browserRunRejection && stack?.includes(`cmux-run-${runId}.js`) !== true) return false; + if (!isBrowserRunOwnedRejection(reason, rejectionOwner, `cmux-run-${runId}.js`)) return false; recordFloatingFailure(reason); return true; }); diff --git a/packages/coding-agent/src/tools/browser/tab-worker.ts b/packages/coding-agent/src/tools/browser/tab-worker.ts index cd50e8795..f8d442afe 100644 --- a/packages/coding-agent/src/tools/browser/tab-worker.ts +++ b/packages/coding-agent/src/tools/browser/tab-worker.ts @@ -24,7 +24,7 @@ import { bindRunFacade, CELL_BUDGET_SLACK_MS, installBrowserWorkerRejectionGuard, - isBrowserRunRejection, + isBrowserRunOwnedRejection, markBrowserRunRejection, markHandled, observeBrowserRunPromise, @@ -784,10 +784,7 @@ export class WorkerCore { #consumeUnhandledRejection(reason: unknown): boolean { const active = this.#active; if (!active) return false; - const browserRunRejection = isBrowserRunRejection(reason, active.rejectionOwner); - const stack = reason instanceof Error && typeof reason.stack === "string" ? reason.stack : undefined; - const fromRun = stack?.includes(`browser-run-${active.id}.js`) === true; - if (!browserRunRejection && !this.#isolated && !fromRun) return false; + if (!isBrowserRunOwnedRejection(reason, active.rejectionOwner, `browser-run-${active.id}.js`)) return false; this.#recordFloatingRejection(active, reason); return true; } diff --git a/packages/coding-agent/src/tools/run-scope.ts b/packages/coding-agent/src/tools/run-scope.ts index f519565c8..db83e1bc7 100644 --- a/packages/coding-agent/src/tools/run-scope.ts +++ b/packages/coding-agent/src/tools/run-scope.ts @@ -22,6 +22,12 @@ export function isBrowserRunRejection(reason: unknown, owner: object): boolean { ); } +/** Returns whether a rejection belongs to the marked browser run or its evaluated source file. */ +export function isBrowserRunOwnedRejection(reason: unknown, owner: object, filename: string): boolean { + if (isBrowserRunRejection(reason, owner)) return true; + return reason instanceof Error && typeof reason.stack === "string" && reason.stack.includes(filename); +} + type FloatingRejectionHandler = (reason: unknown) => void; interface ObservedPromiseState { diff --git a/packages/coding-agent/test/tools/browser-run-cancellation.test.ts b/packages/coding-agent/test/tools/browser-run-cancellation.test.ts index 5c85bf566..133a7bdd9 100644 --- a/packages/coding-agent/test/tools/browser-run-cancellation.test.ts +++ b/packages/coding-agent/test/tools/browser-run-cancellation.test.ts @@ -4,6 +4,7 @@ import { postmortem } from "@oh-my-pi/pi-utils"; import { JsRuntime, type RuntimeHooks } from "../../src/eval/js/shared/runtime"; import { bindRunFacade, + isBrowserRunOwnedRejection, isBrowserRunRejection, markBrowserRunRejection, markHandled, @@ -147,6 +148,20 @@ describe("browser run cancellation", () => { expect(isBrowserRunRejection(new Error("unrelated", { cause: browserFailure }), owner)).toBe(false); }); + it("keeps unrelated worker rejections outside the active browser run", () => { + const owner = {}; + const workerFailure = new Error("transport failed"); + workerFailure.stack = "Error: transport failed\n at tab-worker.ts:1:1"; + const evaluatedFailure = new Error("evaluated failure"); + evaluatedFailure.stack = "Error: evaluated failure\n at browser-run-run-1.js:1:1"; + + expect(isBrowserRunOwnedRejection(workerFailure, owner, "browser-run-run-1.js")).toBe(false); + expect(isBrowserRunOwnedRejection(evaluatedFailure, owner, "browser-run-run-1.js")).toBe(true); + expect( + isBrowserRunOwnedRejection(markBrowserRunRejection(workerFailure, owner), owner, "browser-run-run-1.js"), + ).toBe(true); + }); + it("keeps a later cause-wrapped rejection on the fatal path", async () => { vi.useRealTimers(); const script = `