Skip to content

Commit b6df969

Browse files
committed
fix(browser): keep snapshot ids valid after an observed action is refused
An observed click refused before dispatch (for example, covered by an overlay) never starts its observation, yet it invalidated every id in the snapshot, so the agent could not click the overlay's own close button without re-observing. A failed observed action with no dispatched outcome now only cancels an in-flight capture, which clears the refs itself when it starts.
1 parent f734ba5 commit b6df969

2 files changed

Lines changed: 31 additions & 7 deletions

File tree

‎apps/desktop/src/main/browser-agent/driver.test.ts‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3425,6 +3425,24 @@ describe('credential protection', () => {
34253425
expect(second).not.toMatchObject({ result: { notices: expect.anything() } })
34263426
})
34273427

3428+
it('keeps element ids valid when an observed action is refused before dispatch', async () => {
3429+
const contents = await openPage()
3430+
respondWith(contents, { clickElement: { error: 'obstructed', blocker: 'IMG' } })
3431+
3432+
const refused = await driver.executeTool('chat-test', 'browser_click', {
3433+
elementId: 0,
3434+
observe: {},
3435+
})
3436+
respondWith(contents, {})
3437+
const retried = await driver.executeTool('chat-test', 'browser_click', { elementId: 0 })
3438+
3439+
expect(refused).toEqual({
3440+
ok: false,
3441+
error: expect.stringContaining('That element is covered by IMG'),
3442+
})
3443+
expect(retried).toMatchObject({ ok: true, result: { dispatched: true } })
3444+
})
3445+
34283446
it('invalidates element ids when the active tab changes', async () => {
34293447
await openPage()
34303448
await driver.executeTool('chat-test', 'browser_open_tab', {})

‎apps/desktop/src/main/browser-agent/driver.ts‎

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,14 @@ function createDriverScopeState(): DriverScopeState {
313313
function invalidateSnapshot(state = driverScopeState()): void {
314314
state.snapshotTabId = null
315315
state.snapshotTargets.clear()
316+
cancelPendingSnapshotCapture(state)
317+
}
318+
319+
/**
320+
* Keeps the current refs but stops any in-flight capture from committing its own. A capture
321+
* clears the refs when it starts, so this is all a failed action with a pending observation needs.
322+
*/
323+
function cancelPendingSnapshotCapture(state: DriverScopeState): void {
316324
state.snapshotCaptureEpoch++
317325
}
318326

@@ -5002,7 +5010,10 @@ export async function executeTool(
50025010
try {
50035011
observedResult = await guardedExecution
50045012
} catch (error) {
5005-
if (!actionOutcome) throw error
5013+
if (!actionOutcome) {
5014+
if (params.observe !== undefined) cancelPendingSnapshotCapture(state)
5015+
throw error
5016+
}
50065017
invalidateSnapshot(state)
50075018
observedResult =
50085019
actionOutcome.status === 'pending'
@@ -5049,12 +5060,7 @@ export async function executeTool(
50495060
// The watchdog cannot cancel an in-flight renderer promise. Invalidate its
50505061
// capture token before releasing the queue so a late snapshot cannot
50515062
// overwrite refs belonging to a newer tab or snapshot.
5052-
if (
5053-
tool === 'browser_snapshot' ||
5054-
tool === 'browser_open_url' ||
5055-
tool === 'browser_find' ||
5056-
params.observe !== undefined
5057-
) {
5063+
if (tool === 'browser_snapshot' || tool === 'browser_open_url' || tool === 'browser_find') {
50585064
invalidateSnapshot(state)
50595065
}
50605066
const message = String(sanitizeBrowserResult(getErrorMessage(error), undefined, 0, 'error'))

0 commit comments

Comments
 (0)