pi-agent-browser-native 0.6.12 → 0.6.14

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 +27 -0
  2. package/README.md +24 -20
  3. package/dist/extensions/agent-browser/index.js +24 -10
  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/batch-stdin.js +32 -0
  7. package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.js +14 -3
  8. package/dist/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.js +1 -1
  9. package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +21 -5
  10. package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +15 -4
  11. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/snapshot-filter.js +3 -0
  12. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +20 -6
  13. package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +14 -3
  14. package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +5 -2
  15. package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +4 -2
  16. package/dist/extensions/agent-browser/lib/orchestration/native-session-defaults.js +100 -9
  17. package/dist/extensions/agent-browser/lib/playbook.js +5 -4
  18. package/dist/extensions/agent-browser/lib/process.js +18 -5
  19. package/dist/extensions/agent-browser/lib/recording-reservations.js +8 -1
  20. package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +15 -1
  21. package/dist/extensions/agent-browser/lib/results/presentation/batch.js +18 -9
  22. package/dist/extensions/agent-browser/lib/results/presentation/common.js +3 -0
  23. package/dist/extensions/agent-browser/lib/results/presentation/registry.js +1 -1
  24. package/dist/extensions/agent-browser/lib/results/presentation.js +7 -3
  25. package/dist/extensions/agent-browser/lib/results/snapshot-refs.js +14 -2
  26. package/dist/extensions/agent-browser/lib/results/snapshot.js +12 -5
  27. package/dist/extensions/agent-browser/lib/runtime.js +3 -1
  28. package/dist/extensions/agent-browser/lib/session-page-state.js +7 -3
  29. package/dist/extensions/agent-browser/lib/webmcp-observation.js +28 -0
  30. package/dist/scripts/agent-browser-target.mjs +1 -1
  31. package/docs/ARCHITECTURE.md +16 -5
  32. package/docs/COMMAND_REFERENCE.md +71 -15
  33. package/docs/ELECTRON.md +1 -1
  34. package/docs/RELEASE.md +7 -6
  35. package/docs/SUPPORT_MATRIX.md +15 -9
  36. package/docs/TOOL_CONTRACT.md +19 -5
  37. package/docs/platform-smoke.md +3 -3
  38. package/package.json +1 -1
  39. package/scripts/agent-browser-capability-baseline.mjs +34 -3
  40. package/scripts/agent-browser-target.mjs +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,33 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.6.14 - 2026-09-17
6
+
7
+ ### Changed
8
+
9
+ - Recommend `agent-browser` 0.38.1 while retaining the stable 0.35.0 minimum and separate upstream installation.
10
+ - Support native delta snapshots and persistent refs without treating partial revisions as empty pages; keep native delta baselines upstream-owned.
11
+ - Preserve conditional screenshot options and skipped-image results without inventing files or attaching old images.
12
+ - 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.
13
+
14
+ ### Fixed
15
+
16
+ - 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.
17
+
18
+ ## 0.6.13 - 2026-09-14
19
+
20
+ ### Fixed
21
+
22
+ - Suppress Chrome's extra startup window using stock `--no-startup-window`, preserving headless defaults and composing caller launch arguments. URL-less `open` now uses native lazy URL reads so active pages keep their browser, profile, and URL across direct, batch, and script calls. External engines and attachments remain unchanged.
23
+
24
+ ### Changed
25
+
26
+ - Give ordinary browser calls a native session and restore key per root Pi session. Parent/descendant groups share one browser without a global queue or child-exit teardown. Scope named Chrome profile bootstrap to automatic roots; preserve explicit browsers, script, and Electron isolation.
27
+
28
+ ### Validation
29
+
30
+ - Start managed-lifecycle release checks explicitly in fresh mode; retain root-session ownership without leaving test browsers open.
31
+
5
32
  ## 0.6.12 - 2026-09-13
6
33
 
7
34
  ### Fixed
