pi-agent-browser-native 0.6.13 → 0.6.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +17 -15
  3. package/dist/extensions/agent-browser/index.js +7 -4
  4. package/dist/extensions/agent-browser/lib/argv-grammar.js +1 -0
  5. package/dist/extensions/agent-browser/lib/command-taxonomy.js +4 -0
  6. package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.js +14 -3
  7. package/dist/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.js +38 -5
  8. package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +30 -4
  9. package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +15 -4
  10. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/network-page-filter.js +4 -3
  11. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/scroll-shims.js +1 -2
  12. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/snapshot-filter.js +3 -0
  13. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +2 -17
  14. package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +14 -3
  15. package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +5 -2
  16. package/dist/extensions/agent-browser/lib/playbook.js +6 -5
  17. package/dist/extensions/agent-browser/lib/recording-reservations.js +8 -1
  18. package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +15 -1
  19. package/dist/extensions/agent-browser/lib/results/presentation/batch.js +18 -9
  20. package/dist/extensions/agent-browser/lib/results/presentation/common.js +3 -0
  21. package/dist/extensions/agent-browser/lib/results/presentation/registry.js +1 -1
  22. package/dist/extensions/agent-browser/lib/results/presentation.js +7 -3
  23. package/dist/extensions/agent-browser/lib/results/snapshot-refs.js +14 -2
  24. package/dist/extensions/agent-browser/lib/results/snapshot.js +12 -5
  25. package/dist/extensions/agent-browser/lib/runtime.js +3 -1
  26. package/dist/extensions/agent-browser/lib/session-page-state.js +7 -3
  27. package/dist/extensions/agent-browser/lib/webmcp-observation.js +28 -0
  28. package/dist/scripts/agent-browser-target.mjs +1 -1
  29. package/docs/ARCHITECTURE.md +12 -4
  30. package/docs/COMMAND_REFERENCE.md +73 -20
  31. package/docs/ELECTRON.md +1 -1
  32. package/docs/RELEASE.md +4 -3
  33. package/docs/REQUIREMENTS.md +1 -1
  34. package/docs/SUPPORT_MATRIX.md +15 -9
  35. package/docs/TOOL_CONTRACT.md +24 -12
  36. package/docs/platform-smoke.md +2 -2
  37. package/package.json +1 -1
  38. package/scripts/agent-browser-capability-baseline.mjs +34 -3
  39. package/scripts/agent-browser-target.mjs +1 -1
  40. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/direct-anchor-download.js +0 -142
package/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ ## 0.6.15 - 2026-09-18
6
+
7
+ ### Fixed
8
+
9
+ - Apply ordinary credential redaction to page/origin/URL-filtered network results before structured details and `outputPath` exports.
10
+ - Leave every download's click and file transfer to native download, including loopback links, generated Blob exports, and redirects; remove the anchor-fetch shortcut that could save page HTML as an export.
11
+ - Verify click-probe candidate identity with native selector/ref attribute readback, including XPath scope; clean up and abstain when identity is unproven instead of falsely failing a completed click or recommending a duplicate action.
12
+ - Scroll CSS containers instantly before measuring movement so smooth-scroll styles cannot cause false no-movement failures.
13
+ - Preserve explicitly requested JSON for early snapshot/network filters, scroll results, and preparation failures, retaining metadata in `details` without output-file prose; help/version remains native text.
14
+
15
+ ### Tests
16
+
17
+ - Add native browser regressions for filtered credential redaction, generated Blob download bytes, accessible-name click identity, smooth container movement, and filtered snapshot JSON, alongside deterministic cleanup and early-result output checks.
18
+
19
+ ## 0.6.14 - 2026-09-17
20
+
21
+ ### Changed
22
+
23
+ - Recommend `agent-browser` 0.38.1 while retaining the stable 0.35.0 minimum and separate upstream installation.
24
+ - Support native delta snapshots and persistent refs without treating partial revisions as empty pages; keep native delta baselines upstream-owned.
25
+ - Preserve conditional screenshot options and skipped-image results without inventing files or attaching old images.
26
+ - Parse session pointer modes and recording cursor/contact-sheet options, verify contact-sheet images, and surface native WebMCP catalog updates. Document stateful auth login and native recording timing fixes.
27
+
28
+ ### Fixed
29
+
30
+ - Give automatic managed-session cleanup the normal 35-second command budget, rather than aborting during native Chrome's five-second shutdown grace. Explicit cleanup-timeout overrides remain supported. Report cleanup deadlines with the phase and elapsed time instead of incorrectly reporting caller cancellation.
31
+
3
32
  ## 0.6.13 - 2026-09-14
4
33
 
5
34
  ### Fixed
package/README.md CHANGED
@@ -75,17 +75,17 @@ The result is optimized for agent work:
75
75
  |---|---|---|
76
76
  | Agents build fragile shell commands or repeat browser calls for loops and branches | Exposes `agent_browser` with one-shot sandboxed `script` orchestration, exact `args`, an optional `semanticAction` shorthand for common `find` flows and native `select`, constrained `job` / `qa` presets, experimental `sourceLookup` / `networkSourceLookup` that compile short workflows to `batch`, top-level `electron` for desktop lifecycle, plus controlled `stdin` and `sessionMode` | `extensions/agent-browser/index.ts`, `extensions/agent-browser/lib/input-modes/`, [`docs/TOOL_CONTRACT.md`](docs/TOOL_CONTRACT.md) |
77
77
  | Page snapshots are too large or viewport-blind | Shows compact, main-content-first summaries, surfaces an `Omitted high-value controls` section (plus `details.data.highValueControlRefIds`) when dense pages or desktop host screens hide editables, named surfaces/tabs, primary action buttons, and high-signal named links such as repository results from the trimmed ref lists, supports wrapper-side `snapshot -i --search <text>` / `--filter role=<role>` to trim dense pages while preserving full `details.refSnapshot`, supports `snapshot --viewport` for scroll/viewport metadata, supports `snapshot --diff` for quick ref-map deltas versus the prior tracked snapshot, and stores full redacted output in spill files when needed | `extensions/agent-browser/lib/results/snapshot.ts`, `extensions/agent-browser/lib/orchestration/browser-run/prepare.ts`, `test/agent-browser.presentation.test.ts`, `test/agent-browser.extension-validation.test.ts` |
