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.
- package/CHANGELOG.md +30 -1
- package/README.md +20 -5
- package/dist/extensions/agent-browser/index.js +138 -75
- package/dist/extensions/agent-browser/lib/command-taxonomy.js +6 -5
- package/dist/extensions/agent-browser/lib/electron/cleanup.js +10 -1
- package/dist/extensions/agent-browser/lib/input-modes/params.js +20 -7
- package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +0 -1
- package/dist/extensions/agent-browser/lib/managed-session-restore.js +13 -12
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.js +6 -25
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +2 -3
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +10 -3
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +4 -1
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/wait-timeouts.js +1 -1
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +104 -117
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +61 -35
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +100 -135
- package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +3 -1
- package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +3 -1
- package/dist/extensions/agent-browser/lib/page-target-validation.js +10 -10
- package/dist/extensions/agent-browser/lib/parsing.js +7 -0
- package/dist/extensions/agent-browser/lib/playbook.js +5 -8
- package/dist/extensions/agent-browser/lib/process-identity.js +10 -2
- package/dist/extensions/agent-browser/lib/process.js +23 -7
- package/dist/extensions/agent-browser/lib/recording-reservations.js +3 -1
- package/dist/extensions/agent-browser/lib/results/envelope.js +4 -1
- package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +45 -43
- package/dist/extensions/agent-browser/lib/results/recovery-actions.js +4 -4
- package/dist/extensions/agent-browser/lib/runtime.js +18 -2
- package/dist/extensions/agent-browser/lib/session-page-state.js +29 -10
- package/docs/ARCHITECTURE.md +7 -4
- package/docs/COMMAND_REFERENCE.md +22 -15
- package/docs/ELECTRON.md +6 -6
- package/docs/RELEASE.md +14 -5
- package/docs/REQUIREMENTS.md +1 -1
- package/docs/SUPPORT_MATRIX.md +13 -3
- package/docs/TOOL_CONTRACT.md +33 -22
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
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
|
|
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`,
|
|
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`
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
889
|
-
|
|
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:
|
|
895
|
-
reason:
|
|
896
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
1111
|
-
if (restoredRecordingState.terminal.has(key))
|
|
1112
|
-
|
|
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
|
|
1157
|
+
for (const [key, reservation] of recordingSessionTombstones) {
|
|
1121
1158
|
restoredRecordingState.active.delete(key);
|
|
1122
|
-
|
|
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(() =>
|
|
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
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
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(() =>
|
|
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
|
|
1629
|
+
const callerOwnedSessionNamespace = explicitSessionName
|
|
1630
|
+
? resolveAgentBrowserNamespace(toolArgs, getAgentBrowserProcessEnvironment().AGENT_BROWSER_NAMESPACE)
|
|
1631
|
+
: undefined;
|
|
1584
1632
|
const serializeBrowserCommand = shouldSerializeBrowserCommand({
|
|
1585
|
-
|
|
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
|
-
:
|
|
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
|
-
:
|
|
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,
|
|
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(() =>
|
|
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
|
},
|