fix(browser): notify agent when click triggers download#93307
Conversation
|
Codex review: needs real behavior proof before merge. Reviewed July 5, 2026, 10:40 PM ET / 02:40 UTC. Summary PR surface: Source +200, Tests +467, Docs +1. Total +668 across 10 files. Reproducibility: yes. at source level: current main auto-saves unmanaged Playwright downloads but Review metrics: 2 noteworthy metrics.
Stored data model Root-cause cluster Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Proof guidance:
Risk before merge
Maintainer options:
Next step before merge
Maintainer decision needed
Security Review detailsBest possible solution: Land this PR or a narrow replacement after updated current-head proof shows the array response and maintainers accept the implicit Do we have a high-confidence way to reproduce the issue? Yes, at source level: current main auto-saves unmanaged Playwright downloads but Is this the best way to solve the issue? Unclear as a final product shape: optional AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 7b9df9e18765. Label changesLabel changes:
Label justifications:
Evidence reviewedPR surface: Source +200, Tests +467, Docs +1. Total +668 across 10 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
Review history (1 earlier review cycle)
|
31a2a96 to
d920f31
Compare
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
|
@clawsweeper re-review |
1 similar comment
|
@clawsweeper re-review |
Fixes eslint(curly) failures in drainPendingDownloadSaves and pickNewDownloads. PR openclaw#93307 required CI gate. Ref: ClawSweeper P1 review finding
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
|
@clawsweeper re-review |
1 similar comment
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
89b1705 to
57eb990
Compare
|
@clawsweeper re-review |
1 similar comment
|
@clawsweeper re-review |
aa5c3d5 to
5f08d18
Compare
5f08d18 to
6eacdd4
Compare
Fixes eslint(curly) failures in drainPendingDownloadSaves and pickNewDownloads. PR openclaw#93307 required CI gate. Ref: ClawSweeper P1 review finding
|
Land-ready proof for exact head What changed from the contributor branch:
Verification:
Real Gateway + browser-agent proof on predecessor
This is the best fix for #93250: one managed save owner, one response type, pre-write policy validation, and real agent-visible completion evidence. |
…esponse - Add drainPendingDownloadSaves() to wait for in-flight saveAs before sampling - Use monotonic downloadSeq cursor instead of bounded-list length - Propagate downloads info in POST /act response for click/batch/evaluate Fixes openclaw#93250
Fixes eslint(curly) failures in drainPendingDownloadSaves and pickNewDownloads. PR openclaw#93307 required CI gate. Ref: ClawSweeper P1 review finding
…isattribution When concurrent /act calls overlap on the same page, using state.actionDownloadCaptures.at(-1) assigned downloads to the wrong action's capture. Push managed save promises to all active captures so the triggering action always receives its download metadata. Also adds regression tests: broadcast-to-all-captures, sequential capture isolation, and strengthen the dispose test to assert no re-capture after disposal.
…n throws before drain Move managedSave.catch() before the captures-branch check so the rejection handler always runs, preventing an unhandled promise rejection when the action throws before drain() is called. Simplify the handler by removing the now-dead return + catch at the bottom.
- Add optional downloads field to BrowserActResponse type contract so typed callers of browserAct() can consume the new payload without casts. - Remove unused afterDispose variable in pw-session tests (lint fix).
capture.promises is not cleared by dispose(), so re-draining a disposed capture still returns pre-dispose results. This is benign — callers should drain before disposing. Update the test to assert the actual behavior (still shows old results, new download not captured).
6eacdd4 to
1a1564c
Compare
|
Merged via squash.
|
* fix(browser): drain download saves and use monotonic cursor for act response - Add drainPendingDownloadSaves() to wait for in-flight saveAs before sampling - Use monotonic downloadSeq cursor instead of bounded-list length - Propagate downloads info in POST /act response for click/batch/evaluate Fixes openclaw#93250 * fix(lint): add braces around single-line if returns Fixes eslint(curly) failures in drainPendingDownloadSaves and pickNewDownloads. PR openclaw#93307 required CI gate. Ref: ClawSweeper P1 review finding * fix(browser): scope act download metadata to action * fix(browser): broadcast downloads to all active captures to prevent misattribution When concurrent /act calls overlap on the same page, using state.actionDownloadCaptures.at(-1) assigned downloads to the wrong action's capture. Push managed save promises to all active captures so the triggering action always receives its download metadata. Also adds regression tests: broadcast-to-all-captures, sequential capture isolation, and strengthen the dispose test to assert no re-capture after disposal. * fix(browser): prevent unhandled rejection when download capture action throws before drain Move managedSave.catch() before the captures-branch check so the rejection handler always runs, preventing an unhandled promise rejection when the action throws before drain() is called. Simplify the handler by removing the now-dead return + catch at the bottom. * fix(browser): type downloads in BrowserActResponse, fix lint unused var - Add optional downloads field to BrowserActResponse type contract so typed callers of browserAct() can consume the new payload without casts. - Remove unused afterDispose variable in pw-session tests (lint fix). * fix(test): correct post-dispose download assertion capture.promises is not cleared by dispose(), so re-draining a disposed capture still returns pre-dispose results. This is benign — callers should drain before disposing. Update the test to assert the actual behavior (still shows old results, new download not captured). * test(browser): cover /act download metadata response * refactor(browser): report action-owned downloads safely * fix(browser): close action download ownership races * test(browser): type action download capture mock --------- Co-authored-by: Peter Steinberger <[email protected]>
* fix(browser): drain download saves and use monotonic cursor for act response - Add drainPendingDownloadSaves() to wait for in-flight saveAs before sampling - Use monotonic downloadSeq cursor instead of bounded-list length - Propagate downloads info in POST /act response for click/batch/evaluate Fixes openclaw#93250 * fix(lint): add braces around single-line if returns Fixes eslint(curly) failures in drainPendingDownloadSaves and pickNewDownloads. PR openclaw#93307 required CI gate. Ref: ClawSweeper P1 review finding * fix(browser): scope act download metadata to action * fix(browser): broadcast downloads to all active captures to prevent misattribution When concurrent /act calls overlap on the same page, using state.actionDownloadCaptures.at(-1) assigned downloads to the wrong action's capture. Push managed save promises to all active captures so the triggering action always receives its download metadata. Also adds regression tests: broadcast-to-all-captures, sequential capture isolation, and strengthen the dispose test to assert no re-capture after disposal. * fix(browser): prevent unhandled rejection when download capture action throws before drain Move managedSave.catch() before the captures-branch check so the rejection handler always runs, preventing an unhandled promise rejection when the action throws before drain() is called. Simplify the handler by removing the now-dead return + catch at the bottom. * fix(browser): type downloads in BrowserActResponse, fix lint unused var - Add optional downloads field to BrowserActResponse type contract so typed callers of browserAct() can consume the new payload without casts. - Remove unused afterDispose variable in pw-session tests (lint fix). * fix(test): correct post-dispose download assertion capture.promises is not cleared by dispose(), so re-draining a disposed capture still returns pre-dispose results. This is benign — callers should drain before disposing. Update the test to assert the actual behavior (still shows old results, new download not captured). * test(browser): cover /act download metadata response * refactor(browser): report action-owned downloads safely * fix(browser): close action download ownership races * test(browser): type action download capture mock --------- Co-authored-by: Peter Steinberger <[email protected]>
What Problem This Solves
When a browser
/actaction triggers a file download, OpenClaw already saves the unmanaged Playwright download, but the agent-visible/actresponse did not include the downloaded filename or saved path.That meant an agent could successfully trigger a download, receive only a generic success response, and then retry because it had no completion/path feedback for the saved file.
Fixes #93250
Why This Change Was Made
This change adds action-scoped download capture around Playwright-backed
/actexecution and returns optionaldownloadsmetadata in the/actJSON response.The response includes the saved path and suggested filename for downloads observed during click, coordinate click, batch, and evaluate actions. The
downloadspayload is page-level observation metadata for the action window, not an exclusive ownership claim: overlapping/actcalls on the same page may receive the same saved download so the browser event is not silently dropped.User Impact
Agents can now see that a browser-triggered download completed, including where the file was saved, and avoid retrying an action that already produced the expected file.
Existing
/actresponses remain compatible: thedownloadsfield is optional and only appears when a download is captured.Evidence
Live click-triggered
/actproof on macOS, Node.js v26.0.0, Chrome 149.0.7827.197:Focused tests:
$ node scripts/run-vitest.mjs run --config test/vitest/vitest.extension-browser.config.ts browser/src/browser/pw-session.test.ts Test Files 1 passed (1) Tests 21 passed (21)Includes overlap contract coverage:
broadcasts downloads to all active captures to prevent misattribution.$ node scripts/run-vitest.mjs run --config test/vitest/vitest.extension-browser.config.ts browser/src/browser/server.agent-contract-core.test.ts Test Files 1 passed (1) Tests 20 passed (20)AI-assisted: built with Codex