package/README.md CHANGED
@@ -85,7 +85,7 @@ The result is optimized for agent work:
85
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` |
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
 
@@ -317,6 +319,10 @@ Open a page and inspect it (first-call recipe: open → snapshot -i → interact
317
319
 
318
320
  Chromium launch switches belong in the value of `--args`, for example `{ "args": ["--args", "--no-sandbox", "open", "https://example.com"], "sessionMode": "fresh" }` when that switch is needed. A bare `--no-sandbox` in the command slot is an unknown command; in an `open` / `goto` / `navigate` option position, upstream ignores it. The wrapper diagnoses those forms without rejecting literal text, select values or paths in other commands. For batches, put `--args` in top-level `args` before `batch`, not inside a row.
319
321
 
322
+ Local Chrome startup uses the stock binary's `--args --no-startup-window` capability to suppress Chrome's extra startup window. Headless remains the default. Caller launch arguments keep native CLI → environment → config precedence and follow this default; browsers started before this change with custom arguments may restart once when their native launch configuration changes. Ordinary follow-ups do not resend bootstrap settings. CDP, providers, Electron, and Lightpanda remain native-owned; no tabs are deleted or profiles edited by this behavior.
323
+
324
+ `open` without a URL uses native `get url`: it launches when needed and otherwise preserves the current page. This also applies to effective batch rows and script calls. `details.args` preserves the request, while `effectiveArgs`, command, URL data, and lifecycle describe the actual native operation. Use `open about:blank` only when you intend to navigate there. Help/version requests remain unchanged.
325
+
320
326
  Watch a browser window during a demo, QA run, or user-completed login by adding upstream's global `--headed` flag on the first launch. Use `sessionMode: "fresh"` if a managed session may already exist, because headed/headless state is launch-scoped. A successful first/fresh local wrapper-managed headed launch, including a launch inside `batch`, returns `details.browserWindow = { mode: "headed", ownership: "wrapper-managed", sessionName, visibility: "unverified" }` and one visible handoff sentence; CDP, auto-connect, provider, and Electron attachments do not. This proves that the wrapper requested and upstream launched headed mode, not that the OS window is visible on the user's display; remote, container, or virtual-display setups can still hide it. After the user finishes in the window, continue with `sessionMode: "auto"`.
321
327
 
322
328
  ```json
@@ -608,33 +614,31 @@ After a successful unnamed fresh launch, later default `sessionMode: "auto"` cal
608
614
 
609
615
  ### Shared browser defaults
610
616
 
611
- To reuse one signed-in browser across Pi sessions, configure native `agent-browser` defaults in `~/.agent-browser/config.json`:
617
+ Ordinary browser calls use one native named browser per **root Pi session**. A parent and its subagents share that browser; unrelated roots get different names and can browse concurrently. `pi-subagents` supplies `PI_SUBAGENT_ROOT_SESSION_ID` through its existing launch records, including detached runs, delegated forks, grandchildren, and revived children. Ordinary Pi new/fork/clone sessions get their own root identity; resuming the same root or changing its cwd keeps its browser name.
618
+
619
+ Coordinate multi-call navigation within the group using existing subagent/intercom tools. Native commands are serialized by each browser daemon; wrapper helper queues are process-local. There is no global focus queue. Parent or child exit does not close the group browser. Native idle policy applies; explicitly close only your group's browser when the group is finished.
620
+
621
+ To bootstrap new root browsers from an authenticated normal Chrome profile, set the existing **Pi package** config in `~/.pi/config/pi-agent-browser-native/config.json`:
612
622
 
613
623
  ```json
614
624
  {
615
- "session": "shared-work",
616
- "namespace": "",
617
- "profile": "/absolute/path/to/persistent-browser-profile",
618
- "headed": true
625
+ "browser": {
626
+ "defaultProfile": { "name": "Default", "policy": "always" },
627
+ "executablePath": "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
628
+ }
619
629
  }
620
630
  ```
621
631
 
