diff --git a/packages/coding-agent/src/tools/run-scope.ts b/packages/coding-agent/src/tools/run-scope.ts index 07ac2a697..f519565c8 100644 --- a/packages/coding-agent/src/tools/run-scope.ts +++ b/packages/coding-agent/src/tools/run-scope.ts @@ -33,6 +33,7 @@ const observedBrowserPromises = new WeakMap, ObservedPromiseSta const observedPromiseConstructor = { [Symbol.species]: Promise }; type PromiseCombinatorName = "all" | "race"; +type PromiseCombinator = (this: PromiseConstructor, values: Iterable) => Promise; interface PromiseCombinatorTrackingContext { owner: object; @@ -40,8 +41,13 @@ interface PromiseCombinatorTrackingContext { } const PROMISE_COMBINATORS: readonly PromiseCombinatorName[] = ["all", "race"]; +const NativePromise = Promise; +const nativePromiseCombinators: Record = { + all: Promise.all, + race: Promise.race, +}; const promiseCombinatorTracking = new AsyncLocalStorage(); -const originalPromiseCombinatorDescriptors = new Map(); +let previousPromiseDescriptor: PropertyDescriptor | undefined; let promiseCombinatorTrackingScopes = 0; /** @@ -64,15 +70,40 @@ export async function withBrowserPromiseCombinatorTracking( } function installPromiseCombinatorTracking(): void { - promiseCombinatorTrackingScopes++; - if (promiseCombinatorTrackingScopes !== 1) return; + if (promiseCombinatorTrackingScopes > 0) { + promiseCombinatorTrackingScopes++; + return; + } + const descriptor = Object.getOwnPropertyDescriptor(globalThis, "Promise"); + if (!descriptor) throw new Error("Global Promise descriptor is unavailable"); + const trackedPromise = createTrackedPromiseConstructor(); + Object.defineProperty(globalThis, "Promise", { ...descriptor, value: trackedPromise }); + previousPromiseDescriptor = descriptor; + promiseCombinatorTrackingScopes = 1; +} + +function restorePromiseCombinatorTracking(): void { + if (promiseCombinatorTrackingScopes > 1) { + promiseCombinatorTrackingScopes--; + return; + } + const descriptor = previousPromiseDescriptor; + try { + if (!descriptor) throw new Error("Global Promise tracking scope is not installed"); + Object.defineProperty(globalThis, "Promise", descriptor); + } finally { + previousPromiseDescriptor = undefined; + promiseCombinatorTrackingScopes = 0; + } +} + +function createTrackedPromiseConstructor(): PromiseConstructor { + class TrackedPromise extends NativePromise {} for (const name of PROMISE_COMBINATORS) { - const descriptor = Object.getOwnPropertyDescriptor(Promise, name); - if (!descriptor || typeof descriptor.value !== "function") continue; - originalPromiseCombinatorDescriptors.set(name, descriptor); - const original = descriptor.value as (this: PromiseConstructor, values: Iterable) => Promise; - Object.defineProperty(Promise, name, { - ...descriptor, + const original = nativePromiseCombinators[name]; + Object.defineProperty(TrackedPromise, name, { + configurable: true, + writable: true, value(this: PromiseConstructor, values: Iterable): Promise { let hasObservedInput = false; const result = Reflect.apply(original, this, [ @@ -87,15 +118,7 @@ function installPromiseCombinatorTracking(): void { }, }); } -} - -function restorePromiseCombinatorTracking(): void { - promiseCombinatorTrackingScopes--; - if (promiseCombinatorTrackingScopes !== 0) return; - for (const [name, descriptor] of originalPromiseCombinatorDescriptors) { - Object.defineProperty(Promise, name, descriptor); - } - originalPromiseCombinatorDescriptors.clear(); + return TrackedPromise; } function* tapObservedBrowserPromises( diff --git a/packages/coding-agent/test/tools/browser-tab-evaluate.test.ts b/packages/coding-agent/test/tools/browser-tab-evaluate.test.ts index 53a34f693..08d0ed3b4 100644 --- a/packages/coding-agent/test/tools/browser-tab-evaluate.test.ts +++ b/packages/coding-agent/test/tools/browser-tab-evaluate.test.ts @@ -350,6 +350,37 @@ describe.skipIf(!CHROMIUM_AVAILABLE)("browser tab evaluation", () => { } }, 30_000); + it("restores promise tracking after evaluated code freezes Promise", async () => { + const tool = new BrowserTool(makeSession()); + const name = `frozen-promise-${process.pid}`; + + try { + await tool.execute("open", { + action: "open", + name, + url: "data:text/html,

ready

", + }); + const frozen = await tool.execute("run", { + action: "run", + name, + code: ` + Object.freeze(Promise); + return Object.isFrozen(Promise); + `, + }); + expect(frozen.content).toEqual([{ type: "text", text: "true" }]); + + const followup = await tool.execute("run", { + action: "run", + name, + code: "return (await Promise.all([42]))[0];", + }); + expect(followup.content).toEqual([{ type: "text", text: "42" }]); + } finally { + await tool.execute("close", { action: "close", name, kill: true }); + } + }, 30_000); + it("folds a user continuation rejection that settles during cleanup", async () => { const tool = new BrowserTool(makeSession()); const name = `cleanup-continuation-rejection-${process.pid}`;