Skip to content

Commit 5f82b5b

Browse files
committed
Bound incomplete frame metadata
1 parent ab5e5f5 commit 5f82b5b

2 files changed

Lines changed: 55 additions & 6 deletions

File tree

‎packages/agent/src/translator/browser.ts‎

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const SNAPSHOT_CHAR_LIMIT = 50_000;
2828
const DEFAULT_SNAPSHOT_DEPTH = 15;
2929
const FIND_MATCH_LIMIT = 20;
3030
const REF_LIMIT_PER_TARGET = 1000;
31+
const FRAME_STATE_LIMIT = 1000;
3132
const SCROLL_NOTCH_PX = 120;
3233
const EXPECTATION_TIMEOUT_MS = 2_000;
3334
const EXPECTATION_POLL_MS = 50;
@@ -414,7 +415,9 @@ export class BrowserExecutor {
414415
if ([...generations].some(([frameKey, generation]) => this.generation(frameKey) !== generation)) {
415416
throw new ObservationChangedError();
416417
}
417-
this.pruneFrameState(targetId, new Set(generations.keys()));
418+
const observedFrames = new Set(generations.keys());
419+
if (complete) this.pruneFrameState(targetId, observedFrames);
420+
else this.boundFrameState(observedFrames);
418421
return {
419422
targetId,
420423
tree,
@@ -1346,6 +1349,36 @@ export class BrowserExecutor {
13461349
}
13471350
}
13481351

1352+
private boundFrameState(observed: ReadonlySet<string>): void {
1353+
const protectedFrames = new Set(observed);
1354+
for (const frameId of this.frameSessions.keys()) protectedFrames.add(frameId);
1355+
for (const entry of this.refs.values()) {
1356+
protectedFrames.add(entry.frameId);
1357+
if (entry.sessionTargetId) protectedFrames.add(entry.sessionTargetId);
1358+
}
1359+
let added = true;
1360+
while (added) {
1361+
added = false;
1362+
for (const [child, parent] of this.frameParents) {
1363+
if (protectedFrames.has(child) && !protectedFrames.has(parent)) {
1364+
protectedFrames.add(parent);
1365+
added = true;
1366+
}
1367+
}
1368+
}
1369+
for (const frameId of this.frameParents.keys()) {
1370+
if (this.frameParents.size <= FRAME_STATE_LIMIT) break;
1371+
if (!protectedFrames.has(frameId)) {
1372+
this.frameParents.delete(frameId);
1373+
this.generations.delete(frameId);
1374+
}
1375+
}
1376+
for (const frameId of this.generations.keys()) {
1377+
if (this.generations.size <= FRAME_STATE_LIMIT) break;
1378+
if (!protectedFrames.has(frameId)) this.generations.delete(frameId);
1379+
}
1380+
}
1381+
13491382
private pruneFrameState(targetId: string, observed: ReadonlySet<string>): void {
13501383
const stale = new Set<string>();
13511384
for (const frameId of this.frameParents.keys()) {

‎packages/agent/test/translator-browser.test.ts‎

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,7 @@ describe("BrowserExecutor ref lifecycle", () => {
368368
await expect(executor.execute({ type: "browser_click", ref: "e1" } as CuaBrowserAction)).rejects.toThrow(/stale/);
369369
});
370370

371-
it("prunes rotated iframe state when another iframe is incomplete", async () => {
371+
it("preserves incomplete frame state while bounding stale metadata", async () => {
372372
const fake = createFakeCdp([
373373
ax({ nodeId: "1", role: "RootWebArea", name: "Page", childIds: ["2", "3"] }),
374374
ax({ nodeId: "2", role: "Iframe", backendDOMNodeId: 50, parentId: "1" }),
@@ -385,10 +385,26 @@ describe("BrowserExecutor ref lifecycle", () => {
385385
}
386386
}
387387

388-
const generations = (executor as unknown as { generations: Map<string, number> }).generations;
389-
const frameParents = (executor as unknown as { frameParents: Map<string, string> }).frameParents;
390-
expect([...generations.keys()].filter((key) => key.startsWith("FRAME-"))).toEqual(["FRAME-4"]);
391-
expect([...frameParents.keys()]).toEqual(["FRAME-4"]);
388+
const internal = executor as unknown as {
389+
generations: Map<string, number>;
390+
frameParents: Map<string, string>;
391+
boundFrameState: (observed: ReadonlySet<string>) => void;
392+
};
393+
expect([...internal.generations.keys()].filter((key) => key.startsWith("FRAME-"))).toEqual([
394+
"FRAME-0",
395+
"FRAME-1",
396+
"FRAME-2",
397+
"FRAME-3",
398+
"FRAME-4",
399+
]);
400+
for (let index = 0; index < 1100; index += 1) {
401+
internal.frameParents.set(`STALE-${index}`, "TARGET-1");
402+
internal.generations.set(`STALE-${index}`, 0);
403+
}
404+
internal.boundFrameState(new Set(["TARGET-1", "FRAME-4"]));
405+
expect(internal.frameParents.size).toBeLessThanOrEqual(1000);
406+
expect(internal.generations.size).toBeLessThanOrEqual(1000);
407+
expect(internal.frameParents.has("FRAME-4")).toBe(true);
392408
});
393409

394410
it("drops generation state when a target detaches", async () => {

0 commit comments

Comments
 (0)