622
- Use a dedicated full profile directory path, not a copied Chrome profile name. Add native `executablePath` if needed. Ordinary `args`, `semanticAction`, `job`, `qa`, and lookup calls then select that same caller-owned browser without repeating flags. This is separate from the advisory Pi package `browser.defaultProfile` setting. Existing native project config, environment and per-call flags keep their precedence; `--config` or `AGENT_BROWSER_CONFIG` replaces user/project config discovery.
623
-
624
- Pi quit does not close a caller-owned shared browser or impose its implicit 15-minute idle timeout. Native idle policy applies, including its default headed-browser exemption; explicit `--idle-timeout` applies to every helper in that call too. Keep native idle settings consistent between calls because changing them can restart the daemon. `AGENT_BROWSER_SOCKET_DIR` is honored for caller-owned sessions; the wrapper-specific socket override still takes priority.
632
+ For inactive automatic roots, global/override `always` profile **names** and the configured executable bootstrap native launch through a scoped subprocess environment shared with helpers. Active roots retain native launch settings instead of resending defaults that could replace an explicitly profiled browser; their existing restore key is preserved, including follow-ups naming the same root browser. Native Chrome profile names use independent temporary copies, not the source profile's lock. Profile paths and other profile policies remain advisory; project guidance does not change a root's launch settings when a child changes cwd. Explicit unrelated sessions, native launch/config/environment choices, isolated `script`, and Electron launches do not receive the root bootstrap defaults. Do not set a global native profile merely to enable this feature: it can change the launch configuration of existing unrelated browsers.
625
633
 
626
- A configured native session, like explicit `--session`, takes precedence over `sessionMode: "fresh"`. To select a different browser, override the native session **and** profile/config as appropriate; changing only the name can contend for the same profile. Without a configured session, implicit ownership and fresh rotation are unchanged. `script` remains disposable: its helpers and cleanup use an empty temporary native config, never your user/project profile defaults. Electron launch retains its own isolated app lifecycle and generated session, even with a native session environment default. With a shared native default configured, target Electron's returned `sessionName` explicitly for browser follow-ups.
634
+ Each automatic root has its own stable native restore key. Native save/restore preserves cookies, localStorage, and sessionStorage across normal close/restart; it does not merge auth between roots or save new IndexedDB-only credentials, service workers, or page-memory grants. Named profile copies can bootstrap existing IndexedDB, but are discarded on close and never write back to the communal profile. Verify the actual application after restart, not just a successful restore receipt. If an application needs unsupported state, use a supported persistent **per-root** profile and collect required sign-ins together; never point concurrent roots at one writable profile directory. Sites can expire sessions or require a fresh human challenge.
627
635
 
628
- Use `{ "args": ["session", "info"] }` for one read-only preflight of the selected native session (or add `--namespace` / `--session` explicitly). It separates daemon activity/PID from verified browser liveness, Chrome PID, exact profile, tabs and native launched/attached ownership. Pi cleanup ownership is separate: a caller-owned shared browser is not closed by Pi, while an explicit name can still identify a wrapper-owned browser. Missing native fields remain unknown, never inferred from config or a live daemon.
636
+ Explicit native `session` defaults (config or `AGENT_BROWSER_SESSION`) still override automatic root selection, including `sessionMode: "fresh"`. They intentionally share whichever browser the caller selected. Per-call session/namespace flags retain precedence. Ordinary launch options such as `--profile` or `--executable-path` still use the root-group name. Repeat explicit options when deliberately relaunching a persistent per-root profile after close. Explicit `fresh` and attachment choices retain their existing managed-session lifecycle; later auto calls follow that selected managed session. Use `session info` with the reported `sessionName` for read-only native daemon/browser evidence. Unknown native fields remain unknown.
629
637
 
630
638
  Policy-required URL reads keep the two-call read → confirm/deny flow. Returned actions name the actual native namespace/session, including `default`; routing survives transcript resume and branch changes. Only explicit-read provenance plus native `capabilities.readRequiresConfirmation: true` permits matching confirm/deny without page helpers. This capability includes native confirmation-ID checking. Legacy prompts retain correct routing but normal page checks; DOM or content-shaped prompts never receive the exemption.
