pi-agent-browser-native 0.6.5 → 0.6.7

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 (37) hide show
  1. package/CHANGELOG.md +30 -1
  2. package/README.md +20 -5
  3. package/dist/extensions/agent-browser/index.js +138 -75
  4. package/dist/extensions/agent-browser/lib/command-taxonomy.js +6 -5
  5. package/dist/extensions/agent-browser/lib/electron/cleanup.js +10 -1
  6. package/dist/extensions/agent-browser/lib/input-modes/params.js +20 -7
  7. package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +0 -1
  8. package/dist/extensions/agent-browser/lib/managed-session-restore.js +13 -12
  9. package/dist/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.js +6 -25
  10. package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +2 -3
  11. package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +10 -3
  12. package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +4 -1
  13. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/wait-timeouts.js +1 -1
  14. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +104 -117
  15. package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +61 -35
  16. package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +100 -135
  17. package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +3 -1
  18. package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +3 -1
  19. package/dist/extensions/agent-browser/lib/page-target-validation.js +10 -10
  20. package/dist/extensions/agent-browser/lib/parsing.js +7 -0
  21. package/dist/extensions/agent-browser/lib/playbook.js +5 -8
  22. package/dist/extensions/agent-browser/lib/process-identity.js +10 -2
  23. package/dist/extensions/agent-browser/lib/process.js +23 -7
  24. package/dist/extensions/agent-browser/lib/recording-reservations.js +3 -1
  25. package/dist/extensions/agent-browser/lib/results/envelope.js +4 -1
  26. package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +45 -43
  27. package/dist/extensions/agent-browser/lib/results/recovery-actions.js +4 -4
  28. package/dist/extensions/agent-browser/lib/runtime.js +18 -2
  29. package/dist/extensions/agent-browser/lib/session-page-state.js +29 -10
  30. package/docs/ARCHITECTURE.md +7 -4
  31. package/docs/COMMAND_REFERENCE.md +22 -15
  32. package/docs/ELECTRON.md +6 -6
  33. package/docs/RELEASE.md +14 -5
  34. package/docs/REQUIREMENTS.md +1 -1
  35. package/docs/SUPPORT_MATRIX.md +13 -3
  36. package/docs/TOOL_CONTRACT.md +33 -22
  37. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,6 +1,35 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 0.6.7 - 2026-09-07
