pi-agent-browser-native 0.6.14 → 0.6.16

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 (27) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +21 -9
  3. package/dist/extensions/agent-browser/index.js +79 -4
  4. package/dist/extensions/agent-browser/lib/electron/cleanup.js +21 -6
  5. package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +47 -11
  6. package/dist/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.js +37 -4
  7. package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +13 -3
  8. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/network-page-filter.js +4 -3
  9. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/scroll-shims.js +1 -2
  10. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +0 -17
  11. package/dist/extensions/agent-browser/lib/playbook.js +5 -5
  12. package/dist/extensions/agent-browser/lib/process-identity.js +2 -1
  13. package/dist/extensions/agent-browser/lib/process.js +56 -20
  14. package/dist/extensions/agent-browser/lib/runtime.js +1 -1
  15. package/dist/extensions/agent-browser/lib/windows-stock-launcher.js +70 -0
  16. package/docs/ARCHITECTURE.md +17 -4
  17. package/docs/COMMAND_REFERENCE.md +6 -6
  18. package/docs/ELECTRON.md +4 -2
  19. package/docs/RELEASE.md +35 -1
  20. package/docs/REQUIREMENTS.md +1 -1
  21. package/docs/SUPPORT_MATRIX.md +18 -6
  22. package/docs/TOOL_CONTRACT.md +13 -9
  23. package/docs/platform-smoke.md +5 -3
  24. package/package.json +10 -6
  25. package/scripts/config.mjs +1 -1
  26. package/scripts/doctor.mjs +2 -2
  27. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/direct-anchor-download.js +0 -142