631
639
 
632
640
  Full live identity, browser-independent native reads/confirmations and detailed recording receipts require the companion upstream fixes; the current recommended release does not yet supply all of them. Older supported versions remain accepted with unavailable evidence marked unknown. The extension does not upgrade or restart your installed browser to obtain these fields.
633
641
 
634
- Coordinate tabs/navigation between agents sharing a browser; the wrapper's per-session queue is local to one Pi process. Persistent profile storage preserves browser data, not a promise of permanent website login: sites can expire or revoke sessions and require a new human challenge. Standing permission avoids repeated permission requests, but cannot change those site policies.
635
-
636
- Before enabling global native profile defaults, update and fully restart every participating Pi runtime. Older loaded wrappers can still let disposable `script` launches read HOME config; editing this source or running `/reload` does not replace their cached code. Coordinate the browser cutover too: moving an existing daemon from the old wrapper's explicit 15-minute idle timeout to native idle policy changes its launch fingerprint and can restart it. Do not change that policy while another agent is using the browser.
637
-
638
642
  ### Profile selection
639
643
 
640
644
  The wrapper does not clone profiles or hide what upstream Chrome/Chromium profile or executable you chose. Passing `--profile` or `--executable-path` is an explicit upstream `agent-browser` choice. Visible page content from real profiles is model-visible and may persist in transcripts or saved artifacts; redaction protects credential-like cookie/storage/auth values, not ordinary page text you asked the browser to read. Redactions use `[REDACTED]` (URL-encoded in parsed URLs); ordinary technical phrases such as `bearer token` stay intact. URL redaction includes `code`, `authorization_session_id`, and auth-context `state` / `nonce` across visible text, details, spills, and `outputPath` exports. URLs needing no redaction keep their original spelling.
@@ -756,7 +760,7 @@ The opt-in real-upstream suite is separate because it drives a real browser inst
756
760
  npm run verify -- real-upstream