4
+
5
+ ### Fixed
6
+
7
+ - Remove four unused prompt suffix entries without changing runtime guidance. Thanks to @JsonKim for #133.
8
+ - Diagnose misplaced Chromium `--no-sandbox` command/navigation options with effective top-level `--args` guidance, while preserving literal operands and help. Adapted from @ahalekelly's #152.
9
+ - Correct Electron list timeout guidance, label explicit-ID cleaned launch records as historical, and report fresh tracked-profile path presence independently of process/port liveness. Failed-launch output capture remains unresolved (#128).
10
+ - Return structured artifact-directory preparation failures for direct, stdin and raw batch commands, retaining the attempted path and recovery guidance. Document absolute artifact paths for raw batches without rewriting their command strings (#124).
11
+ - Identify image media types and inline attachments from bytes instead of filename suffixes, distinguish known requested and reported artifact paths, and surface fresh-snapshot warnings for reached recording page transitions on success and failure (#127).
12
+ - Accept native `upgrade` text without reporting a JSON parse failure, retain failure diagnostics and explicit `--json` output, and keep timeout/cancellation failures even when the child exits zero. Thanks to @fgpaz for the report and regression approach in #156.
13
+ - Clarify positional screenshot and recording paths, the screenshot `--full` flag, batch stdin's JSON token-array format, text-only `job.assertText`, ref refreshes after clicks, focused `keyboard type <text>`, and positional `wait <ms>` in tool guidance.
14
+ - Stop using duplicate-name snapshot ordinals as click-failure evidence after the page changes. Ambiguous refs pass through to native clicking without a probe; unique targets retain no-event checks, and native dispatch still does not prove application state.
15
+ - Accept an unmapped owner for the operating environment's actual filesystem root when validating private socket storage in Linux user namespaces. Preserve non-root ownership, permissions, alias-destination and entry checks, including existing root-owned sticky modes; automatic restore still rejects unmapped non-root HOME ancestry.
16
+ - Let URL-opening QA clear diagnostics and navigate even when the previous tab is gone. Explicit URL reads, URL accessibility/vitals audits, URL diffs, new windows and URL-bearing recording commands also keep their own destination in direct and batch calls; attached QA and current-page actions still require the intended page.
17
+ - Retain a resumed managed session's pending URL reopen after confirmed shutdown, even when non-page calls start the daemon first or a batch begins with non-page steps. Reopen the complete URL, including its fragment, before current-page reads or history commands; verify the observed tab and discard old refs/frame scope. Unreached batch navigation does not consume the reopen, and native row/error order is unchanged. Restored cookies/storage do not recover unsaved forms, JavaScript memory, or history; live wrong-tab recovery and explicit navigation keep their own intent.
18
+ - Keep follow-ups on the observed page after `window new` or `diff url`, including redirected destinations and reached native batch rows. Do not reselect the old tab for an intentional blank window or an observed blank diff destination; invalidate old refs and require a verified target when the final URL cannot be observed.
19
+ - Preserve the consumed cold-reopen marker and exact session identity in aborted results, so cancellation after an attempted reopen cannot navigate a live browser again after reload. Cancellation before the attempt leaves the reopen pending.
20
+ - Require observed successful page results in lifecycle verification and report the first unexpected completed tool result instead of accepting recovery text or waiting for a later result.
21
+
22
+ - Preserve native arguments, literal values, refs, and continue-on-error behavior during tab recovery. Failed tab selection stops before user commands; mixed batch failures retain their per-step results and failure counts.
23
+ - Apply stale-ref checks to `@eN`, `eN`, and `ref=eN` selector operands without treating text, paths, or keyboard/mouse data as refs. Explain unsupported `batch --bail=<value>` without running ignored stdin.
24
+ - Retry failed recording journal writes, preserve closed recording state across branch changes and reloads, require absolute stored recording paths, and target cleanup to the exact session and namespace. Preserve the selected managed-session namespace and automatic restore when ambient namespace settings change.
25
+ - Limit `semanticAction.values` to select actions in the tool schema and clarify the supported `stdin` commands. Valid semantic calls are unchanged; runtime validation still applies. Thanks to @lindsayemarc for #139.
26
+ - Check socket-directory ancestry through root-owned symlinks, rejecting unsafe destination parents and intermediate user-owned links while preserving trusted system aliases.
27
+
28
+ ## 0.6.6 - 2026-09-05
29
+
30
+ ### Fixed
31
+
32
+ - Resolve POSIX process identity with `ps` from `PATH` when the system paths are unavailable, so managed-session locks work on NixOS-style installations. Keep system-path preference and reject malformed identity output. Thanks to @GodTamIt for the report and fix in #142 / #143.
4
33
 
5
34
  ## 0.6.5 - 2026-09-04
6
35
 
package/README.md CHANGED
@@ -79,7 +79,7 @@ The result is optimized for agent work:
79
79
  | 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` |
80
80
  | 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` |
81
81
  | 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` |
82
- | 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 use the latest snapshot role/name metadata, and duplicate-name refs use snapshot-order `duplicateIndex` rather than requiring a unique name. 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` |
82
+ | 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` |
83
83
  | Dashboard scroll commands can look successful while nothing moves | Handles standard `scroll <dir> [px]` against the document first (including pages whose smooth-scroll CSS defeats upstream wheel timing), falls back upstream when the document cannot move, and samples viewport/containers around the fallback; unchanged positions fail as `upstream-error` with `details.scrollNoop`, visible recovery guidance, and exact snapshot/screenshot checks. Unsupported `scrollintoview text=...` fails before dispatch, including inside effective batch rows, and shows exact native `find text ... hover` and snapshot/ref recovery payloads; help remains native pass-through. | [`docs/TOOL_CONTRACT.md`](docs/TOOL_CONTRACT.md#details), [`docs/COMMAND_REFERENCE.md`](docs/COMMAND_REFERENCE.md#core-page-and-element-commands), `test/agent-browser.extension-validation.test.ts` |
84
84
  | 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` |
85
85
  | 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 `ffmpeg` is unavailable, and tells agents that `record start` switches to a fresh active page whose in-page state does not carry over while invalidating prior page-scoped `@e…` refs on every executed start attempt (even a failed already-active one) and on URL-bearing `record restart` (stale-ref until a fresh snapshot); 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,6 +88,8 @@ The result is optimized for agent work:
88
88
  | 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) |
89
89
  | 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` |
90
90
 
91
+ Artifact results show known requested paths separately from reported/resolved locations and infer image MIME types from file headers, not suffixes. Parent-directory failures return path-specific `validation-error` guidance before browser dispatch. Use absolute artifact paths in raw batch strings because the daemon's working directory may differ from Pi's. Dispatched `record start` and URL-bearing `record restart` attempts also return one fresh-snapshot warning on success or failure, including in JSON output; unreached rows do not claim a page change.
92
+
91
93
  ## Fastest way to try it
92
94
 
93
95
  Use Pi 0.84.0 or newer. This package keeps optional Pi core imports as wildcard `peerDependencies` because Pi package docs require the host Pi install to provide those packages, pins its direct Pi validation dependencies to 0.84.0, and makes older hosts a setup failure through `pi-agent-browser-doctor`. There are no compatibility shims for older Pi releases.
@@ -190,6 +192,8 @@ It does **not** edit Pi settings and does **not** run upstream `agent-browser do
190
192
 
191
193
  Pi hosts that run as uid 0 should set `PI_AGENT_BROWSER_SOCKET_DIR` to a short absolute directory under private root-owned ancestry, create it with mode `0700`, and keep it owned by the Pi user. The extension validates that directory and forwards it as upstream `AGENT_BROWSER_SOCKET_DIR`; ambient upstream socket overrides remain ignored.
192
194
 
195
+ In a Linux user namespace, `/` may report an unmapped owner. Socket checks trust that operating-environment root while retaining checks on every other ancestor and the private socket directory; they do not protect against whoever controls the root filesystem. The demonstrated bubblewrap layout uses a read-only mode-`0755` root and a current-user-owned mode-`0700` `/tmp`, with private HOME and checkout below it. Unmapped non-root ancestors such as `/home` still fail automatic restore checks; this is not support for every bubblewrap layout. See [filesystem trust](docs/ARCHITECTURE.md#ownership).
196
+
193
197
  ## Optional package config and web search
194
198
 
195
199
  `pi-agent-browser-native` also reads package-owned config under Pi-scoped paths:
@@ -307,6 +311,8 @@ Open a page and inspect it (first-call recipe: open → snapshot -i → interact
307
311
  { "args": ["snapshot", "-i"] }
308
312
  ```
309
313
 
314
+ 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.
315
+
310
316
  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"`.
311
317
 
312
318
  ```json
@@ -441,7 +447,7 @@ Typical pitfalls:
441
447
  - 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.
442
448
  - 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)).
443
449
  - 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).
444
- - 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`, using duplicate-name snapshot order when needed. 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.
450
+ - 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.
445
451
  - 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.
446
452
  - 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.
447
453
  - 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.
@@ -451,6 +457,8 @@ Typical pitfalls:
451
457
 
452
458
  For short repeatable workflows, pass a top-level `job` instead of hand-writing `batch` stdin. Keep dynamic app jobs short around navigation, click, and rerender boundaries; avoid packing a whole checkout into one job. The wrapper only supports constrained steps (`open`, `click`, `fill`, `type`, `select`, `wait`, `assertText`, `assertUrl`, `waitForDownload`, `snapshot`, and `screenshot`), compiles them to existing upstream `batch` commands, and echoes the compiled commands as `details.compiledJob` for auditability. `open` steps can include `loadState` (`domcontentloaded`, `load`, or `networkidle`) to insert a readiness wait before the next step. `click` and `fill` steps can use either CSS `selector` or semantic locator fields (`locator`, `role`/`value`, optional `name`) so a job can express flows like role/name search without brittle selectors. `type` can use `selector`, `text`, optional `delayMs` for per-character pacing, and optional `press` for a final key such as `Enter`; paced type compiles to existing `focus`, `keyboard type`, `wait`, and `press` batch rows, is capped at 200 characters per delayed step, and compacts model-visible batch text while full rows remain in `details.batchSteps`. The same compile path backs top-level `qa`, so long `qa` runs surface the same timeout evidence shape. If a long `job`, `qa`, or `batch` hits the wrapper watchdog, `details.timeoutPartialProgress` may recover per-step status (`completed`, `failed`, `pending`, or `unknown`), current page URL plus a title after the URL is verified, declared artifact paths that already exist on disk, and either a `retry-timeout-step` next action for the first incomplete read-only or idempotent step, `inspect-current-page-after-timeout` when the target is already verified, or a fail-fast `verify-page-target-after-timeout` batch that runs `get url` before `snapshot -i` when the target is unknown (see [`docs/TOOL_CONTRACT.md#details`](docs/TOOL_CONTRACT.md#details)). There is no separate catalog of reusable named browser recipes above one-shot ad hoc `script`, `job`, `qa`, and raw `batch`; `script` has no names, registry, or persistent workflow state; see [`docs/ARCHITECTURE.md#no-reusable-recipe-layer-yet`](docs/ARCHITECTURE.md#no-reusable-recipe-layer-yet) for the closed `RQ-0068` decision and when to revisit it.
453
459
 
460
+ `assertText` accepts only `text`, not selector or locator fields. Clicks can stale later `@refs`; split the job and take a fresh snapshot before using them.
461
+
454
462
  **Navigation inside `job` is explicit.** A successful `click` does not prove the next page loaded; add `assertUrl` and/or `assertText` after navigation-prone clicks (forms, checkout, tabs, submit buttons) before screenshots or steps that assume the new page. `assertUrl` accepts exact URLs and `*` / `**` glob-style patterns and now compiles directly to upstream `wait --url` for both forms.
455
463
 
456
464
  ```json
@@ -497,7 +505,9 @@ For desktop Electron apps, use top-level `electron` to avoid hand-building the d
497
505
  { "electron": { "action": "cleanup", "launchId": "electron-…" } }
498
506
  ```
499
507
 
500
- `electron.probe.timeoutMs` bounds each underlying read subprocess when dense desktop apps need a shorter or longer probe budget (omit for the normal tool subprocess default). `electron.cleanup.timeoutMs` caps upstream `close` plus host profile/process teardown and defaults to the implicit session close budget unless overridden; if the managed-session close step succeeds but host cleanup is partial, later default browser calls still rotate away from that closed wrapper-managed session. `electron.status.timeoutMs` only tightens managed-session title/url reads used for mismatch checks. Pass `electron.probe.launchId` when you want the probe tied to a wrapper-tracked launch instead of only the current managed session. Launch/status/probe results show both `launchId` (for status/cleanup/probe) and `sessionName` (for browser `snapshot`/`tab` commands); if the managed session drifts to `about:blank` while wrapper status still sees a live renderer, Electron-specific mismatch warnings and `status`/`probe`/`reattach`/`snapshot` next actions replace generic tab guidance. `/reload` preserves the current branch-visible active Electron launch and its isolated temp `userDataDir` for continuity, and cleans off-branch owned Electron launches; if cleanup is partial and skips or fails profile removal, the generic temp sweep preserves that `userDataDir` across reload, quit, later temp cleanup, process exit, and stale temp-root pruning after restart. If the app process/debug port dies after a successful-looking mutation, the wrapper reports `details.electronPostCommandHealth` and fails with `tab-drift` instead of quietly continuing on `about:blank`. Launch timeouts expose `details.electron.failure.diagnostics` for PID, profile, DevToolsActivePort, and timing evidence.
508
+ `electron.list` has no configurable timeout. Other Electron actions accept nested `electron.timeoutMs`; `electron.probe.timeoutMs` bounds each underlying read subprocess when dense desktop apps need a shorter or longer probe budget (omit for the normal tool subprocess default). `electron.cleanup.timeoutMs` applies separately to upstream `close` and the initial process-exit wait, not the whole teardown, and defaults to the implicit session close budget unless overridden; if the managed-session close step succeeds but host cleanup is partial, later default browser calls still rotate away from that closed wrapper-managed session. `electron.status.timeoutMs` only tightens managed-session title/url reads used for mismatch checks. Pass `electron.probe.launchId` when you want the probe tied to a wrapper-tracked launch instead of only the current managed session. Launch/status/probe results show both `launchId` (for status/cleanup/probe) and `sessionName` (for browser `snapshot`/`tab` commands); if the managed session drifts to `about:blank` while wrapper status still sees a live renderer, Electron-specific mismatch warnings and `status`/`probe`/`reattach`/`snapshot` next actions replace generic tab guidance. `/reload` preserves the current branch-visible active Electron launch and its isolated temp `userDataDir` for continuity, and cleans off-branch owned Electron launches; if cleanup is partial and skips or fails profile removal, the generic temp sweep preserves that `userDataDir` across reload, quit, later temp cleanup, process exit, and stale temp-root pruning after restart. If the app process/debug port dies after a successful-looking mutation, the wrapper reports `details.electronPostCommandHealth` and fails with `tab-drift` instead of quietly continuing on `about:blank`. Launch timeouts expose `details.electron.failure.diagnostics` for PID, profile, DevToolsActivePort, and timing evidence.
509
+
510
+ Explicit-ID `electron.status` labels historical cleaned launch records; default and `all: true` selection exclude them. Current PID/port liveness stays independent of cleanup history. `details.electron.statuses[].userDataDirState` freshly measures only the tracked profile path: `present` (including dangling symlinks), `absent` (ENOENT), or `unknown` (other `lstat` errors), not whether all app residue is gone.
501
511
 
502
512
  `launch.handoff` still defaults to `"snapshot"`; it retries briefly when the first Electron snapshot has no refs. Use `handoff: "tabs"` as a safer diagnostic starting point when you only need target discovery and do not want interactive refs captured yet, or `handoff: "connect"` when you want attach-only and will run your own `snapshot -i` / tab commands next. For Electron quick inputs that rerender in place, a successful `fill` may include `details.fillVerification` if `get value` still disagrees; re-snapshot and use focus plus keyboard typing before submitting.
503
513
 
@@ -512,7 +522,7 @@ For an app you launched yourself with remote debugging enabled, use raw upstream
512
522
 
513
523
  `connect` success means the debug endpoint accepted the session, not that an active page is ready. Use the returned `verify-connected-session-url` (`get url`) action before page-content reads, then inspect/select a stable tab and verify its URL. If a snapshot says `No active page`, the wrapper clears prior refs for that session; choose a stable `t<N>` tab and retry a condition wait or fresh `snapshot -i` before using `@e…` refs. Close commands (`close`, `quit`, or `exit`) only close the browser/CDP session; manually launched apps, their profiles, and explicit screenshots/downloads/HARs/traces/recordings remain host-owned.
514
524
 
515
- After either path, use `qa: { "attached": true, ... }` for a current-session smoke check without opening a URL. Attached QA preserves existing network/console/page-error buffers instead of clearing them, so it can catch errors raised before the check started; visible output and `details.compiledQaPreset.checks.diagnosticsResetAtStart` identify that scope. Prefer condition waits (`wait --text`, `wait --url`, `wait --fn`, `wait --load <state>`, `wait --download`), `qa.attached`, `electron.probe` / `electron.status`, `tab list` → `tab t<N>`, fresh snapshots, or screenshots over blind sleeps. Fixed waits are a last resort: use explicit `--timeout` or top-level `timeoutMs` for legitimately slow waits, and treat a result like `"waited":"timeout"` as elapsed time only. Batch output promotes dispatch-only mutation evidence and states that fixed waits are not postconditions.
525
+ After either path, use `qa: { "attached": true, ... }` for a current-session smoke check without opening a URL. Attached QA preserves existing network/console/page-error buffers instead of clearing them, so it can catch errors raised before the check started; visible output and `details.compiledQaPreset.checks.diagnosticsResetAtStart` identify that scope. Prefer condition waits (`wait --text`, `wait --url`, `wait --fn`, `wait --load <state>`, `wait --download`), `qa.attached`, `electron.probe` / `electron.status`, `tab list` → `tab t<N>`, fresh snapshots, or screenshots over blind sleeps. Fixed waits are a last resort: use positional `wait <ms>`, not `wait --time <ms>`. Use explicit `--timeout` or top-level `timeoutMs` for legitimately slow waits, and treat a result like `"waited":"timeout"` as elapsed time only. Batch output promotes dispatch-only mutation evidence and states that fixed waits are not postconditions.
516
526
 
517
527
  ### Lightweight QA preset
518
528
 
@@ -574,6 +584,10 @@ Start a fresh launch with a different Chromium-compatible executable:
574
584
  }
575
585
  ```
576
586
 
587
+ After resuming a wrapper-managed session whose daemon has fully stopped, the first current-page operation, including `get url` or `reload`, reopens the complete recorded URL, including its fragment, when automatic managed restore is enabled. Non-page calls such as `tab list` or `read <url>` may start a daemon first without losing that pending reopen. The wrapper verifies the observed page and discards old refs; take a fresh `snapshot -i` before using refs. This restores the URL with cookies/storage, not unsaved forms, JavaScript memory, or history. A still-live wrong tab is selected or reported as missing, never silently navigated. Explicit URL reads, URL `a11y`/`vitals`, `diff url`, `window new`, and explicit navigation/context changes do not require the old tab. Caller-owned/attached and restore-disabled sessions are not auto-reopened. Cancellation after the reopen CLI starts preserves the consumed marker in the aborted result; replay does not repeat the navigation. Cancellation before that attempt leaves the reopen pending.
588
+
589
+ `window new` leaves its new `about:blank` page active; `diff url` leaves the observed second destination active, including redirects to `about:blank`. Direct calls and reached batch rows discard the old target and refs rather than repinning the old tab. If the final URL cannot be observed, run `get url` before a fresh `snapshot -i`; the second URL argument alone is not proof of the final page.
590
+
577
591
  After a successful unnamed fresh launch, later default `sessionMode: "auto"` calls follow that browser automatically. If the fresh launch fails or times out, `details.managedSessionOutcome` records whether the previous managed session was preserved or the attempted fresh session was abandoned before any managed session became current; a `Managed session outcome: …` line is appended only when the failing call used `sessionMode: "fresh"`. If you explicitly close the current wrapper-managed session with `--session <name> close`, later default auto calls rotate to a new wrapper-generated session instead of reusing that closed name, and repeated closes keep reserving fresh names across resume/branch restore.
578
592
 
579
593
  ## Authenticated/profile workflows
@@ -797,7 +811,8 @@ These calls return plain text and stay stateless: the extension does not inject
797
811
  <!-- agent-browser-playbook:start wrapper-tab-recovery -->
798
812
  <!-- Generated from extensions/agent-browser/lib/playbook.ts. Run `npm run docs -- playbook write` to update. -->
799
813
  - After open/goto/navigate calls with --profile, --restore, --session-name, or --state, agent_browser best-effort re-selects the tab whose URL matches the returned page when restored tabs steal focus during launch or reconnect.
800
- - After the wrapper observes tab-drift risk for a session (for example open correction, overlapping stale opens, or resumed session state), later active-tab commands best-effort pin that tab inside the same upstream invocation. Routine same-session commands are not preflighted with tab list just because a target tab or ref snapshot is known.
814
+ - After confirmed shutdown of an automatically restored managed session, the wrapper retains its complete recorded URL, including the fragment, until the first current-page operation (including get url and reload). Non-page calls such as tab list or read <url> may start a daemon without fulfilling that reopen. The wrapper uses native open once, verifies the observed tab, and discards old refs/frame scope; it does not restore unsaved forms, JavaScript memory, or history. Explicit navigation, caller-owned/attached sessions, and restore-disabled sessions are not auto-reopened.
815
+ - For a still-live browser after tab drift or resume, the wrapper verifies/selects the intended tab before ref/semantic helpers and page commands; failed selection stops the call without navigating. Local commands, read <url>, URL a11y/vitals, diff url, window new, and explicit tab/navigation/connection/state recovery do not require the prior tab. Batch checks follow effective rows past non-page prefixes and stop at explicit context changes, preserving caller argv/stdin and continue-on-error behavior. Same-tab reselection is avoided because it clears refs. Use exact batch --bail for fail-fast, not --bail=<value>. Routine same-session calls skip tab-list preflights.
801
816
  - For sessions with observed tab-drift risk, after a successful command on a known target tab, agent_browser also best-effort restores that intended tab if a restored/background tab steals focus after the command completes. Routine same-session commands skip this post-command tab-list probe.
802
817
  - If a known session target unexpectedly reports about:blank, agent_browser best-effort re-selects the prior intended target when it still exists; if recovery fails, it records the observed about:blank target and reports exact recovery guidance instead of treating the prior page as active.
803
818
  - If upstream reports tab_gone, the pinned bound tab is gone; use details.nextActions (tab list / tab new) instead of assuming another tab is yours.
@@ -5,7 +5,7 @@ import { Text } from "@earendil-works/pi-tui";
5
5
  import { batchHasSuccessfulCloseAll, getSuccessfulBatchCloseLifecycle } from "./lib/batch-lifecycle.js";
6
6
  import { PROJECT_RULE_PROMPT, buildBrowserDefaultProfileGuideline, buildBrowserExecutablePathGuideline, buildToolPromptGuidelines, } from "./lib/playbook.js";
7
7
  import { SessionPageState } from "./lib/session-page-state.js";
8
- import { canUseHeadlessCompatibilityUserAgent, createEphemeralSessionSeed, createFreshSessionName, createImplicitSessionName, extractUpstreamCommandTokens, getImplicitSessionCloseTimeoutMs, getImplicitSessionIdleTimeoutMs, isRestorableManagedSessionName, restoreManagedSessionStateFromBranch, validateToolArgs, redactSensitiveText, isPlainTextInspectionArgs, } from "./lib/runtime.js";
8
+ import { buildExecutionPlan, canUseHeadlessCompatibilityUserAgent, createEphemeralSessionSeed, createFreshSessionName, createImplicitSessionName, extractUpstreamCommandTokens, getImplicitSessionCloseTimeoutMs, getImplicitSessionIdleTimeoutMs, isRestorableManagedSessionName, restoreManagedSessionStateFromBranch, validateToolArgs, redactSensitiveText, isPlainTextInspectionArgs, } from "./lib/runtime.js";
9
9
  import { 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";
@@ -680,7 +680,7 @@ function shouldSerializeBrowserCommand(options) {
680
680
  return true;
681
681
  if (options.explicitSessionName === options.managedSessionName)
682
682
  return true;
683
- if (options.ownedManagedSessions.has(getSessionContextKey(options.explicitSessionName, options.explicitNamespace) ?? options.explicitSessionName))
683
+ if (options.ownedManagedSessions.has(getSessionContextKey(options.explicitSessionName, options.namespace) ?? options.explicitSessionName))
684
684
  return true;
685
685
  return getActiveElectronRecords(options.ownedElectronLaunchRecords).some((record) => record.sessionName === options.explicitSessionName);
686
686
  }
@@ -844,7 +844,7 @@ export default function agentBrowserExtension(pi) {
844
844
  let artifactManifest;
845
845
  let activeRecordingReservations = new Map();
846
846
  let recordingSessionTombstones = new Map();
847
- let recordingSessionTombstonesToPersist = new Map();
847
+ let recordingReservationsDirty = false;
848
848
  let attachedSessionKeys = new Set();
849
849
  let networkRoutesBySession = new Map();
850
850
  let electronLaunchRecords = new Map();
@@ -861,23 +861,64 @@ export default function agentBrowserExtension(pi) {
861
861
  let branchRestoreGeneration = 0;
862
862
  let branchStateGeneration = 0;
863
863
  const validatedUpstreamPathKeys = new Set();
864
+ const recordingPersistenceWarning = "Recording persistence warning: recording protection could not be saved to the Pi journal. Restart protection is not yet durable; keep recording destinations untouched until exact stop or close. The next browser operation retries journal persistence; cleanup remains available.";
865
+ const flushRecordingReservations = () => {
866
+ if (!recordingReservationsDirty)
867
+ return;
868
+ try {
869
+ // Pi updates branch memory before writing. One failed append can hide an earlier
870
+ // durable reservation after reopen, so republish all current state, not just the failed row.
871
+ for (const reservation of recordingSessionTombstones.values())
872
+ appendRecordingReservationTransition(pi, { reservation, state: "closed" });
873
+ for (const reservation of activeRecordingReservations.values())
874
+ appendRecordingReservationTransition(pi, { reservation, state: "active" });
875
+ recordingReservationsDirty = false;
876
+ }
877
+ catch { }
878
+ };
879
+ const warnRecordingPersistence = (result) => {
880
+ if (!recordingReservationsDirty)
881
+ return result;
882
+ const content = [...result.content];
883
+ const first = content[0];
884
+ let json;
885
+ if (first?.type === "text") {
886
+ try {
887
+ json = JSON.parse(first.text);
888
+ }
889
+ catch { }
890
+ }
891
+ if (isRecord(json) && typeof json.success === "boolean") {
892
+ content[0] = { type: "text", text: JSON.stringify({ ...json, warnings: [...(Array.isArray(json.warnings) ? json.warnings : []), recordingPersistenceWarning] }, null, 2) };
893
+ }
894
+ else if (first?.type === "text")
895
+ content[0] = { ...first, text: `${first.text}\n\n${recordingPersistenceWarning}` };
896
+ else
897
+ content.push({ type: "text", text: recordingPersistenceWarning });
898
+ return { ...result, content, details: { ...(isRecord(result.details) ? result.details : {}), recordingPersistenceWarning } };
899
+ };
900
+ const notifyRecordingPersistence = (ctx) => {
901
+ if (!recordingReservationsDirty)
902
+ return;
903
+ if (ctx.hasUI)
904
+ ctx.ui.notify(recordingPersistenceWarning, "warning");
905
+ else
906
+ console.warn(recordingPersistenceWarning);
907
+ };
864
908
  const appendRecordingTransitions = (transitions) => {
865
909
  for (const transition of transitions) {
866
910
  const key = getAgentBrowserSessionIdentityKey(transition.reservation.sessionName, transition.reservation.namespace);
867
- if (transition.state === "active") {
911
+ if (transition.state === "active")
868
912
  recordingSessionTombstones.delete(key);
869
- recordingSessionTombstonesToPersist.delete(key);
870
- }
871
- else {
913
+ else
872
914
  recordingSessionTombstones.set(key, transition.reservation);
873
- }
915
+ if (recordingReservationsDirty)
916
+ continue;
874
917
  try {
875
918
  appendRecordingReservationTransition(pi, transition);
876
- recordingSessionTombstonesToPersist.delete(key);
877
919
  }
878
920
  catch {
879
- if (transition.state === "closed")
880
- recordingSessionTombstonesToPersist.set(key, transition.reservation);
921
+ recordingReservationsDirty = true;
881
922
  }
882
923
  }
883
924
  };
@@ -885,19 +926,28 @@ export default function agentBrowserExtension(pi) {
885
926
  if (result.isError !== true)
886
927
  return result;
887
928
  const details = isRecord(result.details) ? result.details : {};
888
- const nextActions = Array.isArray(details.nextActions) ? [...details.nextActions] : [];
889
- if (nextActions.some((action) => action.id === "stop-pending-recording"))
929
+ const cleanupOnly = details.managedSessionCleanupOnlyReason === "restore-disabled-daemon-without-provenance";
930
+ const nextActions = (Array.isArray(details.nextActions) ? details.nextActions : [])
931
+ .filter((action) => !cleanupOnly || action.id !== "stop-pending-recording");
932
+ const actionId = cleanupOnly ? "close-pending-recording" : "stop-pending-recording";
933
+ if (nextActions.some((action) => action.id === actionId))
890
934
  return result;
891
935
  const stopActions = applyNamespaceToNextActions(applySessionToNextActions([
892
936
  buildNextToolAction({
893
- args: ["record", "stop"],
894
- id: "stop-pending-recording",
895
- reason: "Stop the active recording so the requested video can be finalized and verified on disk.",
896
- safety: "The file remains pending until record stop succeeds; verify details.artifactVerification afterward.",
937
+ args: cleanupOnly ? ["close"] : ["record", "stop"],
938
+ id: actionId,
939
+ reason: cleanupOnly
940
+ ? "Close this exact session to abandon the recording; its live daemon lacks current-instance provenance."
941
+ : "Stop the active recording so the requested video can be finalized and verified on disk.",
942
+ safety: cleanupOnly
943
+ ? "Close does not verify the WebM. The recording is abandoned/unverified, even if close leaves a file on disk."
944
+ : "The file remains pending until record stop succeeds; verify details.artifactVerification afterward.",
897
945
  }),
898
- ], reservation.sessionName), reservation.namespace);
946
+ ], reservation.sessionName), cleanupOnly ? reservation.namespace ?? "" : reservation.namespace);
899
947
  appendUniqueAgentBrowserNextActions(nextActions, stopActions);
900
- const cleanupNotice = "An active recording remains open. Use the exact stop-pending-recording payload in details.nextActions before leaving this session.";
948
+ const cleanupNotice = cleanupOnly
949
+ ? "This recording cannot be stopped through the unproven daemon. Use the exact close-pending-recording payload in details.nextActions; any WebM left by close is abandoned/unverified."
950
+ : "An active recording remains open. Use the exact stop-pending-recording payload in details.nextActions before leaving this session.";
901
951
  let noticeAppended = false;
902
952
  const content = result.content.map((item) => {
903
953
  if (noticeAppended || item.type !== "text")
@@ -917,18 +967,7 @@ export default function agentBrowserExtension(pi) {
917
967
  if (!reservation && artifactManifest === previousManifest)
918
968
  return;
919
969
  const terminalReservation = reservation ?? { absolutePath: "", cwd: managedSessionCwd, namespace, path: "", sessionName };
920
- const terminalKey = getAgentBrowserSessionIdentityKey(sessionName, namespace);
921
- recordingSessionTombstones.set(terminalKey, terminalReservation);
922
- try {
923
- appendRecordingReservationTransition(pi, {
924
- reservation: terminalReservation,
925
- state: "closed",
926
- });
927
- recordingSessionTombstonesToPersist.delete(terminalKey);
928
- }
929
- catch {
930
- recordingSessionTombstonesToPersist.set(terminalKey, terminalReservation);
931
- }
970
+ appendRecordingTransitions([{ reservation: terminalReservation, state: "closed" }]);
932
971
  };
933
972
  const syncRecordingReservationsFromResult = (result) => {
934
973
  const handledClosedSessionKeys = new Set();
@@ -1107,20 +1146,25 @@ export default function agentBrowserExtension(pi) {
1107
1146
  traceOwners = new Map();
1108
1147
  artifactManifest = restoreArtifactManifestFromBranch(branch);
1109
1148
  const restoredRecordingState = restoreRecordingReservationStateFromBranch(branch);
1110
- for (const [key, reservation] of recordingSessionTombstones) {
1111
- if (restoredRecordingState.terminal.has(key))
1112
- recordingSessionTombstonesToPersist.delete(key);
1113
- else
1114
- recordingSessionTombstonesToPersist.set(key, reservation);
1149
+ for (const key of recordingSessionTombstones.keys()) {
1150
+ if (!restoredRecordingState.terminal.has(key))
1151
+ recordingReservationsDirty = true;
1115
1152
  }
1116
1153
  for (const [key, reservation] of restoredRecordingState.terminal) {
1117
1154
  if (!activeRecordingReservations.has(key))
1118
1155
  recordingSessionTombstones.set(key, reservation);
1119
1156
  }
1120
- for (const key of recordingSessionTombstones.keys())
1157
+ for (const [key, reservation] of recordingSessionTombstones) {
1121
1158
  restoredRecordingState.active.delete(key);
1122
- for (const [key, reservation] of activeRecordingReservations)
1159
+ if (artifactManifest)
1160
+ artifactManifest = retirePendingRecordingManifestEntries(artifactManifest, reservation.sessionName, reservation.namespace);
1161
+ }
1162
+ for (const [key, reservation] of activeRecordingReservations) {
1163
+ const restored = restoredRecordingState.active.get(key);
1164
+ if (restored?.absolutePath !== reservation.absolutePath || restored.cwd !== reservation.cwd)
1165
+ recordingReservationsDirty = true;
1123
1166
  restoredRecordingState.active.set(key, reservation);
1167
+ }
1124
1168
  activeRecordingReservations = restoredRecordingState.active;
1125
1169
  attachedSessionKeys = restoreAttachedSessionKeysFromBranch(branch);
1126
1170
  networkRoutesBySession = new Map();
@@ -1214,7 +1258,11 @@ export default function agentBrowserExtension(pi) {
1214
1258
  cwd: ctx.cwd,
1215
1259
  includeProjectConfig: shouldIncludeProjectConfig(ctx),
1216
1260
  }));
1217
- await artifactExecutionQueue.run(() => managedSessionExecutionQueue.run(() => recoverScriptSessionLeasesWithinQueue(ctx)));
1261
+ await artifactExecutionQueue.run(() => managedSessionExecutionQueue.run(async () => {
1262
+ await recoverScriptSessionLeasesWithinQueue(ctx);
1263
+ flushRecordingReservations();
1264
+ notifyRecordingPersistence(ctx);
1265
+ }));
1218
1266
  });
1219
1267
  pi.on("session_tree", async (_event, ctx) => {
1220
1268
  for (const controller of activeScriptControllers)
@@ -1223,6 +1271,8 @@ export default function agentBrowserExtension(pi) {
1223
1271
  await artifactExecutionQueue.run(() => managedSessionExecutionQueue.run(async () => {
1224
1272
  restoreBranchBackedState(ctx, { resetRuntimeOwnership: false });
1225
1273
  await recoverScriptSessionLeasesWithinQueue(ctx);
1274
+ flushRecordingReservations();
1275
+ notifyRecordingPersistence(ctx);
1226
1276
  }));
1227
1277
  });
1228
1278
  pi.on("session_shutdown", async (event, ctx) => {
@@ -1263,30 +1313,21 @@ export default function agentBrowserExtension(pi) {
1263
1313
  else {
1264
1314
  await closeOwnedManagedSessionsExcept(ownedManagedSessions, managedSessionRestoreState, managedSessionActive ? managedSessionName : undefined, implicitSessionCloseTimeoutMs, attachedSessionKeys, managedSessionActive ? managedSessionNamespace : undefined, (owner) => retireRecordingSession(owner.sessionName, owner.namespace));
1265
1315
  }
1316
+ flushRecordingReservations();
1317
+ notifyRecordingPersistence(ctx);
1266
1318
  }));
1267
1319
  managedSessionActive = false;
1268
1320
  managedSessionCompatibilityWorkaround = undefined;
1269
1321
  managedSessionHeadedAutosaveDisabled = false;
1270
1322
  managedSessionHeadedAutosaveInterval = undefined;
1271
1323
  managedSessionNamespace = undefined;
1272
- for (const reservation of recordingSessionTombstonesToPersist.values()) {
1273
- try {
1274
- appendRecordingReservationTransition(pi, { reservation, state: "closed" });
1275
- }
1276
- catch { }
1277
- }
1278
- for (const reservation of activeRecordingReservations.values()) {
1279
- try {
1280
- appendRecordingReservationTransition(pi, { reservation, state: "active" });
1281
- }
1282
- catch { }
1283
- }
1284
1324
  sessionPageState.reset();
1285
1325
  traceOwners = new Map();
1286
1326
  artifactManifest = undefined;
1287
- activeRecordingReservations = new Map();
1288
- recordingSessionTombstones = new Map();
1289
- recordingSessionTombstonesToPersist = new Map();
1327
+ if (!recordingReservationsDirty) {
1328
+ activeRecordingReservations = new Map();
1329
+ recordingSessionTombstones = new Map();
1330
+ }
1290
1331
  attachedSessionKeys = new Set();
1291
1332
  networkRoutesBySession = new Map();
1292
1333
  electronLaunchRecords = new Map();
@@ -1365,8 +1406,9 @@ export default function agentBrowserExtension(pi) {
1365
1406
  }
1366
1407
  const applyUnserializedOutputPath = async (result, preserveTextContent = false) => {
1367
1408
  if (!outputPath || result.isError === true || (isRecord(result.details) && result.details.resultCategory === "failure"))
1368
- return result;
1409
+ return warnRecordingPersistence(result);
1369
1410
  return artifactExecutionQueue.run(async () => {
1411
+ flushRecordingReservations();
1370
1412
  const reservationError = getArtifactPreflightValidationError({
1371
1413
  activeRecordingReservations: activeRecordingReservations.values(),
1372
1414
  args: [],
@@ -1374,9 +1416,9 @@ export default function agentBrowserExtension(pi) {
1374
1416
  outputPath,
1375
1417
  });
1376
1418
  if (reservationError) {
1377
- return buildValidationFailureResult({ attemptedKind: resolvedInput.kind, kind: "invalid", redactedArgs: resolvedInput.redactedArgs, status: "invalid", toolArgs: resolvedInput.toolArgs, toolStdin: resolvedInput.toolStdin, validationError: reservationError });
1419
+ return warnRecordingPersistence(buildValidationFailureResult({ attemptedKind: resolvedInput.kind, kind: "invalid", redactedArgs: resolvedInput.redactedArgs, status: "invalid", toolArgs: resolvedInput.toolArgs, toolStdin: resolvedInput.toolStdin, validationError: reservationError }));
1378
1420
  }
1379
- return applyAgentBrowserOutputPath({ cwd: ctx.cwd, outputPath, preserveTextContent, result });
1421
+ return applyAgentBrowserOutputPath({ cwd: ctx.cwd, outputPath, preserveTextContent, result: warnRecordingPersistence(result) });
1380
1422
  });
1381
1423
  };
1382
1424
  const versionCheckCommand = extractUpstreamCommandTokens(resolvedInput.toolArgs)[0];
@@ -1459,7 +1501,10 @@ export default function agentBrowserExtension(pi) {
1459
1501
  signal?.removeEventListener("abort", abortScript);
1460
1502
  if (leased) {
1461
1503
  try {
1462
- cleanupError = await artifactExecutionQueue.run(() => managedSessionExecutionQueue.run(() => closeScriptSessionLeaseWithinQueue(sessionName, ctx.cwd)));
1504
+ cleanupError = await artifactExecutionQueue.run(() => managedSessionExecutionQueue.run(() => {
1505
+ flushRecordingReservations();
1506
+ return closeScriptSessionLeaseWithinQueue(sessionName, ctx.cwd);
1507
+ }));
1463
1508
  }
1464
1509
  catch {
1465
1510
  cleanupError = "The isolated script session cleanup operation failed.";
@@ -1563,6 +1608,7 @@ export default function agentBrowserExtension(pi) {
1563
1608
  : runElectronHostInput();
1564
1609
  const electronHostResult = compiledElectron?.action === "cleanup"
1565
1610
  ? await artifactExecutionQueue.run(async () => {
1611
+ flushRecordingReservations();
1566
1612
  const reservationError = outputPath ? getArtifactPreflightValidationError({
1567
1613
  activeRecordingReservations: activeRecordingReservations.values(),
1568
1614
  args: [],
@@ -1570,31 +1616,31 @@ export default function agentBrowserExtension(pi) {
1570
1616
  outputPath,
1571
1617
  }) : undefined;
1572
1618
  if (reservationError) {
1573
- return buildValidationFailureResult({ attemptedKind: resolvedInput.kind, kind: "invalid", redactedArgs: resolvedInput.redactedArgs, status: "invalid", toolArgs: resolvedInput.toolArgs, toolStdin: resolvedInput.toolStdin, validationError: reservationError });
1619
+ return warnRecordingPersistence(buildValidationFailureResult({ attemptedKind: resolvedInput.kind, kind: "invalid", redactedArgs: resolvedInput.redactedArgs, status: "invalid", toolArgs: resolvedInput.toolArgs, toolStdin: resolvedInput.toolStdin, validationError: reservationError }));
1574
1620
  }
1575
1621
  const result = await runSerializedElectronHostInput();
1576
- return result ? applyAgentBrowserOutputPath({ cwd: ctx.cwd, outputPath, result }) : result;
1622
+ return result ? applyAgentBrowserOutputPath({ cwd: ctx.cwd, outputPath, result: warnRecordingPersistence(result) }) : result;
1577
1623
  })
1578
1624
  : await runSerializedElectronHostInput();
1579
1625
  if (electronHostResult) {
1580
1626
  return compiledElectron?.action === "cleanup" ? electronHostResult : applyUnserializedOutputPath(electronHostResult);
1581
1627
  }
1582
1628
  const explicitSessionName = extractExplicitSessionName(toolArgs);
1583
- const explicitNamespace = extractExplicitNamespace(toolArgs);
1629
+ const callerOwnedSessionNamespace = explicitSessionName
1630
+ ? resolveAgentBrowserNamespace(toolArgs, getAgentBrowserProcessEnvironment().AGENT_BROWSER_NAMESPACE)
1631
+ : undefined;
1584
1632
  const serializeBrowserCommand = shouldSerializeBrowserCommand({
1585
- explicitNamespace,
1633
+ namespace: callerOwnedSessionNamespace,
1586
1634
  explicitSessionName,
1587
1635
  managedSessionName,
1588
1636
  ownedElectronLaunchRecords,
1589
1637
  ownedManagedSessions,
1590
1638
  });
1591
- const callerOwnedSessionNamespace = explicitSessionName
1592
- ? resolveAgentBrowserNamespace(toolArgs, getAgentBrowserProcessEnvironment().AGENT_BROWSER_NAMESPACE)
1593
- : undefined;
1594
1639
  const callerOwnedSessionQueueKey = !serializeBrowserCommand && explicitSessionName
1595
1640
  ? getSessionContextKey(explicitSessionName, callerOwnedSessionNamespace) ?? explicitSessionName
1596
1641
  : undefined;
1597
1642
  const runBrowserCommand = async () => {
1643
+ flushRecordingReservations();
1598
1644
  const branchRestoreGenerationAtStart = branchRestoreGeneration;
1599
1645
  const generationAtStart = branchStateGeneration;
1600
1646
  const sessionPageStateUpdate = sessionPageState.beginUpdate();
@@ -1620,6 +1666,14 @@ export default function agentBrowserExtension(pi) {
1620
1666
  sessionPageState,
1621
1667
  traceOwners,
1622
1668
  };
1669
+ const selectedPlan = buildExecutionPlan(toolArgs, {
1670
+ freshSessionName: createFreshSessionName(browserRunState.managedSessionBaseName, browserRunState.ephemeralSessionSeed, browserRunState.freshSessionOrdinal + 1),
1671
+ managedSessionActive: browserRunState.managedSessionActive,
1672
+ managedSessionCompatibilityWorkaround: browserRunState.managedSessionCompatibilityWorkaround,
1673
+ managedSessionName: browserRunState.managedSessionName,
1674
+ managedSessionNamespace: browserRunState.managedSessionNamespace,
1675
+ sessionMode: compiledElectron?.action === "launch" ? "fresh" : params.sessionMode ?? "auto",
1676
+ });
1623
1677
  const initialArtifactManifest = browserRunState.artifactManifest;
1624
1678
  const initialNetworkRoutesBySession = browserRunState.networkRoutesBySession;
1625
1679
  const attachedSessionRequested = isAttachedBrowserInvocation(toolArgs)
@@ -1628,7 +1682,8 @@ export default function agentBrowserExtension(pi) {
1628
1682
  && (params.sessionMode === "fresh" || (resolvedInput.kind === "electron" && resolvedInput.compiledElectron.action === "launch"));
1629
1683
  const reusableSessionKey = allocatesFreshManagedSession
1630
1684
  ? undefined
1631
- : callerOwnedSessionQueueKey ?? getSessionContextKey(browserRunState.managedSessionName, browserRunState.managedSessionNamespace);
1685
+ : getSessionContextKey(selectedPlan.sessionName, selectedPlan.namespace)
1686
+ ?? getSessionContextKey(browserRunState.managedSessionName, browserRunState.managedSessionNamespace);
1632
1687
  const attachedSessionKnown = reusableSessionKey !== undefined && attachedSessionKeys.has(reusableSessionKey);
1633
1688
  let result = await runAgentBrowserTool({
1634
1689
  ctx,
@@ -1654,7 +1709,7 @@ export default function agentBrowserExtension(pi) {
1654
1709
  : extractExplicitSessionName(toolArgs);
1655
1710
  const resultNamespace = typeof resultDetails?.namespace === "string"
1656
1711
  ? resultDetails.namespace
1657
- : resolveAgentBrowserNamespace(toolArgs, getAgentBrowserProcessEnvironment().AGENT_BROWSER_NAMESPACE);
1712
+ : selectedPlan.namespace;
1658
1713
  if (branchRestoreStillCurrent) {
1659
1714
  const resultBatchCloseLifecycle = getSuccessfulBatchCloseLifecycle(resultDetails?.batchSteps);
1660
1715
  const resultSessionKey = getSessionContextKey(resultSessionName, resultNamespace) ?? resultSessionName;
@@ -1732,21 +1787,28 @@ export default function agentBrowserExtension(pi) {
1732
1787
  mergeActiveElectronLaunchRecords(ownedElectronLaunchRecords, electronLaunchRecords, {
1733
1788
  branchOwnedLaunchIds: branchOwnedElectronLaunchIds,
1734
1789
  touchedLaunchIds: !result.isError
1735
- ? getTouchedElectronLaunchIds(explicitSessionName ?? browserRunState.managedSessionName, electronLaunchRecords, explicitSessionName ? resolveAgentBrowserNamespace(toolArgs, getAgentBrowserProcessEnvironment().AGENT_BROWSER_NAMESPACE) : browserRunState.managedSessionNamespace)
1790
+ ? getTouchedElectronLaunchIds(explicitSessionName ?? browserRunState.managedSessionName, electronLaunchRecords, resultNamespace)
1736
1791
  : undefined,
1737
1792
  });
1738
1793
  if (serializeBrowserCommand)
1739
1794
  branchStateGeneration += 1;
1740
1795
  }
1741
- return applyAgentBrowserOutputPath({ cwd: ctx.cwd, outputPath, preserveTextContent: Array.isArray(params.args) && params.args.includes("--json"), result });
1796
+ return applyAgentBrowserOutputPath({ cwd: ctx.cwd, outputPath, preserveTextContent: Array.isArray(params.args) && params.args.includes("--json"), result: warnRecordingPersistence(result) });
1742
1797
  };
1743
1798
  const closesAllSessions = commandClosesAllSessions(toolArgs, resolvedInput.toolStdin);
1744
- const closeAllNamespace = closesAllSessions
1745
- ? resolveAgentBrowserNamespace(toolArgs, getAgentBrowserProcessEnvironment().AGENT_BROWSER_NAMESPACE)
1746
- : undefined;
1747
1799
  const runWithinSessionQueue = () => {
1748
1800
  if (closesAllSessions)
1749
- return managedSessionExecutionQueue.run(() => callerOwnedSessionExecutionQueues.runExclusive(closeAllNamespace, runBrowserCommand));
1801
+ return managedSessionExecutionQueue.run(() => {
1802
+ const plan = buildExecutionPlan(toolArgs, {
1803
+ freshSessionName: createFreshSessionName(managedSessionBaseName, ephemeralSessionSeed, freshSessionOrdinal + 1),
1804
+ managedSessionActive,
1805
+ managedSessionCompatibilityWorkaround,
1806
+ managedSessionName,
1807
+ managedSessionNamespace,
1808
+ sessionMode: params.sessionMode ?? "auto",
1809
+ });
1810
+ return callerOwnedSessionExecutionQueues.runExclusive(plan.namespace, runBrowserCommand);
1811
+ });
1750
1812
  if (serializeBrowserCommand)
1751
1813
  return managedSessionExecutionQueue.run(runBrowserCommand);
1752
1814
  return callerOwnedSessionQueueKey
@@ -1765,10 +1827,11 @@ export default function agentBrowserExtension(pi) {
1765
1827
  });
1766
1828
  if (!artifactValidationError)
1767
1829
  return runWithinSessionQueue();
1830
+ flushRecordingReservations();
1768
1831
  return applyAgentBrowserOutputPath({
1769
1832
  cwd: ctx.cwd,
1770
1833
  outputPath,
1771
- result: buildValidationFailureResult({
1834
+ result: warnRecordingPersistence(buildValidationFailureResult({
1772
1835
  attemptedKind: resolvedInput.kind,
1773
1836
  kind: "invalid",
1774
1837
  redactedArgs: resolvedInput.redactedArgs,
@@ -1776,7 +1839,7 @@ export default function agentBrowserExtension(pi) {
1776
1839
  toolArgs: resolvedInput.toolArgs,
1777
1840
  toolStdin: resolvedInput.toolStdin,
1778
1841
  validationError: artifactValidationError,
1779
- }),
1842
+ })),
1780
1843
  });
1781
1844
  });
1782
1845
  },