78
- | Screenshots/downloads get lost in text | Normalizes artifact paths, creates missing parent directories, saves simple loopback anchor downloads to the requested path when possible, and reports existence, size, cwd, session, and repair status | [`docs/COMMAND_REFERENCE.md`](docs/COMMAND_REFERENCE.md#download-screenshot-and-pdf-files) |
78
+ | Screenshots/downloads get lost in text | Normalizes artifact paths, creates missing parent directories, leaves click/download execution to native download, and reports existence, size, cwd, session, and repair status | [`docs/COMMAND_REFERENCE.md`](docs/COMMAND_REFERENCE.md#download-screenshot-and-pdf-files) |
79
79
  | Profile restores and tab drift confuse agents | Tracks managed sessions, keeps every upstream helper probe on the same idle-timeout launch configuration so the background browser is not restarted between a snapshot and action, re-selects target tabs after observed drift, live-verifies the active target and refreshes its title after successful tab selection or `tab close` (while retaining deliberate `about:blank` selections and post-close blank targets), rehydrates branch-backed session state on Pi session-tree changes, and pins later commands only for sessions with drift/restored-session risk | generated tab-recovery notes below; `test/agent-browser.extension-tab-recovery.test.ts` (drift and about:blank recovery), `test/agent-browser.extension-tabs.test.ts` (post-close target), `test/agent-browser.extension-ref-guards.test.ts` (snapshot/action environment and session-tree rehydration), `test/agent-browser.resume-state.test.ts` (persisted session / resume planning) |
80
80
  | Auth/profile workflows can leak secrets | Supports `auth save --password-stdin`, redacts sensitive args, SAML/OAuth-bearing URLs, stdout/stderr, details, and snapshot spills, and discards malformed oversized stdout instead of persisting a parse-failure spill | `test/agent-browser.extension-security-redaction.test.ts` |
81
81
  | Stateful cookies/storage/auth output bloats or leaks context | Presentation layer redacts `details.data` for cookies and credential-like storage values while keeping low-risk local QA values such as `theme: dark` readable; recursively scrubs other structured upstream JSON (network, diff, trace/profiler, stream, dashboard, chat, auth, dialog, frame, state, and similar) using sensitive key names plus string heuristics; masks sensitive argv flags and positionals; scrubs secrets from failed batch step errors; and exposes a compact redacted `batch` matrix on top-level `details.data` | `extensions/agent-browser/lib/results/presentation.ts`, `extensions/agent-browser/lib/results/presentation/diagnostics.ts`, `extensions/agent-browser/lib/runtime.ts`, `test/agent-browser.presentation-diagnostics.test.ts` |
82
82
  | Stale `@eN` refs fail mysteriously | Records per-session `details.refSnapshot`, rejects mismatched URLs / unknown refs / unsafe `batch` stdin ordering before spawn, adds recovery guidance to rerun `snapshot -i` or use stable `find` locators | `extensions/agent-browser/index.ts`, `extensions/agent-browser/lib/session-page-state.ts`, `test/agent-browser.session-page-state.test.ts`, `test/agent-browser.results.test.ts`, `test/agent-browser.extension-ref-guards.test.ts`, `test/agent-browser.extension-semantic-recovery.test.ts` |
83
83
  | Agents need stable success/failure buckets | Exposes bounded `resultCategory`, `successCategory`, and `failureCategory` on tool `details` for branching without parsing prose; a `tool_result` hook also aligns real Pi `isError` semantics, naming `Pi tool isError: true` in prose output while preserving parseable caller-requested `--json` output | [`docs/TOOL_CONTRACT.md`](docs/TOOL_CONTRACT.md#details), `extensions/agent-browser/lib/results/categories.ts`, `extensions/agent-browser/index.ts`, `extensions/agent-browser/lib/pi-tool-rendering.ts`, `test/agent-browser.results.test.ts`, `test/agent-browser.extension-validation.test.ts`, `test/agent-browser.pi-pipeline.test.ts` |
84
- | Clicks can report success without the page receiving the event | Top-level non-Electron direct `click` calls on `xpath=` targets or role-gated current `@e…` refs (`button`, `checkbox`, `menuitem`, `radio`, `switch`, `tab`) install a bounded target-specific DOM-event probe; eligible `@e…` refs require a unique role/name in both the latest snapshot and the live candidates. Duplicate-name refs pass through without a probe: their old ordinal cannot prove which element received the click. If upstream reports success but no trusted event reaches the resolved target, the wrapper fails the tool, exposes `details.clickDispatch`, and suggests explicit retry/inspect next actions (no in-page replay), including a nested-scroll `scrollintoview` action when the probe sees the target outside a scroll container or viewport. Unresolved locator clicks such as raw `find … click` are left upstream-owned to avoid false failures for frame-scoped targets. Other click results still expose `details.pageChangeSummary`; `observed: false` explicitly marks dispatch-only mutation summaries and adds a visible `Action dispatched; application change unverified` warning. Unchanged-URL clicks can surface evidence-backed `details.overlayBlockers` candidates. | [`docs/TOOL_CONTRACT.md`](docs/TOOL_CONTRACT.md#details), `extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.ts`, `extensions/agent-browser/lib/results/presentation/navigation.ts`, `test/agent-browser.presentation.test.ts`, `test/agent-browser.extension-click-dispatch.test.ts` |
85
- | Dashboard scroll commands can look successful while nothing moves | Handles standard `scroll <dir> [px]` against the document first (including pages whose smooth-scroll CSS defeats upstream wheel timing), falls back upstream when the document cannot move, and samples viewport/containers around the fallback; unchanged positions fail as `upstream-error` with `details.scrollNoop`, visible recovery guidance, and exact snapshot/screenshot checks. Unsupported `scrollintoview text=...` fails before dispatch, including inside effective batch rows, and shows exact native `find text ... hover` and snapshot/ref recovery payloads; help remains native pass-through. | [`docs/TOOL_CONTRACT.md`](docs/TOOL_CONTRACT.md#details), [`docs/COMMAND_REFERENCE.md`](docs/COMMAND_REFERENCE.md#core-page-and-element-commands), `test/agent-browser.extension-validation.test.ts` |
84
+ | Clicks can report success without the page receiving the event | Top-level non-Electron direct `click` calls on `xpath=` targets or role-gated current `@e…` refs (`button`, `checkbox`, `menuitem`, `radio`, `switch`, `tab`) install a bounded target-specific DOM-event probe; eligible `@e…` refs require a unique role/name in both the latest snapshot and the live candidates. A per-probe temporary DOM marker must be read back by native `get attr <original-selector> <marker>` before the candidate can verify dispatch, including XPath scope. Missing or mismatched identity cleans up the probe and leaves the native click unverified, not failed. Duplicate-name refs pass through without a probe: their old ordinal cannot prove which element received the click. If upstream reports success but no trusted event reaches the resolved target, the wrapper fails the tool, exposes `details.clickDispatch`, and suggests explicit retry/inspect next actions (no in-page replay), including a nested-scroll `scrollintoview` action when the probe sees the target outside a scroll container or viewport. Unresolved locator clicks such as raw `find … click` are left upstream-owned to avoid false failures for frame-scoped targets. Other click results still expose `details.pageChangeSummary`; `observed: false` explicitly marks dispatch-only mutation summaries and adds a visible `Action dispatched; application change unverified` warning. Unchanged-URL clicks can surface evidence-backed `details.overlayBlockers` candidates. | [`docs/TOOL_CONTRACT.md`](docs/TOOL_CONTRACT.md#details), `extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.ts`, `extensions/agent-browser/lib/results/presentation/navigation.ts`, `test/agent-browser.presentation.test.ts`, `test/agent-browser.extension-click-dispatch.test.ts` |
85
+ | Dashboard scroll commands can look successful while nothing moves | Handles standard `scroll <dir> [px]` against the document first (including pages whose smooth-scroll CSS defeats upstream wheel timing), falls back upstream when the document cannot move, and samples viewport/containers around the fallback; explicit CSS-container scrolling uses instant movement so smooth-scroll CSS cannot cause a premature no-movement failure; unchanged positions fail as `upstream-error` with `details.scrollNoop`, visible recovery guidance, and exact snapshot/screenshot checks. Unsupported `scrollintoview text=...` fails before dispatch, including inside effective batch rows, and shows exact native `find text ... hover` and snapshot/ref recovery payloads; help remains native pass-through. | [`docs/TOOL_CONTRACT.md`](docs/TOOL_CONTRACT.md#details), [`docs/COMMAND_REFERENCE.md`](docs/COMMAND_REFERENCE.md#core-page-and-element-commands), `test/agent-browser.extension-validation.test.ts` |
86
86
  | Dropdown/combobox clicks can focus or hit native option box-model errors | Adds first-class `select <selector> <value...>` paths through raw `args`, `job`, and `semanticAction`; semantic role/name or label select resolves exactly one current visible combobox/listbox ref before action. Custom combobox clicks still detect focused controls with explicit `aria-expanded` state but no visible options and return `details.comboboxFocus` plus exact recovery `nextActions` | [`docs/TOOL_CONTRACT.md`](docs/TOOL_CONTRACT.md#details), [`docs/COMMAND_REFERENCE.md`](docs/COMMAND_REFERENCE.md#core-page-and-element-commands), `extensions/agent-browser/lib/input-modes/semantic-action.ts`, `test/agent-browser.extension-input-modes.test.ts`, `test/agent-browser.extension-validation.test.ts` |
87
87
  | Recording workflows fail late when `ffmpeg` is missing or report stale lifecycle state | After successful `record start` / `record restart`, reports `successCategory: "artifact-pending"`, returns an exact `stop-pending-recording` action, warns when older natives report pending output without `ffmpeg`, and conservatively invalidates prior page-scoped `@e…` refs on every executed start attempt and URL-bearing restart to protect older supported natives. This is not evidence of a page change: 0.37 records the active page unless a URL is supplied; FPS-only calls keep the intended tab; an unbounded transcript-backed namespace/session index reserves active destinations across aliases, serializes artifact lifecycle and explicit wait/output writes, persists cross-branch close tombstones, retires every successful close path (including every matching namespace owner for `close --all`), rejects missing/stale restart output, coalesces terminal batch state, keeps only the newest pending path per identity, rejects recording starts after a nested close, folds Unicode path aliases, and retains exact cleanup actions with visible guidance on any later same-session failure | [`docs/TOOL_CONTRACT.md`](docs/TOOL_CONTRACT.md#details), [`docs/COMMAND_REFERENCE.md`](docs/COMMAND_REFERENCE.md#diff-debug-and-streaming), `test/agent-browser.extension-validation.test.ts`, `test/agent-browser.presentation-artifacts-batch.test.ts` |
88
- | Upstream CLI drift can silently invalidate wrapper behavior | Publishes a repo-readable command reference, verifies it against the recommended 0.37.0 target, and probes browser-backed calls once per cwd/PATH so stable versions below the 0.35.0 floor fail before browser launch with installed/expected version evidence | `npm run verify` |
88
+ | Upstream CLI drift can silently invalidate wrapper behavior | Publishes a repo-readable command reference, verifies it against the recommended 0.38.1 target, and probes browser-backed calls once per cwd/PATH so stable versions below the 0.35.0 floor fail before browser launch with installed/expected version evidence | `npm run verify` |
89
89
  | Pages can expose structured workflows through experimental WebMCP | Shows native positive WebMCP availability in navigation summaries and passes through `webmcp list`, `invoke`, detached `result` / `cancel`, params/frame/timeout options, and the bundled `webmcp-gen` skill; treats `--no-webmcp` as launch-scoped, keeps pending or unsuccessfully settled targets unverified with an actionable `get url` follow-up, invalidates stale refs after page tools run, and budgets effective raw or stdin batch timeouts | [`docs/COMMAND_REFERENCE.md`](docs/COMMAND_REFERENCE.md#webmcp-page-tools), `test/agent-browser.extension-ref-guards.test.ts`, `test/agent-browser.wait-timeouts.test.ts`, `test/agent-browser.real-upstream-contract.test.ts` |
90
90
  | Desktop Electron apps need discovery, CDP attach, and safe teardown | Top-level `electron` runs host `list` / isolated `launch` (temp profile, OS-chosen debug port) / `status` / `probe` / `cleanup`, merges `launchId` plus managed `sessionName`, supports `handoff` `snapshot` / `tabs` / `connect`, and surfaces mismatch and post-command health guidance; wrapper cleanup applies only to launches it created | `extensions/agent-browser/lib/electron/discovery.ts`, `launch.ts`, `cleanup.ts`, [`docs/TOOL_CONTRACT.md`](docs/TOOL_CONTRACT.md#electron), [`docs/COMMAND_REFERENCE.md`](docs/COMMAND_REFERENCE.md#electron-desktop-apps) |
91
91
  | Agents need bundled `skills` text and local setup/status commands without touching the live session | Treats `skills list/get/path`, local auth/profile/setup commands, `session list`, and local state lifecycle commands as sessionless reads/actions when upstream does not need a live page. Session/state rows and targets remain visible, and supported upstream state/config/path operations pass through unchanged. Browser-backed workflows still receive an implicit session only when the caller did not choose one. | [`docs/COMMAND_REFERENCE.md`](docs/COMMAND_REFERENCE.md#built-in-skills), `extensions/agent-browser/lib/command-policy.ts`, `extensions/agent-browser/lib/runtime.ts` |
@@ -108,7 +108,9 @@ Optional external tools unlock the full command surface:
108
108
  | `agent-browser` | All browser automation through this extension | See upstream install docs |
109
109
  | `ffmpeg` | Recording WebM (libvpx) or MP4 (libx264); install before `record start` / `record restart` | `brew install ffmpeg` or `brew install ffmpeg-full` |
110
110
 
111
- Keep both binaries on `PATH`. This package recommends `agent-browser 0.37.0` and accepts stable versions at or above the 0.35.0 floor; browser-backed calls fail fast below that floor while local inspection/setup commands remain available for diagnosis. Native 0.37 validates `ffmpeg`, the output extension and FPS before recording; older supported natives may defer failures until stop. Use `record start <path.webm|path.mp4> [url] [--fps <n>]` (1–60 fps, default 30), then verify the completed file after `record stop`. Native `doctor` checks the recording dependency and encoders.
111
+ Keep both binaries on `PATH`. This package recommends `agent-browser 0.38.1` and accepts stable versions at or above the 0.35.0 floor; browser-backed calls fail fast below that floor while local inspection/setup commands remain available for diagnosis. Native 0.37 validates `ffmpeg`, the output extension and FPS before recording; older supported natives may defer failures until stop. Use `record start <path.webm|path.mp4> [url] [--fps <n>]` (1–60 fps, default 30), then verify the completed file after `record stop`. Native `doctor` checks the recording dependency and encoders.
112
+
113
+ On 0.38.1, use `snapshot --delta` for native full/unchanged/changed revisions and `snapshot --delta --full` to reset its baseline. Surviving DOM nodes keep their refs, but navigation/replaced nodes still require fresh refs. The wrapper preserves native revisions and refreshes complete refs internally for partial results. `screenshot --if-changed` (or `--threshold 0.01`) skips unchanged images without creating an artifact. `--input-mode instant|smooth|human` controls native session pointer movement; `click ... --human`, `drag ... --human`, and timed `mouse move` remain ordinary `args`. Recordings accept `--cursor` and `--contact-sheet`; the latter produces a timestamped PNG verified after stop. `auth login <name> --no-navigate` fills a prepared login page after upstream checks its origin. See the [command reference](docs/COMMAND_REFERENCE.md#upstream-0381-rebaseline) for examples and limits.
112
114
 
113
115
  ### Android / Termux
114
116
 
@@ -188,7 +190,7 @@ npm run doctor
188
190
  The doctor checks:
189
191
 
190
192
  - upstream `agent-browser` exists on `PATH`
191
- - the installed upstream is a stable version at or above the supported 0.35.0 floor; 0.37.0 remains the recommended baseline
193
+ - the installed upstream is a stable version at or above the supported 0.35.0 floor; 0.38.1 remains the recommended baseline
192
194
  - `pi --version` meets the minimum Pi runtime floor for this release; older Pi versions are setup failures
193
195
  - Pi settings do not point at multiple active `pi-agent-browser-native` sources
194
196
 
@@ -308,7 +310,7 @@ This adds agent guidance for signed-in/account-specific tasks; current releases
308
310
 
309
311
  You usually prompt the agent in natural language. These JSON snippets show the exact native tool shape the agent should use.
310
312
 
311
- Open a page and inspect it (first-call recipe: open → snapshot -i → interact with current `@refs` → snapshot -i after changes). Do not pass `--json` in `args`; the wrapper injects it.
313
+ Open a page and inspect it (first-call recipe: open → snapshot -i → interact with current `@refs` → snapshot -i after changes). Omit `--json` unless you need JSON text; structured details are always available.
312
314
 
313
315
  ```json
314
316
  { "args": ["open", "https://example.com"] }
@@ -393,7 +395,7 @@ Evaluate page JavaScript through stdin. Put the script in the top-level `stdin`
393
395
  { "args": ["eval", "--stdin"], "stdin": "({ title: document.title, url: location.href })", "outputPath": "logs/page-state.json" }
394
396
  ```
395
397
 
396
- Use `outputPath` when `eval`, `get`, `snapshot`, or another extraction should be saved as a durable workspace file. Recording results also export on failure or timeout: their JSON envelope retains the failed attempt, native receipt, verification and any recovery evidence rather than writing misleading bare success data. Keep it distinct from screenshot, download, recording, and other browser artifact destinations; preflight rejects known same-call aliases before browser activity, and the result writer preserves the browser artifact if an alias becomes apparent only afterward. The wrapper writes `details.data` when present, otherwise the model-facing text content. When presentation compacted a large direct result, a result row, or the whole `batch`, it instead reads the full command-redacted pre-compaction payload only from the corresponding live wrapper-managed spill recorded in `details.artifactManifest`; if any required spill is unavailable or untrusted, the call fails without writing compact metadata to the requested path. `details.outputFile` reports the saved path and byte count. Explicit upstream `--json` content stays parseable; in that case the save notice lives only in `details.outputFile`.
398
+ Use `outputPath` when `eval`, `get`, `snapshot`, or another extraction should be saved as a durable workspace file. Recording results also export on failure or timeout: their JSON envelope retains the failed attempt, native receipt, verification and any recovery evidence rather than writing misleading bare success data. Keep it distinct from screenshot, download, recording, and other browser artifact destinations; preflight rejects known same-call aliases before browser activity, and the result writer preserves the browser artifact if an alias becomes apparent only afterward. The wrapper writes `details.data` when present, otherwise the model-facing text content. When presentation compacted a large direct result, a result row, or the whole `batch`, it instead reads the full command-redacted pre-compaction payload only from the corresponding live wrapper-managed spill recorded in `details.artifactManifest`; if any required spill is unavailable or untrusted, the call fails without writing compact metadata to the requested path. `details.outputFile` reports the saved path and byte count. Explicit upstream `--json` content stays parseable, including early snapshot/network filters, scroll results, and preparation failures; metadata stays in `details` and the save notice lives only in `details.outputFile`. Help/version output remains native text.
397
399
 
398
400
  Recording receipts separate captured-frame rate and wall-clock capture duration from nominal/output FPS. `details.artifacts[].recording` includes native capture timestamps, received frame counts (not pixel-unique frames), encoded/written/held/dropped/skipped counts, and output duration. Missing native metrics stay unknown. Repeated, static, sparse or final-state-only frames cannot establish UI smoothness.
399
401
 
@@ -457,7 +459,7 @@ For supported upstream `find` flows, direct selector/ref `click` / `check` / `fi
457
459
  Typical pitfalls:
458
460
 
459
461
  - Supply **exactly one** of `script`, `args`, `semanticAction`, `job`, `qa`, `sourceLookup`, `networkSourceLookup`, or `electron` per call (not more, not none). Prefer `script` only for one-shot loops/branches/aggregation, `args` for routine browse; `semanticAction` for stable locators; `job`/`qa` for multi-step checks; `electron` for desktop apps; treat `sourceLookup` / `networkSourceLookup` as experimental candidates-only.
460
- - Do not pass `--json` in `args`; the wrapper injects it automatically.
462
+ - Omit `--json` for ordinary prose; include it when you need parseable JSON text.
461
463
  - `semanticAction` and `job` are **not** valid inside `batch` stdin; batch steps stay upstream argv string arrays (spell a `find` step as tokens there if you need it in a batch).
462
464
  - Commands or locators outside the supported shorthand still require explicit `args`. Common page getters are grouped under `get`: use `get title`, `get url`, or `get text <selector>` rather than shortcut commands such as `title` or `url`; unknown getter shortcuts can return read-only `details.nextActions` like `use-get-title`.
463
465
  - For `locator: "role"`, pass either `value: "button"` or `role: "button"`; if both are present they must match.
@@ -465,7 +467,7 @@ Typical pitfalls:
465
467
  - Do not reuse `@e…` refs across navigation or in-place rerenders. The wrapper records the latest snapshot refs per session and fails stale/recycled getter and mutation refs, including batched getters, before upstream can silently read or hit a different current-page element; use the session-aware `refresh-interactive-refs` next action.
466
468
  - If upstream classifies the failure as `stale-ref` and `details.compiledSemanticAction` is present for a compiled `find` action, `details.nextActions` may list `retry-semantic-action-after-stale-ref` after `refresh-interactive-refs`, carrying the same compiled `find` argv so you can retry the locator-stable target once it is safe to do so. `select` calls that used stale `@refs` only get refresh guidance; use a fresh snapshot or stable selector before retrying (contract in [`docs/TOOL_CONTRACT.md#semanticaction`](docs/TOOL_CONTRACT.md#semanticaction)).
467
469
  - If the failure is `selector-not-found`, the wrapper may take one fresh snapshot and add `Current snapshot ref fallback` when that snapshot has exact visible role/name matches for the failed `find` / `semanticAction` target. Non-fill targets can include direct `try-current-visible-ref*` next actions, and semantic click misses can still add bounded `Agent-browser candidate fallbacks` such as `button`/`link` role retries for `text` clicks. `semanticAction` does not expose `uncheck` while upstream `find ... uncheck` is not runtime-supported; use raw `args: ["uncheck", <selector-or-ref>]` after a stable selector or fresh snapshot ref. For semantic `fill` misses on desktop or host-controlled rich inputs, prefer `details.richInputRecovery`: refresh refs, choose the current editable `@ref`, focus or click it, then use `keyboard type` for framework-controlled editors that require real key events. `keyboard inserttext` is paste-like and can change a DOM value without updating application state, so use it only with separate application-state verification. Direct contenteditable fills are verified with `get text` when snapshot metadata proves the target is contenteditable; if replacement did not happen, `details.fillVerification` warns before any submit step. Those recovery nextActions do not copy the fill text and do not press `Enter` or submit; only submit when the user flow explicitly calls for it (same contract link).
468
- - A successful upstream `click` is not proof that the web app handled the event or changed state. For top-level non-Electron direct clicks on `xpath=` targets and eligible current `@e…` refs, the wrapper may fail the tool with `details.clickDispatch` and a `Click dispatch diagnostic` line when upstream reported success but no trusted DOM event reached the resolved target. Raw `find … click` locator calls are not probed because the wrapper has no concrete element before upstream resolves the locator, and document-level probes can falsely fail frame-scoped clicks. `@e…` ref click probes are limited to current snapshot refs with accessible role `button`, `checkbox`, `menuitem`, `radio`, `switch`, or `tab`, with a unique role/name in both the saved snapshot and the live candidates. Duplicate-name refs remain upstream-owned; snapshot order is not target identity. Use the suggested `inspect-click-dispatch-miss` / `retry-click-after-dispatch-miss` next actions instead of assuming the click mutated the page; when `details.clickDispatch.scrollContainer` is present, use `scroll-target-into-view-after-dispatch-miss` first. When the task depends on a mutation, follow `inspect-after-mutation` / `pageChangeSummary` evidence with a wait, URL/text check, or fresh snapshot before trusting the result; if the target still did not change, retry with a current visible ref or stable selector and report the workflow issue instead of silently continuing. For static local fixtures where the user only needs to exercise app code, an explicit `eval --stdin` programmatic click such as `document.querySelector("#demo").click()` can be a diagnostic workaround, but treat it as an untrusted scripted activation rather than proof a real user click works, and never use it to bypass user instructions. Respect explicit user stop boundaries yourself: if the user says to stop before order/post/purchase/submit, gather evidence on that page and do not click the final action. The wrapper does not parse broad prompt text into business-intent action blocks; `details.promptGuard` is reserved for concrete artifact-before-close checks.
470
+ - A successful upstream `click` is not proof that the web app handled the event or changed state. For top-level non-Electron direct clicks on `xpath=` targets and eligible current `@e…` refs, the wrapper may fail the tool with `details.clickDispatch` and a `Click dispatch diagnostic` line when upstream reported success but no trusted DOM event reached the resolved target. Raw `find … click` locator calls are not probed because the wrapper has no concrete element before upstream resolves the locator, and document-level probes can falsely fail frame-scoped clicks. `@e…` ref click probes are limited to current snapshot refs with accessible role `button`, `checkbox`, `menuitem`, `radio`, `switch`, or `tab`, with a unique role/name in both the saved snapshot and the live candidates. Ref and XPath candidates must also match native `get attr <original-selector> <marker>` readback of a per-probe temporary DOM marker. If identity is missing or mismatched, the wrapper cleans up and abstains from dispatch verification while the native click runs. Duplicate-name refs remain upstream-owned; snapshot order is not target identity. Use the suggested `inspect-click-dispatch-miss` / `retry-click-after-dispatch-miss` next actions instead of assuming the click mutated the page; when `details.clickDispatch.scrollContainer` is present, use `scroll-target-into-view-after-dispatch-miss` first. When the task depends on a mutation, follow `inspect-after-mutation` / `pageChangeSummary` evidence with a wait, URL/text check, or fresh snapshot before trusting the result; if the target still did not change, retry with a current visible ref or stable selector and report the workflow issue instead of silently continuing. For static local fixtures where the user only needs to exercise app code, an explicit `eval --stdin` programmatic click such as `document.querySelector("#demo").click()` can be a diagnostic workaround, but treat it as an untrusted scripted activation rather than proof a real user click works, and never use it to bypass user instructions. Respect explicit user stop boundaries yourself: if the user says to stop before order/post/purchase/submit, gather evidence on that page and do not click the final action. The wrapper does not parse broad prompt text into business-intent action blocks; `details.promptGuard` is reserved for concrete artifact-before-close checks.
469
471
  - A successful upstream `click` can deliver no input at all on some sites. Reproduced against `https://www.saucedemo.com/` with upstream `agent-browser` 0.34.0 and no wrapper involved: when every CLI invocation in the flow is spaced ~0.6s apart, the React add-to-cart click reports success while **zero** DOM events reach a capture-phase listener and the cart badge never updates (0/5 runs succeeded, versus 4/5 with no spacing). A single idle gap is harmless (5/5); only sustained spacing across the flow triggers it. In that state a scripted `document.querySelector(...).click()` still works and updates the badge, so the page and its handlers are fine and the input never arrives; retries, `scrollintoview`, headed mode, and re-navigation do not recover it. This looks site-specific — React TodoMVC and react.dev were unaffected under the same pacing and command count. Wrapper calls do more per-command work than raw CLI calls, so they sit in the slow regime more often. Running the interaction steps as one `batch` keeps them inside a single upstream process and succeeded 5/5; prefer `batch` for consecutive real-app click flows, and treat a click whose `pageChangeSummary`/`navigationSummary` shows no change as unproven. When a `wait --url` or `assertUrl` times out after a suspected missed click, use the `fresh-session-after-url-wait-timeout` next action (`sessionMode: "fresh"` + `open about:blank`): replace about:blank with the target URL and replay the flow as one batch in a fresh session instead of retrying the wait.
470
472
  - A successful `snapshot -i` can surface `Possible overlay blockers` immediately when refs already contain strong dialog/alertdialog evidence plus close/dismiss controls. If a **top-level** `@e…`/`ref=` click succeeds (unified command `click`, not a `batch` step), upstream reports `data.clicked`, and `details.navigationSummary.url` stays on the same tab URL under the same normalization as ref preflight (fragment-insensitive), the wrapper may take one extra `snapshot -i` and add `Possible overlay blockers` with `details.overlayBlockers` (`candidates`, `summary`, optional `snapshot` refresh for refs) plus session-aware `inspect-overlay-state` / bounded `try-overlay-blocker-candidate-*` next actions when that snapshot shows strong modal context (`dialog` / `alertdialog`) and close/dismiss-like controls. Page-wide words like privacy, sign in, or banner alone do not trigger this diagnostic. The unchanged-URL check compares the prior pinned tab target with `details.navigationSummary.url`; CSS selector clicks do not run this overlay probe. Also skipped when tab correction or about-blank recovery already ran on that result. Separately, when upstream rejects a click because another element covers the target's click point (`is covered by` … `at its click point`), the wrapper keeps `failureCategory: "upstream-error"` and adds only the session-aware `inspect-overlay-state` snapshot action; it does not retry the blocked click or guess a dismiss control without fresh snapshot evidence. This covers direct `click`, `semanticAction` click, raw `find` clicks (including `nth` and an omitted action that defaults to click), and failed `batch`/`job` rows; it does not diagnose silent input-dispatch failures.
471
473
  - If `get text <selector>` reads a non-ref, non-simple-id CSS selector with multiple matches or a hidden first match while visible matches exist, including successful `batch` steps, the wrapper may add `Selector text visibility warning`, `details.selectorTextVisibility` (plus `selectorTextVisibilityAll` for multiple batched warnings), and `inspect-visible-text-candidates` next actions; the warning names the matching `details.nextActions` id. Prefer a visible `@ref`, a scoped selector, or a targeted `eval --stdin` over hidden tab content.
@@ -568,7 +570,7 @@ For local app debugging, `sourceLookup` can gather candidate component/file loca
568
570
 
569
571
  This is an experiment, not a guarantee. React hints require a session opened with `--enable react-devtools`, and many builds do not expose useful sourcemap/source metadata; `status: "no-candidates"` is common when nothing matched, and `status: "unsupported"` only when no candidates were found **and** a compiled `react` batch step failed (if DOM or workspace search still produced candidates, you get `candidates-found` instead). For wrapper-tracked packaged Electron apps, a no-candidate result includes `details.sourceLookup.workspaceRoot`, optional `details.sourceLookup.electronContext`, limitations explaining that the scan is limited to the Pi cwd and does not unpack app bundles/`app.asar`, plus Electron snapshot/probe/tab next actions when a launch is known.
570
572
 
571
- `networkSourceLookup` is the matching failed-request experiment. It runs `network request <id>` when `requestId` is present and/or `network requests --filter …` when `filter` or `url` is present (`url` supplies the filter pattern when `filter` is omitted); add `namespace` / `session` when the generated batch should target an explicit upstream namespace/session (`namespace: ""` explicitly selects the default namespace and overrides an ambient namespace). It merges failed-request rows from the batch JSON with initiator-style hints and a bounded workspace literal scan (`maxWorkspaceFiles` defaults to 2000, cap 5000), surfaces everything under `details.networkSourceLookup`, and avoids automatic blame or edits. Compact `network requests` results with safe request IDs also add `details.nextActions` for request details, bounded `networkSourceLookup` on actionable failures, path filtering, diagnostic-buffer clearing before a repro, or HAR capture so agents can branch without guessing request-id syntax. For noisy aggregate buffers, wrapper-side `network requests --current-page` / `--current-origin` keeps only rows matching the active page origin, while `--current-url` keeps exact active-document URL rows and reports counts in `details.networkRequestsPageFilter`. When the wrapper has seen `network route` in the same session, pending fetch/XHR rows or CORS-looking errors that match the route surface `details.networkRouteDiagnostics` plus executable follow-ups to inspect the request or start HAR capture; same-origin/CORS-correct fixture retry guidance stays in prose. Network diagnostics are read-only for wrapper page state: request URLs in `network request` or generated `networkSourceLookup` batches do not replace the session’s active page target or invalidate page-scoped refs from the app page.
573
+ `networkSourceLookup` is the matching failed-request experiment. It runs `network request <id>` when `requestId` is present and/or `network requests --filter …` when `filter` or `url` is present (`url` supplies the filter pattern when `filter` is omitted); add `namespace` / `session` when the generated batch should target an explicit upstream namespace/session (`namespace: ""` explicitly selects the default namespace and overrides an ambient namespace). It merges failed-request rows from the batch JSON with initiator-style hints and a bounded workspace literal scan (`maxWorkspaceFiles` defaults to 2000, cap 5000), surfaces everything under `details.networkSourceLookup`, and avoids automatic blame or edits. Compact `network requests` results with safe request IDs also add `details.nextActions` for request details, bounded `networkSourceLookup` on actionable failures, path filtering, diagnostic-buffer clearing before a repro, or HAR capture so agents can branch without guessing request-id syntax. For noisy aggregate buffers, wrapper-side `network requests --current-page` / `--current-origin` keeps only rows matching the active page origin, while `--current-url` keeps exact active-document URL rows and reports counts in `details.networkRequestsPageFilter`. Filtered results use the same credential redaction as ordinary network diagnostics before populating `details.data` or writing `outputPath`. When the wrapper has seen `network route` in the same session, pending fetch/XHR rows or CORS-looking errors that match the route surface `details.networkRouteDiagnostics` plus executable follow-ups to inspect the request or start HAR capture; same-origin/CORS-correct fixture retry guidance stays in prose. Network diagnostics are read-only for wrapper page state: request URLs in `network request` or generated `networkSourceLookup` batches do not replace the session’s active page target or invalidate page-scoped refs from the app page.
572
574
 
573
575
  ```json
574
576
  { "networkSourceLookup": { "requestId": "req-1", "url": "/api/fail" } }
@@ -581,7 +583,7 @@ For asynchronous exports, use the export control's current snapshot ref (for exa
581
583
  { "args": ["wait", "--download", "/tmp/report.csv"] }
582
584
  ```
583
585
 
584
- When a user gives exact artifact paths for screenshots, recordings, downloads, PDFs, traces, or HAR files, use those paths or explicitly report why the artifact was unavailable; do not silently substitute a different path in the final report. The wrapper creates missing parent directories for direct artifact paths such as `state save`, screenshots, PDFs, downloads, and `wait --download`. For simple loopback `download <selector> <path>` anchor links with HTTP(S) `href`, it can save the in-page response directly to the requested path before falling back to upstream click/download behavior; non-loopback/profile downloads stay upstream-owned. With current upstream `agent-browser`, treat `details.savedFilePath` as upstream-reported metadata and confirm `details.artifacts[].exists` / `details.artifactVerification.verified` before relying on the requested `wait --download <path>` file being present on disk; non-file download payloads such as `data:` URLs are not verified local artifacts.
586
+ When a user gives exact artifact paths for screenshots, recordings, downloads, PDFs, traces, or HAR files, use those paths or explicitly report why the artifact was unavailable; do not silently substitute a different path in the final report. The wrapper creates missing parent directories for direct artifact paths such as `state save`, screenshots, PDFs, downloads, and `wait --download`. Native `download <selector> <path>` always owns the click and download, including loopback links, generated Blob exports, and redirects; the wrapper does not fetch an anchor's `href` instead of clicking it. With current upstream `agent-browser`, treat `details.savedFilePath` as upstream-reported metadata and confirm `details.artifacts[].exists` / `details.artifactVerification.verified` before relying on the requested `wait --download <path>` file being present on disk; non-file download payloads such as `data:` URLs are not verified local artifacts.
585
587
 
586
588
  For evidence-only screenshots or QA captures, branch on `details.artifactVerification` and `details.artifacts` before reporting PASS/FAIL; a pre-existing path from an artifact-producing command that was not updated during the command fails as `status: "stale"` instead of being accepted as fresh evidence (`wait --download` remains observational). Inline image attachments are optional when size limits allow—do not require vision review unless the user asked for visual inspection. If the latest prompt names exact required artifact paths, browser close can be blocked with `details.promptGuard` until those artifacts are saved and verified.
587
589
 
@@ -758,7 +760,7 @@ The opt-in real-upstream suite is separate because it drives a real browser inst
758
760
  npm run verify -- real-upstream
759
761
  ```
760
762
 
761
- That mode sets `PI_AGENT_BROWSER_REAL_UPSTREAM=1` and runs `test/agent-browser.real-upstream-contract.test.ts` against the real `agent-browser` on `PATH` (the stable version must meet the 0.35.0 floor; current command-reference validation targets the recommended 0.37.0 capability baseline). It covers inspection, skills, experimental WebMCP list/invoke/result/cancel plus `--no-webmcp`, and a broad core interaction and navigation matrix on localhost fixtures (including off-viewport click, frame-scoped selector/wait/click behavior, form command fixes, `batch` stdin, and `pushstate`), plus `vitals`, network route/requests/HAR, diff snapshot/screenshot/url, trace/profiler, console/errors/highlight, stream enable/status/disable, `cookies set --curl`, a `react tree` missing-renderer path, and `wait --download` with the on-disk caveat documented in release notes. The harness uses a throwaway temp `HOME` and dedicated socket/screenshot directories so the run does not touch your normal browser profile paths. Browser-opening or credential-dependent families such as `inspect`, `dashboard`, `chat`, provider clouds, and OS clipboard flows stay in fake-upstream or manual validation unless a safe deterministic fixture is added. For prerequisites, isolation details, and troubleshooting, see [`docs/RELEASE.md`](docs/RELEASE.md#real-upstream-contract-validation).
763
+ That mode sets `PI_AGENT_BROWSER_REAL_UPSTREAM=1` and runs `test/agent-browser.real-upstream-contract.test.ts` against the real `agent-browser` on `PATH` (the stable version must meet the 0.35.0 floor; current command-reference validation targets the recommended 0.38.1 capability baseline). It covers inspection, skills, experimental WebMCP list/invoke/result/cancel plus `--no-webmcp`, and a broad core interaction and navigation matrix on localhost fixtures (including off-viewport click, frame-scoped selector/wait/click behavior, form command fixes, `batch` stdin, and `pushstate`), plus `vitals`, network route/requests/HAR, diff snapshot/screenshot/url, trace/profiler, console/errors/highlight, stream enable/status/disable, `cookies set --curl`, a `react tree` missing-renderer path, and `wait --download` with the on-disk caveat documented in release notes. The harness uses a throwaway temp `HOME` and dedicated socket/screenshot directories so the run does not touch your normal browser profile paths. Browser-opening or credential-dependent families such as `inspect`, `dashboard`, `chat`, provider clouds, and OS clipboard flows stay in fake-upstream or manual validation unless a safe deterministic fixture is added. For prerequisites, isolation details, and troubleshooting, see [`docs/RELEASE.md`](docs/RELEASE.md#real-upstream-contract-validation).
762
764
 
763
765
  A deterministic host-only live-browser wrapper smoke is available without an LLM choosing tool calls:
764
766
 
@@ -791,12 +793,12 @@ npm run verify -- release
791
793
  5. Screenshots, downloads, recordings, traces, profiles, and spill files are normalized as Pi-visible artifacts where possible.
792
794
  6. Generated playbook text in docs and tool metadata stays aligned with `extensions/agent-browser/lib/playbook.ts`.
793
795
 
794
- The upstream browser engine remains [`agent-browser`](https://agent-browser.dev/). This package does not bundle it. The recommended baseline is 0.37.0 and the stable runtime floor is 0.35.0; newer stable versions are accepted without version-specific compatibility shims.
796
+ The upstream browser engine remains [`agent-browser`](https://agent-browser.dev/). This package does not bundle it. The recommended baseline is 0.38.1 and the stable runtime floor is 0.35.0; newer stable versions are accepted without version-specific compatibility shims.
795
797
 
796
798
  ## Current limits
797
799
 
798
800
  - Published pre-1.0 package.
799
- - Recommends upstream `agent-browser` 0.37.0 and accepts stable runtimes at or above 0.35.0.
801
+ - Recommends upstream `agent-browser` 0.38.1 and accepts stable runtimes at or above 0.35.0.
800
802
  - Does not bundle `agent-browser`; users install it separately.
801
803
  - Does not provide a human browser UI inside Pi; the primary UX is agent-invoked tool calls. `--headed` asks upstream to show a browser window, but the wrapper cannot yet prove that the window is visible on the user's desktop.
802
804
  - Localhost means the browser host's loopback, not necessarily the shell/Pi host. If `http://localhost:<port>` or `http://127.0.0.1:<port>` fails with errors such as `ERR_EMPTY_RESPONSE`, use an environment-specific host-reachable HTTP(S) address. A `file://` fixture is supported when upstream browser launch settings allow it; use HTTP(S) only when the browser environment cannot reach the local file.
@@ -22,7 +22,7 @@ import { cleanupSecureTempArtifacts } from "./lib/temp.js";
22
22
  import { AGENT_BROWSER_PARAMS } from "./lib/input-modes/params.js";
23
23
  import { AGENT_BROWSER_SCRIPT_DEFAULT_TIMEOUT_MS, AGENT_BROWSER_SCRIPT_NAMESPACE, createAgentBrowserScriptSessionName, isAgentBrowserScriptSessionName, runAgentBrowserScript, } from "./lib/input-modes/script.js";
24
24
  import { closeManagedSession, getSessionContextKey, runAgentBrowserTool } from "./lib/orchestration/browser-run/index.js";
25
- import { canonicalizeExplicitArtifactDestination, getExplicitArtifactDestination } from "./lib/orchestration/browser-run/artifact-paths.js";
25
+ import { canonicalizeExplicitArtifactDestination, getExplicitArtifactDestination, getRecordContactSheetDestination } from "./lib/orchestration/browser-run/artifact-paths.js";
26
26
  import { findElectronLaunchRecordForSession, getActiveElectronRecords } from "./lib/orchestration/browser-run/session-state.js";
27
27
  import { parseBatchCommandArgument, parseUserBatchStdin } from "./lib/orchestration/batch-stdin.js";
28
28
  import { ELECTRON_POST_COMMAND_STATUS_SETTLE_MS, ELECTRON_PROFILE_ISOLATION_DETAILS, cleanupActiveElectronHostLaunches, handleElectronHostInput, restoreElectronLaunchRecordsFromBranch, } from "./lib/orchestration/electron-host/index.js";
@@ -75,6 +75,8 @@ function getArtifactPreflightValidationError(options) {
75
75
  for (const reservation of options.activeRecordingReservations ?? []) {
76
76
  try {
77
77
  activeRecordingDestinations.add(canonicalizeExplicitArtifactDestination(reservation.cwd, reservation.absolutePath));
78
+ if (reservation.contactSheetPath)
79
+ activeRecordingDestinations.add(canonicalizeExplicitArtifactDestination(reservation.cwd, reservation.contactSheetPath));
78
80
  }
79
81
  catch (canonicalizationError) {
80
82
  if (!cleanupOnly)
@@ -106,8 +108,9 @@ function getArtifactPreflightValidationError(options) {
106
108
  if (isCloseCommand(commandStep[0]))
107
109
  sawBatchClose = true;
108
110
  }
109
- const artifactDestination = getExplicitArtifactDestination(commandStep);
110
- if (artifactDestination) {
111
+ for (const artifactDestination of [getExplicitArtifactDestination(commandStep), getRecordContactSheetDestination(commandStep)]) {
112
+ if (!artifactDestination)
113
+ continue;
111
114
  let canonicalDestination;
112
115
  try {
113
116
  canonicalDestination = canonicalizeExplicitArtifactDestination(options.cwd, artifactDestination);
@@ -1126,7 +1129,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1126
1129
  }
1127
1130
  for (const [key, reservation] of activeRecordingReservations) {
1128
1131
  const restored = restoredRecordingState.active.get(key);
1129
- if (restored?.absolutePath !== reservation.absolutePath || restored.cwd !== reservation.cwd || restored.recordingId !== reservation.recordingId || restored.startedAtMs !== reservation.startedAtMs)
1132
+ if (restored?.absolutePath !== reservation.absolutePath || restored.cwd !== reservation.cwd || restored.recordingId !== reservation.recordingId || restored.startedAtMs !== reservation.startedAtMs || restored.contactSheetPath !== reservation.contactSheetPath)
1130
1133
  recordingReservationsDirty = true;
1131
1134
  restoredRecordingState.active.set(key, reservation);
1132
1135
  }
@@ -36,6 +36,7 @@ export const GLOBAL_VALUE_FLAGS = [
36
36
  "--max-output",
37
37
  "--model",
38
38
  "--idle-timeout",
39
+ "--input-mode",
39
40
  ];
40
41
  export const COMMAND_VALUE_FLAGS = [
41
42
  "--allowed-origins",
@@ -324,6 +324,10 @@ export function getRecordCommandOperands(tokens) {
324
324
  // Native validates the range; bare/non-numeric --fps keeps its old literal meaning.
325
325
  if (tokens[index] === "--fps" && /^\+?\d+$/.test(tokens[index + 1] ?? ""))
326
326
  index += 1;
327
+ else if (tokens[index] === "--cursor" || tokens[index] === "--contact-sheet")
328
+ continue;
329
+ else if (tokens[index] === "--contact-sheet-threshold" && tokens[index + 1] !== undefined)
330
+ index += 1;
327
331
  else
328
332
  operands.push(tokens[index]);
329
333
  }
@@ -1,5 +1,5 @@
1
1
  import { lstatSync, readlinkSync, realpathSync, statSync } from "node:fs";
2
- import { basename, dirname, join, resolve } from "node:path";
2
+ import { basename, dirname, extname, join, resolve } from "node:path";
3
3
  import { foldAgentBrowserFilesystemIdentity } from "../../argv-grammar.js";
4
4
  import { parseWaitCommandTokens } from "../../argv-descriptor.js";
5
5
  import { getRecordCommandOperands } from "../../command-taxonomy.js";
@@ -10,14 +10,18 @@ function isSingleScreenshotPathToken(token) {
10
10
  return false;
11
11
  return explicitlyRelative || token.includes("/") || SCREENSHOT_IMAGE_EXTENSIONS.some((extension) => token.endsWith(extension));
12
12
  }
13
- function getScreenshotPositionalIndices(commandTokens) {
13
+ export function getScreenshotPositionalIndices(commandTokens) {
14
14
  if (commandTokens[0] !== "screenshot")
15
15
  return [];
16
16
  const positionalIndices = [];
17
17
  for (let index = 1; index < commandTokens.length; index += 1) {
18
18
  const token = commandTokens[index];
19
- if (token === "--full" || token === "-f")
19
+ if (token === "--full" || token === "-f" || token === "--if-changed")
20
20
  continue;
21
+ if (token === "--threshold") {
22
+ index += 1;
23
+ continue;
24
+ }
21
25
  positionalIndices.push(index);
22
26
  }
23
27
  return positionalIndices;
@@ -91,6 +95,13 @@ function canonicalizeArtifactPath(absolutePath, platform, seenSymlinks) {
91
95
  export function canonicalizeExplicitArtifactDestination(cwd, destination, platform = process.platform) {
92
96
  return canonicalizeArtifactPath(resolve(cwd, destination), platform, new Set());
93
97
  }
98
+ export function getRecordContactSheetDestination(commandTokens) {
99
+ const path = getRecordCommandOperands(commandTokens).path;
100
+ if (!path || !commandTokens.some((token) => token === "--contact-sheet" || token === "--contact-sheet-threshold"))
101
+ return undefined;
102
+ const extension = extname(path);
103
+ return extension ? `${path.slice(0, -extension.length)}.contact-sheet.png` : undefined;
104
+ }
94
105
  export function getExplicitArtifactDestination(commandTokens) {
95
106
  const command = commandTokens[0];
96
107
  const subcommand = commandTokens[1];
@@ -11,7 +11,7 @@ function normalizeAccessibleName(name) {
11
11
  function getClickDispatchProbeTarget(commandTokens, refSnapshot) {
12
12
  if (commandTokens[0] !== "click" || commandTokens.includes("--new-tab"))
13
13
  return undefined;
14
- const selector = commandTokens[1];
14
+ const selector = commandTokens.slice(1).find((token) => token !== "--human");
15
15
  if (!selector || selector.startsWith("-"))
16
16
  return undefined;
17
17
  const refId = parseRefId(selector);
@@ -31,6 +31,9 @@ function getClickDispatchProbeTarget(commandTokens, refSnapshot) {
31
31
  function getEvalResultRecord(data) {
32
32
  return isRecord(data) && isRecord(data.result) ? data.result : undefined;
33
33
  }
34
+ function getClickDispatchIdentityAttribute(probe) {
35
+ return `data-pi-click-dispatch-${probe.marker.toLowerCase()}`;
36
+ }
34
37
  function buildClickDispatchProbeInstallScript(probe) {
35
38
  const target = probe.target;
36
39
  const resolveTarget = target.kind === "selector"
@@ -124,8 +127,14 @@ const listeners = eventTypes.map((type) => {
124
127
  document.addEventListener(type, listener, true);
125
128
  return [type, listener];
126
129
  });
127
- state.cleanup = () => listeners.forEach(([type, listener]) => document.removeEventListener(type, listener, true));
130
+ const identityAttribute = ${JSON.stringify(getClickDispatchIdentityAttribute(probe))};
131
+ state.removeIdentityMarker = () => element.removeAttribute(identityAttribute);
132
+ state.cleanup = () => {
133
+ state.removeIdentityMarker();
134
+ listeners.forEach(([type, listener]) => document.removeEventListener(type, listener, true));
135
+ };
128
136
  window[marker] = state;
137
+ element.setAttribute(identityAttribute, marker);
129
138
  return { status: "installed", marker, target: state.target };
130
139
  })()`;
131
140
  }
@@ -200,9 +209,33 @@ export async function prepareClickDispatchProbe(options) {
200
209
  if (!target)
201
210
  return undefined;
202
211
  const probe = { marker: `${CLICK_DISPATCH_MARKER_PREFIX}${Date.now().toString(36)}_${Math.random().toString(36).slice(2)}`, target };
203
- const installData = await runSessionCommandData({ args: ["eval", "--stdin"], cwd: options.cwd, namespace: options.namespace, sessionName: options.sessionName, signal: options.signal, stdin: buildClickDispatchProbeInstallScript(probe) });
204
- const installResult = getEvalResultRecord(installData);
205
- return installResult?.status === "installed" ? probe : undefined;
212
+ let prepared = false;
213
+ try {
214
+ const installData = await runSessionCommandData({ args: ["eval", "--stdin"], cwd: options.cwd, namespace: options.namespace, sessionName: options.sessionName, signal: options.signal, stdin: buildClickDispatchProbeInstallScript(probe) });
215
+ if (getEvalResultRecord(installData)?.status !== "installed")
216
+ return undefined;
217
+ // Name matching and in-page XPath only find candidates. Native resolution
218
+ // must prove identity, including frame scope, before missing events mean failure.
219
+ const selector = target.kind === "accessible" ? `@${target.refId}` : target.kind === "xpath" ? `xpath=${target.selector}` : target.selector;
220
+ const identity = await runSessionCommandData({ args: ["get", "attr", selector, getClickDispatchIdentityAttribute(probe)], cwd: options.cwd, namespace: options.namespace, sessionName: options.sessionName, signal: options.signal });
221
+ if (!isRecord(identity) || identity.value !== probe.marker)
222
+ return undefined;
223
+ const removed = await runSessionCommandData({ args: ["eval", "--stdin"], cwd: options.cwd, namespace: options.namespace, sessionName: options.sessionName, signal: options.signal, stdin: `(() => {
224
+ const state = window[${JSON.stringify(probe.marker)}];
225
+ if (!state || typeof state.removeIdentityMarker !== "function") return { status: "probe-missing" };
226
+ state.removeIdentityMarker();
227
+ return { status: "identity-marker-removed" };
228
+ })()` });
229
+ if (getEvalResultRecord(removed)?.status !== "identity-marker-removed")
230
+ return undefined;
231
+ prepared = !options.signal?.aborted;
232
+ return prepared ? probe : undefined;
233
+ }
234
+ finally {
235
+ // Even a lost/aborted install response may have left listeners in-page.
236
+ if (!prepared)
237
+ await cleanupClickDispatchProbe({ ...options, probe });
238
+ }
206
239
  }
207
240
  function getClickDispatchScrollContainerDiagnostic(result) {
208
241
  const target = isRecord(result.target) ? result.target : undefined;
@@ -1,16 +1,32 @@
1
1
  import { runAgentBrowserProcess, withAttachedBrowserSessionContext, withChromeStartupArgs } from "../../process.js";
2
2
  import { isRecord } from "../../parsing.js";
3
+ import { collectNativeWebMcp, getNativeWebMcpCatalog } from "../../webmcp-observation.js";
4
+ import { isPlainTextInspectionArgs, redactSensitiveValue } from "../../runtime.js";
5
+ import { formatWebMcpCatalogUpdate } from "../../results/presentation/common.js";
3
6
  import { withOwnedManagedSessionContext } from "../../managed-session-restore.js";
4
7
  import { cleanupClickDispatchProbe } from "./click-dispatch.js";
5
8
  import { applyBrowserRunStatePatch, getSessionContextKey } from "./session-state.js";
6
- import { buildMissingBinaryFailureResult } from "./final-result.js";
9
+ import { buildJsonVisibleContent, buildMissingBinaryFailureResult } from "./final-result.js";
7
10
  import { prepareBrowserRun } from "./prepare.js";
8
11
  import { processBrowserOutput } from "./process-output.js";
9
12
  export { closeManagedSession } from "./managed-session-daemon-policy.js";
10
13
  export { getSessionContextKey } from "./session-state.js";
11
14
  export async function runAgentBrowserTool(options) {
12
- const result = await withChromeStartupArgs(options.input.persistentChromeArgs, () => withAttachedBrowserSessionContext(options.preserveAttachedBrowserSession === true, () => runAgentBrowserToolInContext(options)));
13
- const details = isRecord(result.details) ? result.details : undefined;
15
+ const observed = await collectNativeWebMcp(() => withChromeStartupArgs(options.input.persistentChromeArgs, () => withAttachedBrowserSessionContext(options.preserveAttachedBrowserSession === true, () => runAgentBrowserToolInContext(options))));
16
+ const result = observed.result;
17
+ let details = isRecord(result.details) ? result.details : undefined;
18
+ if (observed.catalog) {
19
+ const catalog = redactSensitiveValue(observed.catalog);
20
+ details = { ...details, webMcpCatalog: catalog };
21
+ result.details = details;
22
+ if (!options.input.toolArgs.includes("--json") && JSON.stringify(getNativeWebMcpCatalog(details.data)) !== JSON.stringify(catalog)) {
23
+ const notice = formatWebMcpCatalogUpdate(catalog);
24
+ if (result.content[0]?.type === "text")
25
+ result.content[0] = { ...result.content[0], text: `${result.content[0].text}\n\n${notice}` };
26
+ else
27
+ result.content.push({ type: "text", text: notice });
28
+ }
29
+ }
14
30
  const sessionKey = getSessionContextKey(typeof details?.sessionName === "string" ? details.sessionName : undefined, typeof details?.namespace === "string" ? details.namespace : undefined);
15
31
  const page = options.state.sessionPageState.get(sessionKey);
16
32
  return page.tabReopenPending === undefined ? result : {
@@ -22,7 +38,17 @@ async function runAgentBrowserToolInContext(options) {
22
38
  const preparedResult = await prepareBrowserRun(options);
23
39
  applyBrowserRunStatePatch(options.state, preparedResult.kind === "ready" ? preparedResult.prepared.statePatch : preparedResult.statePatch);
24
40
  if (preparedResult.kind === "early-result") {
25
- return preparedResult.result;
41
+ const result = preparedResult.result;
42
+ if (options.input.toolArgs.includes("--json") && !isPlainTextInspectionArgs(options.input.toolArgs)) {
43
+ const details = isRecord(result.details) ? result.details : {};
44
+ const summary = result.content.filter(item => item.type === "text").map(item => item.text).join("\n");
45
+ result.content = buildJsonVisibleContent({
46
+ error: result.isError ? details.validationError ?? summary : null,
47
+ presentation: { content: result.content, data: details.data, summary },
48
+ succeeded: result.isError !== true,
49
+ });
50
+ }
51
+ return result;
26
52
  }
27
53
  const { prepared } = preparedResult;
28
54
  const ownedManagedSession = prepared.ownedManagedSessionContext;
@@ -142,7 +142,13 @@ export async function acquireOwnedManagedSessionDaemonPolicy(options) {
142
142
  }
143
143
  export async function closeManagedSession(options) {
144
144
  const controller = new AbortController();
145
- const timer = setTimeout(() => controller.abort(), options.timeoutMs);
145
+ let phase = "policy coordination";
146
+ let phaseStartedAt = Date.now();
147
+ let timeoutError;
148
+ const timer = setTimeout(() => {
149
+ timeoutError = `Managed-session cleanup timed out after ${options.timeoutMs} ms during ${phase} (${Date.now() - phaseStartedAt} ms in this phase).`;
150
+ controller.abort();
151
+ }, options.timeoutMs);
146
152
  let stdoutSpillPath;
147
153
  const closeArgs = [...(options.namespace !== undefined ? ["--namespace", options.namespace] : []), "--session", options.sessionName, "close"];
148
154
  const policyLock = options.policyLock ?? await acquireManagedSessionPolicyLock({
@@ -153,9 +159,11 @@ export async function closeManagedSession(options) {
153
159
  });
154
160
  if (!policyLock) {
155
161
  clearTimeout(timer);
156
- return "Managed-session policy coordination is unavailable or busy; cleanup did not run. Retry after the current operation finishes or repair the private policy-lock directory.";
162
+ return timeoutError ?? "Managed-session policy coordination is unavailable or busy; cleanup did not run. Retry after the current operation finishes or repair the private policy-lock directory.";
157
163
  }
158
164
  try {
165
+ phase = "daemon inspection";
166
+ phaseStartedAt = Date.now();
159
167
  const daemon = await inspectManagedSessionDaemon({
160
168
  cwd: options.cwd,
161
169
  headedManagedAutosaveInterval: options.headedManagedAutosaveInterval,
@@ -170,6 +178,8 @@ export async function closeManagedSession(options) {
170
178
  const daemonRestoreKey = options.restoreState.getDaemonRestoreKey(options.sessionName, options.namespace);
171
179
  const ownedRestoreKey = !options.restoreState.isDisabled(options.sessionName, options.namespace)
172
180
  && isManagedSessionRestoreKey(daemonRestoreKey) ? daemonRestoreKey : null;
181
+ phase = "native close";
182
+ phaseStartedAt = Date.now();
173
183
  const processResult = await runAgentBrowserProcess({
174
184
  args: closeArgs,
175
185
  cwd: options.cwd,
@@ -179,6 +189,7 @@ export async function closeManagedSession(options) {
179
189
  preserveAttachedBrowserSession: options.preserveAttachedBrowserSession,
180
190
  signal: controller.signal,
181
191
  });
192
+ clearTimeout(timer);
182
193
  stdoutSpillPath = processResult.stdoutSpillPath;
183
194
  if (!processResult.aborted && !processResult.spawnError && processResult.exitCode === 0) {
184
195
  const parsed = await parseAgentBrowserEnvelope({ stdout: processResult.stdout, stdoutPath: processResult.stdoutSpillPath });
@@ -191,7 +202,7 @@ export async function closeManagedSession(options) {
191
202
  statePath: typeof data?.statePath === "string" ? data.statePath : undefined,
192
203
  });
193
204
  }
194
- return getAgentBrowserErrorText({
205
+ return timeoutError ?? getAgentBrowserErrorText({
195
206
  aborted: processResult.aborted,
196
207
  command: "close",
197
208
  effectiveArgs: redactInvocationArgs(closeArgs),
@@ -204,7 +215,7 @@ export async function closeManagedSession(options) {
204
215
  });
205
216
  }
206
217
  catch (error) {
207
- return error instanceof Error ? error.message : String(error);
218
+ return timeoutError ?? (error instanceof Error ? error.message : String(error));
208
219
  }
209
220
  finally {
210
221
  clearTimeout(timer);
@@ -1,6 +1,7 @@
1
1
  import { isRecord } from "../../../parsing.js";
2
2
  import { buildAgentBrowserResultCategoryDetails } from "../../../results/categories.js";
3
- import { redactSensitiveText } from "../../../runtime.js";
3
+ import { redactPresentationData } from "../../../results/presentation/diagnostics.js";
4
+ import { redactInvocationArgs, redactSensitiveText } from "../../../runtime.js";
4
5
  import { buildSessionDetailFields, runSessionCommandData } from "../session-state.js";
5
6
  function parseNetworkRequestsPageFilterRequest(commandTokens) {
6
7
  if (commandTokens[0] !== "network" || commandTokens[1] !== "requests")
@@ -95,9 +96,9 @@ export async function tryNetworkRequestsPageFilter(options) {
95
96
  args: options.redactedArgs,
96
97
  command: "network",
97
98
  compatibilityWorkaround: options.compatibilityWorkaround,
98
- data: filtered.data,
99
+ data: redactPresentationData({ command: "network", subcommand: "requests" }, filtered.data),
99
100
  effectiveArgs: options.effectiveArgs,
100
- networkRequestsPageFilter: { cleanArgs: request.cleanArgs, currentUrl: redactSensitiveText(currentUrl), matchedRows: filtered.matchedRows, mode: request.mode, totalRows: filtered.totalRows },
101
+ networkRequestsPageFilter: { cleanArgs: redactInvocationArgs(request.cleanArgs), currentUrl: redactSensitiveText(currentUrl), matchedRows: filtered.matchedRows, mode: request.mode, totalRows: filtered.totalRows },
101
102
  sessionMode: options.sessionMode,
102
103
  ...buildAgentBrowserResultCategoryDetails({ args: options.effectiveArgs, command: "network", succeeded: true }),
103
104
  ...buildSessionDetailFields(options.sessionName, options.usedImplicitSession, options.namespace, options.managedSessionRestoreDisabled()),
@@ -37,8 +37,7 @@ function buildContainerScrollScript(request) {
37
37
  return Number.isFinite(pixels) && pixels > 0 ? Math.floor(pixels) : Math.max(1, Math.floor(extent * 0.8));
38
38
  };
39
39
  const delta = parseAmount() * (direction === "up" || direction === "left" ? -1 : 1);
40
- if (axis === "x") element.scrollLeft += delta;
41
- else element.scrollTop += delta;
40
+ element.scrollBy({ [axis === "x" ? "left" : "top"]: delta, behavior: "instant" });
42
41
  const after = { scrollLeft: element.scrollLeft, scrollTop: element.scrollTop, scrollHeight: element.scrollHeight, scrollWidth: element.scrollWidth, clientHeight: element.clientHeight, clientWidth: element.clientWidth };
43
42
  const moved = before.scrollLeft !== after.scrollLeft || before.scrollTop !== after.scrollTop;
44
43
  return { status: moved ? "scrolled" : "no-movement", selector, direction, amount: amountToken || undefined, before, after };
@@ -13,6 +13,9 @@ function parseSnapshotFilterRequest(commandTokens) {
13
13
  let search;
14
14
  for (let index = 0; index < commandTokens.length; index += 1) {
15
15
  const token = commandTokens[index];
16
+ // Wrapper filters need a full native tree, not a revision-relative patch.
17
+ if (token === "--delta" || token === "--full")
18
+ continue;
16
19
  if (token === "--viewport")
17
20
  continue;
18
21
  if (token === "--diff")
@@ -9,7 +9,6 @@ import { cleanupElectronLaunchResources } from "../../electron/cleanup.js";
9
9
  import { launchElectronApp } from "../../electron/launch.js";
10
10
  import { pathExists } from "../../fs-utils.js";
11
11
  import { getCompiledSemanticActionSessionPrefix } from "../../input-modes/semantic-action.js";
12
- import { tryDirectAnchorDownload } from "./prepare/direct-anchor-download.js";
13
12
  import { tryNetworkRequestsPageFilter } from "./prepare/network-page-filter.js";
14
13
  import { tryContainerScroll, tryPageScrollTo } from "./prepare/scroll-shims.js";
15
14
  import { trySnapshotFilter } from "./prepare/snapshot-filter.js";
@@ -172,6 +171,8 @@ export async function prepareAgentBrowserArgs(args, stdin, cwd) {
172
171
  }
173
172
  async function repairScreenshotData(options) {
174
173
  const { cwd, data, request } = options;
174
+ if (data.changed === false)
175
+ return { data };
175
176
  const reportedPath = typeof data.path === "string" ? data.path : undefined;
176
177
  const reportedAbsolutePath = reportedPath ? resolve(cwd, reportedPath) : undefined;
177
178
  let status = await pathExists(request.absolutePath) ? "saved" : "missing";
@@ -1003,22 +1004,6 @@ export async function prepareBrowserRun(options) {
1003
1004
  if (pageScrollTo)
1004
1005
  return { kind: "early-result", statePatch, result: pageScrollTo };
1005
1006
  }
1006
- const directAnchorDownload = await tryDirectAnchorDownload({
1007
- artifactManifest: state.artifactManifest,
1008
- commandTokens,
1009
- compatibilityWorkaround,
1010
- cwd,
1011
- effectiveArgs: redactedEffectiveArgs,
1012
- managedSessionRestoreDisabled,
1013
- redactedArgs,
1014
- sessionMode,
1015
- namespace: executionPlan.namespace,
1016
- sessionName: executionPlan.sessionName,
1017
- signal,
1018
- usedImplicitSession: executionPlan.usedImplicitSession,
1019
- });
1020
- if (directAnchorDownload)
1021
- return { kind: "early-result", statePatch: { ...statePatch, artifactManifest: directAnchorDownload.artifactManifest ?? statePatch.artifactManifest }, result: directAnchorDownload.result };
1022
1007
  const processArgs = executionPlan.effectiveArgs;
1023
1008
  const processStdin = preparedArgs.stdin ?? runtimeToolStdin;
1024
1009
  const clickDispatchProbe = compiledElectron === undefined