fix(coding-agent): isolated promise combinator tracking

Used a run-scoped Promise subclass instead of mutating native combinator methods. Evaluated code can now freeze its Promise constructor without breaking cleanup or later browser runs.
This commit is contained in:
brymko
2026-08-02 16:39:58 +08:00
parent af35be1105
commit 5c0187dec5
2 changed files with 72 additions and 18 deletions
+41 -18
View File
@@ -33,6 +33,7 @@ const observedBrowserPromises = new WeakMap<Promise<unknown>, ObservedPromiseSta
const observedPromiseConstructor = { [Symbol.species]: Promise };
type PromiseCombinatorName = "all" | "race";
type PromiseCombinator = (this: PromiseConstructor, values: Iterable<unknown>) => Promise<unknown>;
interface PromiseCombinatorTrackingContext {
owner: object;
@@ -40,8 +41,13 @@ interface PromiseCombinatorTrackingContext {
}
const PROMISE_COMBINATORS: readonly PromiseCombinatorName[] = ["all", "race"];
const NativePromise = Promise;
const nativePromiseCombinators: Record<PromiseCombinatorName, PromiseCombinator> = {
all: Promise.all,
race: Promise.race,
};
const promiseCombinatorTracking = new AsyncLocalStorage<PromiseCombinatorTrackingContext>();
const originalPromiseCombinatorDescriptors = new Map<PromiseCombinatorName, PropertyDescriptor>();
let previousPromiseDescriptor: PropertyDescriptor | undefined;
let promiseCombinatorTrackingScopes = 0;
/**
@@ -64,15 +70,40 @@ export async function withBrowserPromiseCombinatorTracking<T>(
}
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<T> extends NativePromise<T> {}
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<unknown>) => Promise<unknown>;
Object.defineProperty(Promise, name, {
...descriptor,
const original = nativePromiseCombinators[name];
Object.defineProperty(TrackedPromise, name, {
configurable: true,
writable: true,
value(this: PromiseConstructor, values: Iterable<unknown>): Promise<unknown> {
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(
@@ -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,<h1>ready</h1>",
});
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}`;