757
761
  ```
758
762
 
759
- 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).
760
764
 
761
765
  A deterministic host-only live-browser wrapper smoke is available without an LLM choosing tool calls:
762
766
 
@@ -789,12 +793,12 @@ npm run verify -- release
789
793
  5. Screenshots, downloads, recordings, traces, profiles, and spill files are normalized as Pi-visible artifacts where possible.
790
794
  6. Generated playbook text in docs and tool metadata stays aligned with `extensions/agent-browser/lib/playbook.ts`.
791
795
 
792
- 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.
793
797
 
794
798
  ## Current limits
795
799
 
796
800
  - Published pre-1.0 package.
797
- - 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.
798
802
  - Does not bundle `agent-browser`; users install it separately.
799
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.
800
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
  }
@@ -1370,7 +1373,12 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1370
1373
  }
1371
1374
  if (resolvedInput.kind !== "script")
1372
1375
  await beforeExecute?.(nativeToolCallId, { ...ctx, signal });
1373
- return withNativeSessionDefaults(resolvedInput, ctx.cwd, signal, async (resolvedInput) => {
1376
+ const runtimeBrowserConfig = loadAgentBrowserConfigSync({ cwd: ctx.cwd, includeProjectConfig: shouldIncludeProjectConfig(ctx) });
1377
+ const rootProfile = runtimeBrowserConfig.trustedBrowserDefaultProfile;
1378
+ const pendingReadConfirmation = sessionPageState.findReadConfirmation(resolvedInput.toolArgs, resolveAgentBrowserNamespace(resolvedInput.toolArgs, getAgentBrowserProcessEnvironment().AGENT_BROWSER_NAMESPACE));
1379
+ const rootSessionId = process.env.PI_SUBAGENT_CHILD === "1" && process.env.PI_SUBAGENT_ROOT_SESSION_ID
1380
+ ? process.env.PI_SUBAGENT_ROOT_SESSION_ID : ctx.sessionManager.getSessionId();
1381
+ return withNativeSessionDefaults(resolvedInput, ctx.cwd, signal, async (resolvedInput, withLaunchDefaults) => {
1374
1382
  const readConfirmation = sessionPageState.findReadConfirmation(resolvedInput.toolArgs, resolveAgentBrowserNamespace(resolvedInput.toolArgs, getAgentBrowserProcessEnvironment().AGENT_BROWSER_NAMESPACE));
1375
1383
  if (readConfirmation)
1376
1384
  resolvedInput = { ...resolvedInput, toolArgs: scopeReadConfirmationArgs(resolvedInput.toolArgs, readConfirmation) };
@@ -1614,7 +1622,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1614
1622
  const callerOwnedSessionQueueKey = !serializeBrowserCommand && explicitSessionName
1615
1623
  ? getSessionContextKey(explicitSessionName, callerOwnedSessionNamespace) ?? explicitSessionName
1616
1624
  : undefined;
1617
- const runBrowserCommand = async () => {
1625
+ const runBrowserCommand = async (daemonInactive) => {
1618
1626
  flushRecordingReservations();
1619
1627
  const branchRestoreGenerationAtStart = branchRestoreGeneration;
1620
1628
  const generationAtStart = branchStateGeneration;
@@ -1664,6 +1672,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1664
1672
  ?? getSessionContextKey(browserRunState.managedSessionName, browserRunState.managedSessionNamespace);
1665
1673
  const attachedSessionKnown = reusableSessionKey !== undefined && attachedSessionKeys.has(reusableSessionKey);
1666
1674
  let result = await runAgentBrowserTool({
1675
+ daemonInactive,
1667
1676
  ctx,
1668
1677
  cwd: ctx.cwd,
1669
1678
  electronPostCommandStatusSettleMs: ELECTRON_POST_COMMAND_STATUS_SETTLE_MS,
@@ -1771,6 +1780,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1771
1780
  return applyAgentBrowserOutputPath({ cwd: ctx.cwd, outputPath, preserveTextContent: Array.isArray(params.args) && params.args.includes("--json"), result: warnRecordingPersistence(result) });
1772
1781
  };
1773
1782
  const closesAllSessions = commandClosesAllSessions(toolArgs, resolvedInput.toolStdin);
1783
+ const runWithLaunchDefaults = () => withLaunchDefaults ? withLaunchDefaults(runBrowserCommand) : runBrowserCommand();
1774
1784
  const runWithinSessionQueue = () => {
1775
1785
  if (closesAllSessions)
1776
1786
  return managedSessionExecutionQueue.run(() => {
@@ -1783,13 +1793,13 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1783
1793
  sessionMode: params.sessionMode ?? "auto",
1784
1794
  stdin: resolvedInput.toolStdin,
1785
1795
  });
1786
- return callerOwnedSessionExecutionQueues.runExclusive(plan.namespace, runBrowserCommand);
1796
+ return callerOwnedSessionExecutionQueues.runExclusive(plan.namespace, runWithLaunchDefaults);
1787
1797
  });
1788
1798
  if (serializeBrowserCommand)
1789
- return managedSessionExecutionQueue.run(runBrowserCommand);
1799
+ return managedSessionExecutionQueue.run(runWithLaunchDefaults);
1790
1800
  return callerOwnedSessionQueueKey
1791
- ? callerOwnedSessionExecutionQueues.run(callerOwnedSessionQueueKey, callerOwnedSessionNamespace, runBrowserCommand)
1792
- : runBrowserCommand();
1801
+ ? callerOwnedSessionExecutionQueues.run(callerOwnedSessionQueueKey, callerOwnedSessionNamespace, runWithLaunchDefaults)
1802
+ : runWithLaunchDefaults();
1793
1803
  };
1794
1804
  if (!commandTouchesArtifactLifecycle(toolArgs, resolvedInput.toolStdin, outputPath))
1795
1805
  return runWithinSessionQueue();
@@ -1814,6 +1824,10 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1814
1824
  validationError: artifactValidationError,
1815
1825
  }));
1816
1826
  });
1827
+ }, managedSessionActive || freshSessionOrdinal > 0 || params.sessionMode === "fresh" || pendingReadConfirmation ? undefined : {
1828
+ id: rootSessionId,
1829
+ profile: rootProfile?.policy === "always" && !/[\\/~]/.test(rootProfile.name) ? rootProfile.name : undefined,
1830
+ executablePath: runtimeBrowserConfig.trustedBrowserExecutablePath,
1817
1831
  });
1818
1832
  },
1819
1833
  };
@@ -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,3 +1,35 @@
1
+ import { projectUpstreamGlobalFlags } from "../argv-grammar.js";
2
+ /** Bare open's native launch action drops prior launch options. URL reads lazily launch without navigating. */
3
+ export function normalizeUrlLessOpen(args, stdin, batchStep = false) {
4
+ const { tokens, indices } = batchStep ? { tokens: args, indices: args.map((_, index) => index) } : projectUpstreamGlobalFlags(args);
5
+ if (tokens[0] === "open" && !tokens.slice(1).some(token => !token.startsWith("--"))) {
6
+ const index = indices[0];
7
+ return { args: [...args.slice(0, index), "get", "url", ...args.slice(index + 1)], stdin };
8
+ }
9
+ if (tokens[0] !== "batch")
10
+ return { args, stdin };
11
+ const rawSteps = tokens.slice(1).flatMap((token, offset) => {
12
+ const step = token === "--bail" ? undefined : parseBatchCommandArgument(token).step;
13
+ return step ? [{ index: indices[offset + 1], step }] : [];
14
+ });
15
+ if (tokens.slice(1).some(token => token !== "--bail")) {
16
+ let normalized = args;
17
+ for (const { index, step } of rawSteps) {
18
+ const row = normalizeUrlLessOpen(step, undefined, true).args;
19
+ if (row === step)
20
+ continue;
21
+ if (normalized === args)
22
+ normalized = [...args];
23
+ normalized[index] = row.map(token => `'${token.replaceAll("'", "'\\''")}'`).join(" ");
24
+ }
25
+ return { args: normalized, stdin };
26
+ }
27
+ const steps = parseUserBatchStdin(stdin).steps;
28
+ if (!steps?.length)
29
+ return { args, stdin };
30
+ const normalized = steps.map(step => normalizeUrlLessOpen(step, undefined, true).args);
31
+ return { args, stdin: normalized.some((step, index) => step !== steps[index]) ? JSON.stringify(normalized) : stdin };
32
+ }
1
33
  const BATCH_STDIN_EXAMPLE = ' Example: { "args": ["batch"], "stdin": "[[\\"get\\",\\"title\\"],[\\"get\\",\\"url\\"]]" }';
2
34
  // Mirror upstream commands::shell_words_split so policy inspection sees the same argv.
3
35
  export function parseBatchCommandArgument(command) {
@@ -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);
@@ -1,5 +1,8 @@
1
- import { runAgentBrowserProcess, withAttachedBrowserSessionContext } from "../../process.js";
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 { 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";
@@ -9,8 +12,21 @@ 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 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 : {
@@ -29,7 +45,7 @@ async function runAgentBrowserToolInContext(options) {
29
45
  return await withOwnedManagedSessionContext(ownedManagedSession, async () => {
30
46
  try {
31
47
  const artifactRunStartedAtMs = Date.now();
32
- const processResult = await runAgentBrowserProcess({
48
+ const processResult = await withChromeStartupArgs(prepared.chromeStartupArgs, () => runAgentBrowserProcess({
33
49
  args: prepared.processArgs,
34
50
  browserIndependentReadConfirmation: prepared.readConfirmation !== undefined,
35
51
  cwd: options.cwd,
@@ -43,7 +59,7 @@ async function runAgentBrowserToolInContext(options) {
43
59
  signal: options.signal,
44
60
  stdin: prepared.processStdin,
45
61
  timeoutMs: prepared.processTimeoutMs,
46
- });
62
+ }));
47
63
  const missingBinaryResult = await buildMissingBinaryFailureResult({
48
64
  compatibilityWorkaround: prepared.compatibilityWorkaround,
49
65
  electronLaunch: prepared.electronLaunch,
@@ -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);
@@ -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")
@@ -2,7 +2,9 @@ import { copyFile, mkdir } from "node:fs/promises";
2
2
  import { dirname, resolve } from "node:path";
3
3
  import { extractExplicitSessionName, getBooleanFlagValue, isUpstreamEnvFlagEnabled, projectUpstreamGlobalFlags, resolveAgentBrowserNamespace } from "../../argv-grammar.js";
4
4
  import { isCloseCommand } from "../../command-taxonomy.js";
5
- import { isBrowserIndependentRead } from "../../command-policy.js";
5
+ import { isBrowserIndependentRead, needsManagedSession } from "../../command-policy.js";
6
+ import { parseArgvDescriptor } from "../../argv-descriptor.js";
7
+ import { prepareAgentBrowserSpawnArgs, withChromeStartupArgs } from "../../process.js";
6
8
  import { cleanupElectronLaunchResources } from "../../electron/cleanup.js";
7
9
  import { launchElectronApp } from "../../electron/launch.js";
8
10
  import { pathExists } from "../../fs-utils.js";
@@ -22,7 +24,7 @@ import { buildExecutionPlan, canUseHeadlessCompatibilityUserAgent, createFreshSe
22
24
  import { buildOwnedManagedSessionRestoreContext, resolveExplicitAutosaveInterval, withOwnedManagedSessionContext, } from "../../managed-session-restore.js";
23
25
  import { getAgentBrowserProcessEnvironment } from "../../process-environment.js";
24
26
  import { getExplicitSessionPageVerificationRequirement, getPageTargetValidationError, } from "../../page-target-validation.js";
25
- import { acquireOwnedManagedSessionDaemonPolicy, getRunningHeadedAutosavePolicyChangeError } from "./managed-session-daemon-policy.js";
27
+ import { acquireOwnedManagedSessionDaemonPolicy, getRunningHeadedAutosavePolicyChangeError, inspectManagedSessionDaemon } from "./managed-session-daemon-policy.js";
26
28
  import { buildManagedSessionOutcome, buildSessionDetailFields, buildStaleRefPreflight, getSessionContextKey, findElectronLaunchRecordForSession, extractStringResultField, ensureSessionTabTarget, getGuardedRefUsage, getTraceOwnerGuardMessage, runSessionCommandData, shouldPinSessionTabForCommand, } from "./session-state.js";
27
29
  import { getUpstreamEffectiveBatchSteps, parseBatchStdinJsonArray } from "../batch-stdin.js";
28
30
  import { buildElectronHostFailureResult, formatAgentBrowserNextActionsText, getElectronLaunchFailureCategory, redactRecoveryHint } from "./final-result.js";
@@ -170,6 +172,8 @@ export async function prepareAgentBrowserArgs(args, stdin, cwd) {
170
172
  }
171
173
  async function repairScreenshotData(options) {
172
174
  const { cwd, data, request } = options;
175
+ if (data.changed === false)
176
+ return { data };
173
177
  const reportedPath = typeof data.path === "string" ? data.path : undefined;
174
178
  const reportedAbsolutePath = reportedPath ? resolve(cwd, reportedPath) : undefined;
175
179
  let status = await pathExists(request.absolutePath) ? "saved" : "missing";
@@ -551,7 +555,16 @@ export async function prepareBrowserRun(options) {
551
555
  };
552
556
  }
553
557
  }
554
- return await withOwnedManagedSessionContext(ownedManagedSession, async () => {
558
+ let chromeStartupArgs;
559
+ if (options.input.chromeStartupArgs !== undefined && !options.preserveAttachedBrowserSession && !browserIndependent
560
+ && !executionPlan.validationError && !isCloseCommand(executionPlan.commandInfo.command)
561
+ && needsManagedSession(parseArgvDescriptor(preparedArgs.args), runtimeToolStdin)) {
562
+ const inactive = ownedManagedSession ? managedSessionDaemonInactive : options.daemonInactive
563
+ ?? (executionPlan.sessionName !== undefined && (await inspectManagedSessionDaemon({ cwd, signal, sessionName: executionPlan.sessionName, namespace: executionPlan.namespace })).status === "inactive");
564
+ if (inactive || options.input.configuredChromeLaunch)
565
+ chromeStartupArgs = options.input.chromeStartupArgs;
566
+ }
567
+ return await withChromeStartupArgs(chromeStartupArgs, () => withOwnedManagedSessionContext(ownedManagedSession, async () => {
555
568
  const managedSessionRestoreDisabled = () => state.managedSessionRestoreState.isDisabled(executionPlan.sessionName, executionPlan.namespace);
556
569
  const sessionStateKey = getSessionContextKey(executionPlan.sessionName, executionPlan.namespace);
557
570
  const priorSessionPageState = sessionPageState.get(sessionStateKey);
@@ -737,7 +750,7 @@ export async function prepareBrowserRun(options) {
737
750
  requirement: livePageRequirement,
738
751
  stdin: runtimeToolStdin,
739
752
  });
740
- const redactedEffectiveArgs = redactInvocationArgs(executionPlan.effectiveArgs);
753
+ const redactedEffectiveArgs = redactInvocationArgs(prepareAgentBrowserSpawnArgs(executionPlan.effectiveArgs, undefined, options.preserveAttachedBrowserSession, chromeStartupArgs));
741
754
  const redactedRecoveryHint = redactRecoveryHint(executionPlan.recoveryHint);
742
755
  const compatibilityWorkaround = executionPlan.compatibilityWorkaround;
743
756
  const statePatch = executionPlan.managedSessionName === freshSessionName
@@ -1022,7 +1035,7 @@ export async function prepareBrowserRun(options) {
1022
1035
  catch { }
1023
1036
  }
1024
1037
  const processTimeoutMs = options.params.timeoutMs ?? getDialogAwareProcessTimeoutMs(commandTokens, promptRefSnapshot, processStdin) ?? getCommandAwareProcessTimeoutMs(commandTokens, processStdin, readTimeoutPageUrl);
1025
- const redactedProcessArgs = redactInvocationArgs(processArgs);
1038
+ const redactedProcessArgs = redactInvocationArgs(prepareAgentBrowserSpawnArgs(processArgs, ownedManagedSession?.compatibilityUserAgent, options.preserveAttachedBrowserSession, chromeStartupArgs));
1026
1039
  const scrollAmount = Number(commandTokens.find((token) => /^\d+(?:\.\d+)?$/.test(token)));
1027
1040
  const shouldProbeScrollNoop = executionPlan.commandInfo.command === "scroll" && executionPlan.startupScopedFlags.length === 0 && (state.managedSessionActive || sessionMode === "fresh") && (!Number.isFinite(scrollAmount) || scrollAmount >= 500);
1028
1041
  const scrollPositionBefore = shouldProbeScrollNoop
@@ -1043,6 +1056,7 @@ export async function prepareBrowserRun(options) {
1043
1056
  return {
1044
1057
  kind: "ready",
1045
1058
  prepared: {
1059
+ chromeStartupArgs,
1046
1060
  commandTokens,
1047
1061
  headedLaunch,
1048
1062
  providerLaunch,
@@ -1091,7 +1105,7 @@ export async function prepareBrowserRun(options) {
1091
1105
  userRequestedJson,
1092
1106
  },
1093
1107
  };
1094
- });
1108
+ }));
1095
1109
  }
1096
1110
  finally {
1097
1111
  if (!managedSessionPolicyLockTransferred)