package/CHANGELOG.md CHANGED
@@ -2,6 +2,42 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.6.16 - 2026-09-21
6
+
7
+ ### Qualification
8
+
9
+ - The owner waived Windows qualification for this rollout on 2026-09-21; Windows failures remain visible and nonblocking, not passed. Linux/macOS, Node floors, official/fork consumer checks and all three final reviewer approvals remain required before merge, release or activation.
10
+ - Track unresolved Windows policy-lock and restored Electron cleanup failures in [#191](https://github.com/fitchmultz/pi-agent-browser-native/issues/191). The unchanged Electron repeat passed once without a product patch; its original cause remains unknown. Earlier passing focused Windows controls are bounded evidence, not full-suite qualification. Failed private Windows candidates are not integrated.
11
+
12
+ ### Fixed
13
+
14
+ - Validate explicit Windows socket directories using native directory semantics instead of requiring POSIX ownership metadata.
15
+ - Start the config CLI correctly on Windows and encoded or symlinked paths using Node's native entrypoint identity, while keeping module imports silent.
16
+ - Redact bearer credentials followed by Windows path separators in diagnostic text.
17
+ - Await Windows process-tree termination before returning from browser timeouts or cancellation.
18
+ - Preserve literal newlines through agent-browser's recognized global npm Windows CMD launcher by invoking that installation's native executable directly; custom and unrecognized launchers retain their existing behavior.
19
+ - Allow ten seconds for native Windows process-identity queries, including PowerShell startup, while preserving the identity format and five-second POSIX budget.
20
+ - Await tracked Electron process exit before removing its profile and reporting cleanup complete.
21
+ - Inspect native Windows process command lines when verifying ownership of restored Electron launches.
22
+ - Recognize native Windows absolute checkout paths when checking for duplicate extension sources.
23
+ - Recheck released managed-session policy claims after PID inspection so a completed owner does not leave a stale busy result.
24
+ - Refresh transient choosing tickets so a later managed-session contender cannot block its predecessor after publishing its ticket.
25
+ - Retry native Windows open-file rename conflicts during owned policy-claim cleanup, with token revalidation and a fresh one-second retry window even after long browser shutdowns.
26
+
27
+ ## 0.6.15 - 2026-09-18
28
+
29
+ ### Fixed
30
+
31
+ - Apply ordinary credential redaction to page/origin/URL-filtered network results before structured details and `outputPath` exports.
32
+ - 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.
33
+ - 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.
34
+ - Scroll CSS containers instantly before measuring movement so smooth-scroll styles cannot cause false no-movement failures.
35
+ - 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.
36
+
37
+ ### Tests
38
+
39
+ - 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.
40
+
5
41
  ## 0.6.14 - 2026-09-17
6
42
 
7
43
  ### Changed
package/README.md CHANGED
@@ -4,6 +4,14 @@ A Pi extension that lets coding agents drive real browser sessions with a native
4
4
 
5
5
  It is for Pi users who want agents to browse sites, inspect pages, click through flows, capture screenshots, use persistent profiles, and handle authenticated web apps without spending context on `agent-browser` CLI ceremony.
6
6
 
7
+ ## Pi release qualification
8
+
9
+ Development host dependencies are pinned to official Pi **0.86.1**, with wildcard runtime peers. `npm run check:compat` checks the installed candidate SDK/CLI identity, typechecks, runs the existing offline tests (including the native Pi pipeline with a controlled provider and fake browser), and tests a packed runtime-only installation through both the SDK and the actual bundled RPC CLI. The fork lane additionally requires the native idle checkpoint hook. It does not substitute a fork SDK while leaving official types or child CLI installed.
10
+
11
+ Run in an empty HOME with a **short** TMPDIR outside your real home. Long temporary paths can exceed macOS's 103-byte Unix socket limit; keep the fixture's socket safety checks intact. Real-browser/checkpoint controls, live upstream help sampling, lifecycle dogfood, and platform qualification remain the separate gates documented below. A Pi-only compatibility run uses no live browser profiles, credentials, or agent-browser fork, and does not certify those external integrations.
12
+
13
+ For the 0.6.16 compatibility rollout, the owner explicitly waived Windows qualification on 2026-09-21. Windows diagnostics and failures remain visible but nonblocking; this is **not** a Windows full-suite pass. Linux/macOS, declared Node floors, official/fork host and consumer-artifact checks remain required. Known Windows failures and follow-up are tracked in [#191](https://github.com/fitchmultz/pi-agent-browser-native/issues/191); see the [current support evidence](docs/SUPPORT_MATRIX.md#current-0616-rollout).
14
+
7
15
  ## Source-of-truth map
8
16
 
9
17
  Start here for install and common usage. For deeper work, use the active docs by purpose:
@@ -75,14 +83,14 @@ The result is optimized for agent work:
75
83
  |---|---|---|
76
84
  | 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
85
  | 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) |
86
+ | 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
87
  | 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
88
  | 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
89
  | 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
90
  | 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
91
  | 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` |
92
+ | 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` |
93
+ | 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
94
  | 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
95
  | 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
96
  | 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` |
@@ -310,7 +318,7 @@ This adds agent guidance for signed-in/account-specific tasks; current releases
310
318
 
311
319
  You usually prompt the agent in natural language. These JSON snippets show the exact native tool shape the agent should use.
312
320
 
313
- 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.
321
+ 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.
314
322
 
315
323
  ```json
316
324
  { "args": ["open", "https://example.com"] }
@@ -395,7 +403,7 @@ Evaluate page JavaScript through stdin. Put the script in the top-level `stdin`
395
403
  { "args": ["eval", "--stdin"], "stdin": "({ title: document.title, url: location.href })", "outputPath": "logs/page-state.json" }
396
404
  ```
397
405
 
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; in that case the save notice lives only in `details.outputFile`.
406
+ 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.
399
407
 
400
408
  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.
401
409
 
@@ -459,7 +467,7 @@ For supported upstream `find` flows, direct selector/ref `click` / `check` / `fi
459
467
  Typical pitfalls:
460
468
 
461
469
  - 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.
462
- - Do not pass `--json` in `args`; the wrapper injects it automatically.
470
+ - Omit `--json` for ordinary prose; include it when you need parseable JSON text.
463
471
  - `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).
464
472
  - 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`.
465
473
  - For `locator: "role"`, pass either `value: "button"` or `role: "button"`; if both are present they must match.
@@ -467,7 +475,7 @@ Typical pitfalls:
467
475
  - 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.
468
476
  - 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)).
469
477
  - 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).
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. 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.
478
+ - 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.
471
479
  - 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.
472
480
  - 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.
473
481
  - 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.
@@ -570,7 +578,7 @@ For local app debugging, `sourceLookup` can gather candidate component/file loca
570
578
 
571
579
  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.
572
580
 
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`. 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.
581
+ `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.
574
582
 
575
583
  ```json
576
584
  { "networkSourceLookup": { "requestId": "req-1", "url": "/api/fail" } }
@@ -583,7 +591,7 @@ For asynchronous exports, use the export control's current snapshot ref (for exa
583
591
  { "args": ["wait", "--download", "/tmp/report.csv"] }
584
592
  ```
