Skip to content

Commit fbc43fc

Browse files
committed
Fix wait baseline navigation and mixed batch stop logic
1 parent 1bf73c9 commit fbc43fc

4 files changed

Lines changed: 48 additions & 2 deletions

File tree

packages/agent/src/translator/browser-wait.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,9 @@ export async function waitForBrowserExpectation(runtime: BrowserWaitRuntime, opt
109109
}
110110
const initial = evaluateBrowserExpectation(options.expect, baseline, baseline, runtime.resolveRef);
111111
if (!baseline.complete) return terminal("unverifiable", "unverifiable", initial, initial, started, now(), "incomplete_observation");
112-
if (runtime.liveNavigationEpoch(targetId) !== baseline.navigationEpoch || [...baseline.generations].some(([key, generation]) => runtime.liveGeneration(key) !== generation)) return terminal("interrupted", "unverifiable", initial, initial, started, now(), "navigation");
112+
const baselineNavigated = runtime.liveNavigationEpoch(targetId) !== baseline.navigationEpoch || [...baseline.generations].some(([key, generation]) => runtime.liveGeneration(key) !== generation);
113+
const allowLocationRecovery = !!options.baseline && isLocationExpectation(options.expect);
114+
if (baselineNavigated && !allowLocationRecovery) return terminal("interrupted", "unverifiable", initial, initial, started, now(), "navigation");
113115
if (expired()) return timedOut(started, now());
114116
if (initial.reason === "stale_ref") return terminal("interrupted", "unverifiable", initial, initial, started, now(), initial.reason);
115117
if (initial.truth === true && !options.baseline) return terminal("satisfied", "preexisting", initial, initial, started, now());

packages/agent/src/translator/translator.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,10 @@ export class InternalComputerTranslator {
136136
await flush();
137137
const reads = await this.browser().execute(action);
138138
result.readResults.push(...reads);
139-
if (action.type === "browser_act" && reads.some((read) => read.type === "browser_act" && read.result.stop_reason)) break;
139+
if (
140+
(action.type === "browser_act" && reads.some((read) => read.type === "browser_act" && read.result.stop_reason)) ||
141+
(action.type === "browser_wait_for" && reads.some((read) => read.type === "browser_wait_for" && (read.result.status === "interrupted" || read.result.status === "timed_out" || read.result.status === "unverifiable")))
142+
) break;
140143
continue;
141144
}
142145
switch (action.type) {

packages/agent/test/browser-wait.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,23 @@ describe("waitForBrowserExpectation", () => {
131131
expect(result).toMatchObject({ status: "satisfied", evidence: "newly_verified" });
132132
});
133133

134+
it("allows location expectations to verify when a pre-action baseline is already stale", async () => {
135+
let time = 0;
136+
const baseline = observation([], true, { url: "https://a.test/start", generations: new Map([["page", 0]]) });
137+
const result = await waitForBrowserExpectation({
138+
selectTarget: async () => "page",
139+
observeTarget: async () => observation([], true, { url: "https://a.test/done", navigationEpoch: 1, generations: new Map([["page", 1]]) }),
140+
dialogCount: () => 0,
141+
targetExists: async () => true,
142+
liveGeneration: () => 1,
143+
liveNavigationEpoch: () => 1,
144+
resolveRef: missingRef,
145+
now: () => time,
146+
delay: async (ms) => { time += ms; },
147+
}, { expect: { type: "url", changed: true }, baseline, targetId: "page", timeoutMs: 20, pollMs: 10 });
148+
expect(result).toMatchObject({ status: "satisfied", evidence: "newly_verified" });
149+
});
150+
134151
it("reports navigation instead of timeout when a location change lands at the deadline", async () => {
135152
let time = 0, reads = 0;
136153
const result = await waitForBrowserExpectation({

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,30 @@ describe("browser_act orchestration", () => {
300300
await translator.executeBatch([{ type: "browser_act", steps: [{ type: "wait" }] }, { type: "browser_text" }]);
301301
expect(executed).toEqual(["browser_act"]);
302302
});
303+
304+
it.each(["interrupted", "timed_out", "unverifiable"] as const)("stops a mixed batch after a browser_wait_for %s result", async (status) => {
305+
const { client } = createClient();
306+
const executed: string[] = [];
307+
const result: BrowserWaitForResult = {
308+
status,
309+
evidence: status === "timed_out" ? "failed" : "unverifiable",
310+
initial: { truth: false, details: ["before"] },
311+
final: { truth: false, details: ["after"] },
312+
elapsed_ms: 1,
313+
...(status === "interrupted" ? { reason: "navigation" } : {}),
314+
details: [],
315+
};
316+
const executor = { execute: async (action: CuaBrowserAction) => {
317+
executed.push(action.type);
318+
return action.type === "browser_wait_for" ? [{ type: "browser_wait_for", result } as BatchReadResult] : [];
319+
} } as unknown as BrowserExecutor;
320+
const translator = new InternalComputerTranslator({ browser, client, createBrowserExecutor: () => executor });
321+
await translator.executeBatch([
322+
{ type: "browser_wait_for", expect: { type: "text", text: "Ready" } },
323+
{ type: "browser_text" },
324+
]);
325+
expect(executed).toEqual(["browser_wait_for"]);
326+
});
303327
});
304328

305329
describe("InternalComputerTranslator computer additions", () => {

0 commit comments

Comments
 (0)