585
593
 
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`. 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.
594
+ 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.
587
595
 
588
596
  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.
589
597
 
@@ -639,6 +647,10 @@ Policy-required URL reads keep the two-call read → confirm/deny flow. Returned
639
647
 
640
648
  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.
641
649
 
650
+ ### Native working-session checkpoints
651
+
652
+ On Pi hosts with `session_checkpoint`, an unused or cleanly closed browser integration can qualify for sleep. Live browsers, attachments, scripts, unfinished recordings/cleanup leases, and dirty recording persistence keep compute alive. Pi core owns native journal flush/repair and strict restore; an ongoing journal I/O failure rejects checkpoint acquisition. Checkpointing never closes browsers or changes cleanup ownership. Native save-on-close and per-root restore remain the persistence path; page JavaScript, unsaved forms, and live recordings are not serialized. See [checkpoint boundaries](docs/ARCHITECTURE.md#native-checkpoint-boundary).
653
+
642
654
  ### Profile selection
643
655
 
644
656
  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.
@@ -9,11 +9,12 @@ import { buildExecutionPlan, canUseHeadlessCompatibilityUserAgent, createEphemer
9
9
  import { deleteIdentityKeysInNamespace, extractExplicitNamespace, extractExplicitSessionName, getAgentBrowserSessionIdentityKey, isAgentBrowserSessionIdentityKeyInNamespace, isUpstreamEnvFlagEnabled, resolveAgentBrowserNamespace } from "./lib/argv-grammar.js";
10
10
  import { parseArgvDescriptor } from "./lib/argv-descriptor.js";
11
11
  import { needsManagedSession } from "./lib/command-policy.js";
12
- import { ManagedSessionRestoreState } from "./lib/managed-session-restore.js";
12
+ import { ManagedSessionRestoreState, resolveOwnedManagedSessionContext, withOwnedManagedSessionContext } from "./lib/managed-session-restore.js";
13
13
  import { isRecord } from "./lib/parsing.js";
14
14
  import { runAgentBrowserProcess } from "./lib/process.js";
15
15
  import { getAgentBrowserProcessEnvironment, withIsolatedAgentBrowserEnvironment } from "./lib/process-environment.js";
16
16
  import { withNativeSessionDefaults } from "./lib/orchestration/native-session-defaults.js";
17
+ import { closeManagedSession, inspectManagedSessionDaemon } from "./lib/orchestration/browser-run/managed-session-daemon-policy.js";
17
18
  import { MINIMUM_AGENT_BROWSER_VERSION, SUPPORTED_AGENT_BROWSER_VERSION_LABEL, TARGET_AGENT_BROWSER_VERSION, getAgentBrowserVersionValidationError, parseAgentBrowserVersionOutput, } from "./lib/upstream-version.js";
18
19
  import { buildPromptPolicy, getLatestUserPrompt, shouldAppendBrowserSystemPrompt } from "./lib/prompt-policy.js";
19
20
  import { isCloseAllCommand, isCloseCommand } from "./lib/command-taxonomy.js";
@@ -21,9 +22,8 @@ import { hasLaunchScopedFlagToken } from "./lib/launch-scoped-flags.js";
21
22
  import { cleanupSecureTempArtifacts } from "./lib/temp.js";
22
23
  import { AGENT_BROWSER_PARAMS } from "./lib/input-modes/params.js";
23
24
  import { AGENT_BROWSER_SCRIPT_DEFAULT_TIMEOUT_MS, AGENT_BROWSER_SCRIPT_NAMESPACE, createAgentBrowserScriptSessionName, isAgentBrowserScriptSessionName, runAgentBrowserScript, } from "./lib/input-modes/script.js";
24
- import { closeManagedSession, getSessionContextKey, runAgentBrowserTool } from "./lib/orchestration/browser-run/index.js";
25
25
  import { canonicalizeExplicitArtifactDestination, getExplicitArtifactDestination, getRecordContactSheetDestination } from "./lib/orchestration/browser-run/artifact-paths.js";
26
- import { findElectronLaunchRecordForSession, getActiveElectronRecords } from "./lib/orchestration/browser-run/session-state.js";
26
+ import { findElectronLaunchRecordForSession, getActiveElectronRecords, getSessionContextKey } 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";
29
29
  import { buildValidationFailureResult, resolveAgentBrowserInput } from "./lib/orchestration/input-plan.js";
@@ -571,7 +571,7 @@ function collectBranchManagedResourceEvents(branch) {
571
571
  const replacedSessionNamespace = typeof outcome.replacedSessionNamespace === "string" ? outcome.replacedSessionNamespace : namespace;
572
572
  setBranchRankForString(events.managedSessionCloseRanks, getSessionContextKey(typeof outcome.replacedSessionName === "string" ? outcome.replacedSessionName : undefined, replacedSessionNamespace), eventRank);
573
573
  }
574
- if (succeeded && !isCloseCommand(command) && sessionName && (usedImplicitSession || sessionMode === "fresh" || details.managedSessionHeadedAutosaveDisabled === true || typeof details.managedSessionHeadedAutosaveInterval === "string")) {
574
+ if (succeeded && !isCloseCommand(command) && sessionName && ((!explicitSessionName && (usedImplicitSession || sessionMode === "fresh")) || details.managedSessionHeadedAutosaveDisabled === true || typeof details.managedSessionHeadedAutosaveInterval === "string")) {
575
575
  setBranchManagedSessionActive(events, sessionName, namespace, eventRank);
576
576
  }
577
577
  if (succeeded && isCloseCommand(command)) {
@@ -1243,6 +1243,78 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1243
1243
  notifyRecordingPersistence(ctx);
1244
1244
  }));
1245
1245
  });
1246
+ // Additive native checkpoint event; the package's 0.84 validation types predate it.
1247
+ // Pi owns awaited tools/events (including their execution queues). Only script
1248
+ // children and retained browser/recording resources need extension-side checks.
1249
+ const checkpointPi = pi;
1250
+ checkpointPi.on("session_checkpoint", async (event, ctx) => {
1251
+ const blocked = (reason) => ({ sleepReady: false, reason });
1252
+ if (activeScriptControllers.size || activeScriptExecutions.size)
1253
+ return blocked("Browser script execution or cleanup is pending");
1254
+ flushRecordingReservations();
1255
+ if (recordingReservationsDirty)
1256
+ return blocked("Browser recording journal persistence is dirty");
1257
+ if (activeRecordingReservations.size)
1258
+ return blocked("Browser recording is pending; finish it explicitly before sleep");
1259
+ const entries = ctx.sessionManager.getEntries();
1260
+ if ([...getScriptSessionLeasesFromBranch(entries).values()].some(lease => lease.cleanup !== "closed")
1261
+ || [...ownedManagedSessions.values()].some(owner => isAgentBrowserScriptSessionName(owner.sessionName))) {
1262
+ return blocked("Browser script cleanup lease is unresolved");
1263
+ }
1264
+ if (getActiveElectronRecords(ownedElectronLaunchRecords).length || getActiveElectronRecords(electronLaunchRecords).length
1265
+ || [...electronChildProcesses.values()].some(child => child.exitCode === null && child.signalCode === null)) {
1266
+ return blocked("Electron launch is still active");
1267
+ }
1268
+ if (attachedSessionKeys.size || restoreAttachedSessionKeysFromBranch(entries).size)
1269
+ return blocked("Attached browser state is caller-owned and not checkpointed");
1270
+ if (traceOwners.size || [...networkRoutesBySession.values()].some(routes => routes.length))
1271
+ return blocked("Browser trace or network routes are still active");
1272
+ // Include off-branch and caller-owned/root identities, without acquiring
1273
+ // cleanup ownership. Transcript page/ref details do not serialize a browser.
1274
+ const historicalResources = collectBranchManagedResourceEvents(entries);
1275
+ const sessions = new Map(ownedManagedSessions);
1276
+ if (managedSessionActive)
1277
+ trackOwnedManagedSession(sessions, managedSessionName, managedSessionCwd, { namespace: managedSessionNamespace });
1278
+ for (const entry of entries) {
1279
+ if (entry.type !== "message" || entry.message.role !== "toolResult" || entry.message.toolName !== "agent_browser")
1280
+ continue;
1281
+ const details = isRecord(entry.message.details) ? entry.message.details : undefined;
1282
+ // Helpers can launch before a main-process failure. Conversely, native
1283
+ // launch flags on sessionless reads can open the unnamed default browser.
1284
+ const sessionName = typeof details?.sessionName === "string" ? details.sessionName
1285
+ : details?.agentBrowserStarted === true ? "default" : undefined;
1286
+ if (!sessionName)
1287
+ continue;
1288
+ const namespace = typeof details?.namespace === "string" ? details.namespace : undefined;
1289
+ const key = getSessionContextKey(sessionName, namespace) ?? sessionName;
1290
+ if (!sessions.has(key))
1291
+ trackOwnedManagedSession(sessions, sessionName, ctx.cwd, { namespace });
1292
+ }
1293
+ for (const [key, owner] of sessions) {
1294
+ event.signal.throwIfAborted();
1295
+ // An abnormal restart can leave a wrapper-created browser only off-branch.
1296
+ // Reuse native ownership events for inspection, never cleanup ownership.
1297
+ // A later close can refer to the same identity in a different socket root.
1298
+ const historicalIdentity = historicalResources.managedSessionActiveIdentities.get(key);
1299
+ const historicallyOwned = historicalIdentity && isRestorableManagedSessionName(historicalIdentity.sessionName, managedSessionBaseName);
1300
+ const context = resolveOwnedManagedSessionContext({
1301
+ ...owner,
1302
+ currentManagedSessionName: managedSessionActive ? managedSessionName : undefined,
1303
+ currentManagedSessionNamespace: managedSessionNamespace,
1304
+ recordedOwnedSession: ownedManagedSessions.get(key) ?? (historicallyOwned ? { ...historicalIdentity, cwd: ctx.cwd } : undefined),
1305
+ restoreState: managedSessionRestoreState,
1306
+ });
1307
+ // Owned routing must not mask an explicit caller's same-name ambient daemon.
1308
+ for (const inspectionContext of context ? [{ ...context, reuseOnly: true }, undefined] : [undefined]) {
1309
+ const daemon = await withOwnedManagedSessionContext(inspectionContext, () => inspectManagedSessionDaemon({ ...owner, signal: event.signal, timeoutMs: 2_000 }));
1310
+ if (daemon.status !== "inactive")
1311
+ return blocked("Browser daemon is live or unverified; finish browser work explicitly before sleep");
1312
+ }
1313
+ }
1314
+ // No detached writer remains to pause/resume. Native ingress stays held;
1315
+ // checkpoint never closes a browser or changes ordinary shutdown ownership.
1316
+ return { sleepReady: true };
1317
+ });
1246
1318
  pi.on("session_shutdown", async (event, ctx) => {
1247
1319
  for (const controller of activeScriptControllers)
1248
1320
  controller.abort();
@@ -1623,6 +1695,9 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1623
1695
  ? getSessionContextKey(explicitSessionName, callerOwnedSessionNamespace) ?? explicitSessionName
1624
1696
  : undefined;
1625
1697
  const runBrowserCommand = async (daemonInactive) => {
1698
+ // Load execution-only preparation/output code inside the existing session queue,
1699
+ // before capturing state. Registration, rendering, and restore stay synchronous/eager.
1700
+ const { runAgentBrowserTool } = await import("./lib/orchestration/browser-run/index.js");
1626
1701
  flushRecordingReservations();
1627
1702
  const branchRestoreGenerationAtStart = branchRestoreGeneration;
1628
1703
  const generationAtStart = branchStateGeneration;
@@ -1,5 +1,6 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { lstat, rm } from "node:fs/promises";
3
+ import { win32 } from "node:path";
3
4
  import { promisify } from "node:util";
4
5
  import { fetchCdpJson, parseCdpTargets, parseCdpVersion } from "./cdp.js";
5
6
  import { ELECTRON_PROFILE_DIR_PREFIX } from "./launch.js";
@@ -53,21 +54,35 @@ export async function inspectElectronLaunchStatus(record, signal) {
53
54
  version: cdp.version,
54
55
  };
55
56
  }
57
+ function hasProcessExited(child, pid) {
58
+ // A Windows PID probe can report ESRCH during termination, before the
59
+ // tracked process has exited and released its executable/resources.
60
+ return child ? child.exitCode !== null || child.signalCode !== null : isPidAlive(pid) === false;
61
+ }
56
62
  async function waitForProcessExit(child, pid, deadlineMs) {
57
63
  while (Date.now() <= deadlineMs) {
58
- if (child && (child.exitCode !== null || child.signalCode !== null))
59
- return true;
60
- if (isPidAlive(pid) === false)
64
+ if (hasProcessExited(child, pid))
61
65
  return true;
62
66
  await sleep(ELECTRON_CLEANUP_POLL_INTERVAL_MS);
63
67
  }
64
- return isPidAlive(pid) === false;
68
+ return hasProcessExited(child, pid);
65
69
  }
66
70
  async function readPidCommandLine(pid) {
67
71
  if (!pid || !Number.isSafeInteger(pid) || pid <= 0)
68
72
  return undefined;
69
73
  try {
70
- const { stdout } = await execFileAsync("ps", ["-ww", "-p", String(pid), "-o", "command="], {
74
+ // Win32_Process.CommandLine is the native ownership evidence; process
75
+ // start time (used by daemon-policy locks) cannot prove profile ownership.
76
+ const systemRoot = process.env.SystemRoot;
77
+ const windowsRoot = systemRoot && win32.isAbsolute(systemRoot) ? systemRoot : "C:\\Windows";
78
+ const file = process.platform === "win32"
79
+ ? win32.join(windowsRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe")
80
+ : "ps";
81
+ const args = process.platform === "win32"
82
+ ? ["-NoProfile", "-NonInteractive", "-Command",
83
+ `[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new(); $p = [wmi]"Win32_Process.Handle='${pid}'"; [Console]::WriteLine($p.CommandLine)`]
84
+ : ["-ww", "-p", String(pid), "-o", "command="];
85
+ const { stdout } = await execFileAsync(file, args, {
71
86
  timeout: RESTORED_PROCESS_COMMAND_TIMEOUT_MS,
72
87
  });
73
88
  return stdout.trim() || undefined;
@@ -117,7 +132,7 @@ function signalRestoredLaunchProcess(record, signal) {
117
132
  async function cleanupProcess(record, child, deadlineMs) {
118
133
  if (!record.pid)
119
134
  return { resource: "process", state: "skipped" };
120
- if (isPidAlive(record.pid) === false)
135
+ if (hasProcessExited(child, record.pid))
121
136
  return { resource: "process", state: "already-gone" };
122
137
  if (!child) {
123
138
  const verificationError = await getRestoredProcessVerificationError(record);
@@ -164,16 +164,29 @@ async function ownerAlive(owner) {
164
164
  const current = await readProcessStartIdentity(owner.pid);
165
165
  return current === undefined ? undefined : processStartIdentitiesMatch(owner.startIdentity, current);
166
166
  }
167
- async function removeClaimOwnedBy(path, token) {
168
- const current = await readClaim(path);
169
- if (current?.owner.token !== token)
170
- return false;
167
+ async function removeClaimOwnedBy(path, token, deadline = 0) {
171
168
  const movedPath = join(dirname(path), `.pi-agent-browser-policy-remove-${token}-${randomUUID()}`);
172
- try {
173
- await rename(path, movedPath);
174
- }
175
- catch (error) {
176
- return error.code === "ENOENT";
169
+ while (true) {
170
+ const current = await readClaim(path);
171
+ if (current?.owner.token !== token)
172
+ return false;
173
+ try {
174
+ await rename(path, movedPath);
175
+ break;
176
+ }
177
+ catch (error) {
178
+ const code = error.code;
179
+ if (code === "ENOENT")
180
+ return true;
181
+ // Windows may refuse a directory move while a contender reads its
182
+ // metadata. Revalidate before retrying. Owned-claim cleanup gets
183
+ // its own bounded window, independent of the acquisition wait.
184
+ if (process.platform !== "win32" || code !== "EPERM" || Date.now() >= deadline)
185
+ return false;
186
+ await waitForRetry();
187
+ if (Date.now() >= deadline)
188
+ return false;
189
+ }
177
190
  }
178
191
  const moved = await readClaim(movedPath);
179
192
  if (moved?.owner.token !== token) {
@@ -209,6 +222,12 @@ function claimPrecedes(left, right) {
209
222
  return false;
210
223
  return left.ticket < right.ticket || (left.ticket === right.ticket && left.owner.token < right.owner.token);
211
224
  }
225
+ async function hasPublishedLaterTicket(claim, ownClaim) {
226
+ if (claim.ticket !== null)
227
+ return false;
228
+ const current = await readClaim(claim.path);
229
+ return current?.owner.token === claim.owner.token && !claimPrecedes(current, ownClaim);
230
+ }
212
231
  function waitForRetry(signal) {
213
232
  return new Promise((resolve) => {
214
233
  if (signal?.aborted)
@@ -266,18 +285,35 @@ export async function acquireManagedSessionPolicyLock(options) {
266
285
  for (const claim of claims) {
267
286
  if (claim.owner.token === token || !claimPrecedes(claim, ownClaim))
268
287
  continue;
288
+ // Choosing is transient: a later published ticket must not make
289
+ // its predecessor wait on it using an earlier null snapshot.
290
+ if (await hasPublishedLaterTicket(claim, ownClaim))
291
+ continue;
269
292
  const alive = await ownerAlive(claim.owner);
270
293
  if (alive === false) {
271
294
  await removeClaimOwnedBy(claim.path, claim.owner.token);
272
295
  continue;
273
296
  }
297
+ // A native identity query can outlive the owner's entire critical
298
+ // section. Its result (including unknown after exit) does not make
299
+ // an already released immutable claim a current blocker.
300
+ try {
301
+ await lstat(claim.path);
302
+ }
303
+ catch (error) {
304
+ if (error.code === "ENOENT")
305
+ continue;
306
+ }
307
+ // Publication can also complete during the native identity query.
308
+ if (await hasPublishedLaterTicket(claim, ownClaim))
309
+ continue;
274
310
  blocked = true;
275
311
  break;
276
312
  }
277
313
  if (!blocked) {
278
314
  await cleanDeadPolicyArtifacts(directory);
279
315
  lockAcquired = true;
280
- return { release: async () => { await removeClaimOwnedBy(claimPath, token); } };
316
+ return { release: async () => { await removeClaimOwnedBy(claimPath, token, Date.now() + POLICY_LOCK_WAIT_MS); } };
281
317
  }
282
318
  if (Date.now() >= deadline)
283
319
  return undefined;
@@ -291,6 +327,6 @@ export async function acquireManagedSessionPolicyLock(options) {
291
327
  finally {
292
328
  await rm(candidatePath, { force: true, recursive: true }).catch(() => undefined);
293
329
  if (claimPublished && !lockAcquired)
294
- await removeClaimOwnedBy(claimPath, token);
330
+ await removeClaimOwnedBy(claimPath, token, Date.now() + POLICY_LOCK_WAIT_MS);
295
331
  }
296
332
  }
@@ -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,12 +1,12 @@
1
1
  import { runAgentBrowserProcess, withAttachedBrowserSessionContext, withChromeStartupArgs } from "../../process.js";
2
2
  import { isRecord } from "../../parsing.js";
3
3
  import { collectNativeWebMcp, getNativeWebMcpCatalog } from "../../webmcp-observation.js";
4
- import { redactSensitiveValue } from "../../runtime.js";
4
+ import { isPlainTextInspectionArgs, redactSensitiveValue } from "../../runtime.js";
5
5
  import { formatWebMcpCatalogUpdate } from "../../results/presentation/common.js";
6
6
  import { withOwnedManagedSessionContext } from "../../managed-session-restore.js";
7
7
  import { cleanupClickDispatchProbe } from "./click-dispatch.js";
8
8
  import { applyBrowserRunStatePatch, getSessionContextKey } from "./session-state.js";
9
- import { buildMissingBinaryFailureResult } from "./final-result.js";
9
+ import { buildJsonVisibleContent, buildMissingBinaryFailureResult } from "./final-result.js";
10
10
  import { prepareBrowserRun } from "./prepare.js";
11
11
  import { processBrowserOutput } from "./process-output.js";
12
12
  export { closeManagedSession } from "./managed-session-daemon-policy.js";
@@ -38,7 +38,17 @@ async function runAgentBrowserToolInContext(options) {
38
38
  const preparedResult = await prepareBrowserRun(options);
39
39
  applyBrowserRunStatePatch(options.state, preparedResult.kind === "ready" ? preparedResult.prepared.statePatch : preparedResult.statePatch);
40
40
  if (preparedResult.kind === "early-result") {
41
- 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;
42
52
  }
43
53
  const { prepared } = preparedResult;
44
54
  const ownedManagedSession = prepared.ownedManagedSessionContext;
@@ -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()),