pi-agent-browser-native 0.2.60 → 0.2.62

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 (34) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +11 -11
  3. package/dist/extensions/agent-browser/index.js +51 -30
  4. package/dist/extensions/agent-browser/lib/argv-descriptor.js +6 -3
  5. package/dist/extensions/agent-browser/lib/argv-grammar.js +29 -1
  6. package/dist/extensions/agent-browser/lib/command-policy.js +10 -1
  7. package/dist/extensions/agent-browser/lib/command-taxonomy.js +7 -0
  8. package/dist/extensions/agent-browser/lib/input-modes/job.js +1 -23
  9. package/dist/extensions/agent-browser/lib/input-modes/lookups.js +5 -2
  10. package/dist/extensions/agent-browser/lib/input-modes/params.js +2 -1
  11. package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +25 -1
  12. package/dist/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.js +4 -3
  13. package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +25 -17
  14. package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +14 -11
  15. package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +3 -2
  16. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/direct-anchor-download.js +2 -1
  17. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/network-page-filter.js +3 -3
  18. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/scroll-shims.js +5 -4
  19. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/snapshot-filter.js +4 -4
  20. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +43 -27
  21. package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +66 -54
  22. package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +24 -15
  23. package/dist/extensions/agent-browser/lib/playbook.js +4 -4
  24. package/dist/extensions/agent-browser/lib/results/next-actions.js +19 -1
  25. package/dist/extensions/agent-browser/lib/results/presentation/batch.js +8 -7
  26. package/dist/extensions/agent-browser/lib/results/presentation.js +2 -1
  27. package/dist/extensions/agent-browser/lib/runtime.js +62 -10
  28. package/dist/extensions/agent-browser/lib/session-page-state.js +14 -7
  29. package/docs/ARCHITECTURE.md +5 -5
  30. package/docs/COMMAND_REFERENCE.md +66 -24
  31. package/docs/SUPPORT_MATRIX.md +10 -8
  32. package/docs/TOOL_CONTRACT.md +24 -24
  33. package/package.json +1 -1
  34. package/scripts/agent-browser-capability-baseline.mjs +36 -4
package/CHANGELOG.md CHANGED
@@ -2,6 +2,36 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.2.62 - 2026-06-26
6
+
7
+ ### Changed
8
+
9
+ - Rebaselined upstream capability metadata, command reference, support docs, playbook guidance, and real-upstream output-shape metadata for `agent-browser` `0.31.0` / vercel-labs/agent-browser@5acf7f9.
10
+ - Added upstream `--namespace`, `--restore`, restore-check flags, and `session id` / `session info` support to wrapper parsing, session policy, launch-scoped flag handling, and docs.
11
+
12
+ ### Fixed
13
+
14
+ - Made wrapper-managed browser state namespace-aware across tab/ref tracking, allowed-domain policy, trace/profiler ownership, branch restore, cleanup, nextActions, and fresh-launch recovery.
15
+ - Reduced post-click diagnostic fragility for upstream `agent-browser 0.31.0`: CSS selector clicks without upstream href/navigation fields now skip immediate helper probes, while ref/href clicks keep navigation summaries and overlay diagnostics.
16
+ - Preserved namespace context for managed-session failure recovery and missing-binary nextActions.
17
+
18
+ ### Validation
19
+
20
+ - Ran `npm run verify`, `npm run docs -- command-reference check`, `npm run typecheck`, focused runtime/diagnostic/passthrough tests, `npm run verify -- real-upstream`, `npm run verify -- dogfood`, `npm run verify -- lifecycle`, `npm run smoke:platform:doctor`, `npm run smoke:platform:all`, `npm publish --dry-run`, and `git diff --check`.
21
+ - Ran the required reviewer subagent loop until it returned `no findings`.
22
+
23
+ ## 0.2.61 - 2026-06-24
24
+
25
+ ### Changed
26
+
27
+ - Rebaselined upstream capability metadata, command reference, support docs, playbook guidance, and real-upstream output-shape metadata for `agent-browser` `0.30.1` / vercel-labs/agent-browser@7379f7dbea76ad8dbf47f177349c4c3ce9263dcb.
28
+ - Removed the constrained `job.assertUrl` glob-to-`wait --fn` workaround now that upstream `wait --url` matches glob patterns such as `**/dashboard` against the full active URL.
29
+
30
+ ### Validation
31
+
32
+ - Ran `npm run verify`, `npm run docs`, focused `npx tsx --test test/agent-browser.extension-input-modes.test.ts`, `npm run verify -- command-reference`, and `git diff --check`.
33
+ - Probed upstream `agent-browser 0.30.1` directly: `wait --url "**/dashboard"` succeeds after `pushstate /dashboard`; `find ... uncheck` and `wait <selector> --state hidden|detached` still fail, so only the URL-glob workaround was removed.
34
+
5
35
  ## 0.2.60 - 2026-06-24
6
36
 
7
37
  ### Changed
package/README.md CHANGED
@@ -79,13 +79,13 @@ 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/lib/results/shared.ts` (re-export barrel), `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 CSS selectors / `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`, and 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
- | Dashboard scroll commands can look successful while nothing moves | Samples viewport and prominent scroll-container positions around top-level `scroll` calls; unchanged positions produce `details.scrollNoop`, visible recovery guidance, and exact `nextActions` for snapshot/screenshot verification | [`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` |
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`, and 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
+ | Dashboard scroll commands can look successful while nothing moves | Samples viewport and prominent scroll-container positions around large top-level `scroll` calls on an existing or fresh managed session; unchanged positions produce `details.scrollNoop`, visible recovery guidance, and exact `nextActions` for snapshot/screenshot verification | [`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`, `semanticAction`, and `job`; for custom combobox clicks, detects focused controls with explicit `aria-expanded` state but no visible options and returns `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 | After successful `record start` / `record restart`, warns when `ffmpeg` is not on `PATH` so agents can install or fix PATH before `record stop` | [`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` |
86
86
  | Direct binary help may be blocked in agent sessions | Publishes a repo-readable command reference and verifies it against the target upstream version | `npm run verify` |
87
87
  | 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) |
88
- | Agents need bundled `skills` text and local setup/status commands without touching the live session | Treats `skills list`, `skills get …`, `skills path …`, local auth profile management (`auth save/list/show/delete/remove`), `profiles`, `dashboard`, `device list`, `doctor`, `install`, `upgrade`, `session list`, `plugin add/list/show/run`, `mcp --help`, and targeted/all saved-state maintenance (`state clear --all`, `state clear -a`, named clear, or `state clean --older-than <days>`) as sessionless reads/actions: no implicit managed `--session` under default `sessionMode: "auto"` (same session-ownership goal as plain-text `--help` / `--version`), while bare `mcp` server calls are blocked and provider/browser-backed workflows stay thin passthroughs that require upstream setup and credentials | [`docs/COMMAND_REFERENCE.md`](docs/COMMAND_REFERENCE.md#built-in-skills), `extensions/agent-browser/lib/command-policy.ts`, `extensions/agent-browser/lib/runtime.ts` |
88
+ | Agents need bundled `skills` text and local setup/status commands without touching the live session | Treats `skills list`, `skills get …`, `skills path …`, local auth profile management (`auth save/list/show/delete/remove`), `profiles`, `dashboard`, `device list`, `doctor`, `install`, `upgrade`, `session list`, `session id`, `session info`, `plugin add/list/show/run`, `mcp --help`, and targeted/all saved-state maintenance (`state clear --all`, `state clear -a`, named clear, or `state clean --older-than <days>`) as sessionless reads/actions: no implicit managed `--session` under default `sessionMode: "auto"` (same session-ownership goal as plain-text `--help` / `--version`), while bare `mcp` server calls are blocked and provider/browser-backed workflows stay thin passthroughs that require upstream setup and credentials | [`docs/COMMAND_REFERENCE.md`](docs/COMMAND_REFERENCE.md#built-in-skills), `extensions/agent-browser/lib/command-policy.ts`, `extensions/agent-browser/lib/runtime.ts` |
89
89
 
90
90
  ## Fastest way to try it
91
91
 
@@ -338,16 +338,16 @@ Typical pitfalls:
338
338
  - Do not reuse `@e…` refs across navigation. The wrapper records the latest snapshot refs per session and fails mutation-prone stale/recycled refs before upstream can silently hit a different current-page element; use the session-aware `refresh-interactive-refs` next action.
339
339
  - 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)).
340
340
  - 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 inserttext` or `keyboard type` with the intended text. 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).
341
- - 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 selectors, `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.
342
- - 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** `click` succeeds (unified command `click`, not a `batch` step), upstream reports `data.clicked`, and the tab URL is unchanged 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 uses `details.navigationSummary`, which is populated with one read-only `eval` summary when the click JSON omits **both** string `data.url` and `data.title`; if upstream already includes either, overlay diagnostics are skipped here. Also skipped when tab correction or about-blank recovery already ran on that result.
343
- - If `get text <selector>` reads a non-ref 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.
341
+ - 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.
342
+ - 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.
343
+ - 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.
344
344
  - In wrapper-tracked attached Electron sessions, broad selectors such as `body`, `html`, `main`, or `[role=application]` may read the whole app shell. The wrapper may add `Broad Electron get text selector warning`, `details.electronGetTextScopeWarning`, and `snapshot-for-electron-text-scope`; ordinary browser pages, including `file://` fixtures, do not qualify without Electron launch provenance. Prefer `snapshot -i`, a current `@ref`, or a narrower panel selector.
345
345
 
346
346
  ### Constrained browser jobs
347
347
 
348
348
  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 title/URL, 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 or `inspect-current-page-after-timeout` when the first incomplete step may be mutating and needs state inspection before a shorter follow-up flow (see [`docs/TOOL_CONTRACT.md#details`](docs/TOOL_CONTRACT.md#details)). There is no separate catalog of reusable named browser recipes above `job`, `qa`, and raw `batch`; 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.
349
349
 
350
- **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; glob patterns compile to a `wait --fn` URL predicate so examples like `**/shipping` do not depend on upstream `wait --url` matcher quirks. In those patterns, `*` stays within one path segment and `**` can cross `/`; exact URLs with no `*` stay literal.
350
+ **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.
351
351
 
352
352
  ```json
353
353
  {
@@ -436,7 +436,7 @@ For local app debugging, `sourceLookup` can gather candidate component/file loca
436
436
 
437
437
  This is an experiment, not a guarantee. React hints require a session opened with `--enable react-devtools`, and many builds do not expose useful sourcemap/source metadata; `status: "no-candidates"` is common when nothing matched, and `status: "unsupported"` only when no candidates were found **and** a compiled `react` batch step failed (if DOM or workspace search still produced candidates, you get `candidates-found` instead). For wrapper-tracked packaged Electron apps, a no-candidate result includes `details.sourceLookup.workspaceRoot`, optional `details.sourceLookup.electronContext`, limitations explaining that the scan is limited to the Pi cwd and does not unpack app bundles/`app.asar`, plus Electron snapshot/probe/tab next actions when a launch is known.
438
438
 
439
- `networkSourceLookup` is the matching failed-request experiment. It runs `network request <id>` when `requestId` is present and/or `network requests --filter …` when `filter` or `url` is present (`url` supplies the filter pattern when `filter` is omitted); add `session` when the generated batch should target an explicit upstream session. It merges failed-request rows from the batch JSON with initiator-style hints and a bounded workspace literal scan (`maxWorkspaceFiles` defaults to 2000, cap 5000), surfaces everything under `details.networkSourceLookup`, and avoids automatic blame or edits. Compact `network requests` results with safe request IDs also add `details.nextActions` for request details, bounded `networkSourceLookup` on actionable failures, path filtering, diagnostic-buffer clearing before a repro, or HAR capture so agents can branch without guessing request-id syntax. For noisy aggregate buffers, wrapper-side `network requests --current-page` / `--current-origin` keeps only rows matching the active page origin, while `--current-url` keeps exact active-document URL rows and reports counts in `details.networkRequestsPageFilter`. When the wrapper has seen `network route` in the same session, pending fetch/XHR rows or CORS-looking errors that match the route surface `details.networkRouteDiagnostics` plus executable follow-ups to inspect the request or start HAR capture; same-origin/CORS-correct fixture retry guidance stays in prose. Network diagnostics are read-only for wrapper page state: request URLs in `network request` or generated `networkSourceLookup` batches do not replace the session’s active page target or invalidate page-scoped refs from the app page.
439
+ `networkSourceLookup` is the matching failed-request experiment. It runs `network request <id>` when `requestId` is present and/or `network requests --filter …` when `filter` or `url` is present (`url` supplies the filter pattern when `filter` is omitted); add `namespace` / `session` when the generated batch should target an explicit upstream namespace/session. It merges failed-request rows from the batch JSON with initiator-style hints and a bounded workspace literal scan (`maxWorkspaceFiles` defaults to 2000, cap 5000), surfaces everything under `details.networkSourceLookup`, and avoids automatic blame or edits. Compact `network requests` results with safe request IDs also add `details.nextActions` for request details, bounded `networkSourceLookup` on actionable failures, path filtering, diagnostic-buffer clearing before a repro, or HAR capture so agents can branch without guessing request-id syntax. For noisy aggregate buffers, wrapper-side `network requests --current-page` / `--current-origin` keeps only rows matching the active page origin, while `--current-url` keeps exact active-document URL rows and reports counts in `details.networkRequestsPageFilter`. When the wrapper has seen `network route` in the same session, pending fetch/XHR rows or CORS-looking errors that match the route surface `details.networkRouteDiagnostics` plus executable follow-ups to inspect the request or start HAR capture; same-origin/CORS-correct fixture retry guidance stays in prose. Network diagnostics are read-only for wrapper page state: request URLs in `network request` or generated `networkSourceLookup` batches do not replace the session’s active page target or invalidate page-scoped refs from the app page.
440
440
 
441
441
  ```json
442
442
  { "networkSourceLookup": { "requestId": "req-1", "url": "/api/fail" } }
@@ -481,9 +481,9 @@ Use these rules:
481
481
  - Use public/temp profiles for tests and examples.
482
482
  - Do not assume `--profile Default` is correct. Ask the agent to run `profiles` to list Chrome profile directory names, then `doctor` if profile/user-data-dir resolution still fails.
483
483
  - For non-Chrome Chromium browsers such as Brave, Edge, Arc, or Vivaldi, use `--executable-path <path>` when upstream can launch that executable. If you need that browser's existing login state, use the browser's real profile/user-data directory path when upstream accepts it, or attach with `--auto-connect` / `connect` to a debug-enabled running browser when appropriate.
484
- - Use `sessionMode: "fresh"` when switching from public browsing to `--profile`, `--executable-path`, `--session-name`, `--cdp`, `--state`, `--auto-connect`, `--init-script`, `--enable`, `-p` / `--provider`, or iOS `--device`.
484
+ - Use `sessionMode: "fresh"` when switching from public browsing to `--profile`, `--executable-path`, `--restore`, `--restore-save`, restore check flags, `--namespace`, `--session-name`, `--cdp`, `--state`, `--auto-connect`, `--init-script`, `--enable`, `-p` / `--provider`, or iOS `--device`.
485
485
  - Use `--session` when you want to manage a live upstream session name yourself.
486
- - Do not treat `--session` as persisted auth or tab restore after `close`, `quit`, or `exit`; use `--profile`, `--session-name`, or `--state` for persistence.
486
+ - Do not treat `--session` alone as persisted auth or tab restore after `close`, `quit`, or `exit`; use `--session <id> --restore`, `--profile`, or `--state` for persistence.
487
487
  - Prefer page actions and storage checks over cookie dumps. `cookies get` can expose real profile cookies.
488
488
  - Prefer `auth save --password-stdin` over putting passwords in `args`; the wrapper only accepts caller `stdin` for `batch`, `eval --stdin`, and `auth save --password-stdin` (top-level `job` and `qa` compile to `batch` and supply their own stdin).
489
489
  - Use `state save <path>` / `state load <path>` for portable test state. `state save` is reported as a file artifact with verification metadata; if an upstream-successful artifact command reports a non-pending file path that the wrapper cannot find on disk, the tool fails with `failureCategory: "artifact-missing"` instead of treating the path as durable. `state load` may mention a path but is not treated as a newly saved artifact.
@@ -703,7 +703,7 @@ These calls return plain text and stay stateless: the extension does not inject
703
703
 
704
704
  <!-- agent-browser-playbook:start wrapper-tab-recovery -->
705
705
  <!-- Generated from extensions/agent-browser/lib/playbook.ts. Run `npm run docs -- playbook write` to update. -->
706
- - After launch-scoped open/goto/navigate calls that can restore existing tabs (for example --profile, --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.
706
+ - After launch-scoped open/goto/navigate calls that can restore existing tabs (for example --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.
707
707
  - After the wrapper observes tab-drift risk for a session (for example profile restore 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 is known.
708
708
  - 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.
709
709
  - 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.
@@ -10,14 +10,14 @@ import { fileURLToPath } from "node:url";
10
10
  import { Text } from "@earendil-works/pi-tui";
11
11
  import { PROJECT_RULE_PROMPT, buildBrowserDefaultProfileGuideline, buildBrowserExecutablePathGuideline, buildToolPromptGuidelines, } from "./lib/playbook.js";
12
12
  import { SessionPageState } from "./lib/session-page-state.js";
13
- import { createEphemeralSessionSeed, createFreshSessionName, createImplicitSessionName, extractCommandTokens, getImplicitSessionCloseTimeoutMs, getImplicitSessionIdleTimeoutMs, extractExplicitSessionName, restoreManagedSessionStateFromBranch, validateToolArgs, } from "./lib/runtime.js";
13
+ import { createEphemeralSessionSeed, createFreshSessionName, createImplicitSessionName, extractCommandTokens, extractExplicitNamespace, getImplicitSessionCloseTimeoutMs, getImplicitSessionIdleTimeoutMs, extractExplicitSessionName, restoreManagedSessionStateFromBranch, validateToolArgs, } from "./lib/runtime.js";
14
14
  import { isRecord } from "./lib/parsing.js";
15
15
  import { buildPromptPolicy, getLatestUserPrompt, shouldAppendBrowserSystemPrompt } from "./lib/prompt-policy.js";
16
16
  import { isCloseCommand } from "./lib/command-taxonomy.js";
17
17
  import { cleanupSecureTempArtifacts, } from "./lib/temp.js";
18
18
  import { AGENT_BROWSER_PARAMS, } from "./lib/input-modes.js";
19
19
  import { parseAllowedDomainsPolicyFromArgs } from "./lib/navigation-policy.js";
20
- import { closeManagedSession, runAgentBrowserTool } from "./lib/orchestration/browser-run.js";
20
+ import { closeManagedSession, getSessionContextKey, runAgentBrowserTool } from "./lib/orchestration/browser-run.js";
21
21
  import { findElectronLaunchRecordForSession, getActiveElectronRecords } from "./lib/orchestration/browser-run/session-state.js";
22
22
  import { parseBatchStdinJsonArray } from "./lib/orchestration/batch-stdin.js";
23
23
  import { ELECTRON_POST_COMMAND_STATUS_SETTLE_MS, ELECTRON_PROFILE_ISOLATION_DETAILS, cleanupActiveElectronHostLaunches, handleElectronHostInput, restoreElectronLaunchRecordsFromBranch, } from "./lib/orchestration/electron-host/index.js";
@@ -95,6 +95,8 @@ function restoreAllowedDomainsBySessionFromBranch(branch) {
95
95
  const args = getToolResultArgs(details);
96
96
  const command = typeof details.command === "string" ? details.command : extractCommandTokens(args)[0];
97
97
  const sessionName = typeof details.sessionName === "string" ? details.sessionName : undefined;
98
+ const namespace = typeof details.namespace === "string" ? details.namespace : undefined;
99
+ const sessionKey = getSessionContextKey(sessionName, namespace);
98
100
  const explicitSessionName = extractExplicitSessionName(args);
99
101
  const outcome = getManagedSessionOutcome(details);
100
102
  const outcomeSucceeded = outcome?.succeeded === true;
@@ -104,17 +106,18 @@ function restoreAllowedDomainsBySessionFromBranch(branch) {
104
106
  if (outcomeSucceeded && outcomeStatus === "closed") {
105
107
  const closedSessionName = outcomeAttemptedSessionName ?? outcomeCurrentSessionName ?? sessionName;
106
108
  if (closedSessionName)
107
- restoredPolicies.delete(closedSessionName);
109
+ restoredPolicies.delete(getSessionContextKey(closedSessionName, namespace) ?? closedSessionName);
108
110
  }
109
111
  if (outcomeSucceeded && outcomeStatus === "replaced") {
110
112
  const replacedSessionName = typeof outcome.replacedSessionName === "string" ? outcome.replacedSessionName : undefined;
113
+ const replacedSessionNamespace = typeof outcome.replacedSessionNamespace === "string" ? outcome.replacedSessionNamespace : namespace;
111
114
  if (replacedSessionName)
112
- restoredPolicies.delete(replacedSessionName);
115
+ restoredPolicies.delete(getSessionContextKey(replacedSessionName, replacedSessionNamespace) ?? replacedSessionName);
113
116
  }
114
117
  if (succeeded && isCloseCommand(command)) {
115
118
  const closedSessionName = explicitSessionName ?? sessionName ?? outcomeAttemptedSessionName ?? outcomeCurrentSessionName;
116
119
  if (closedSessionName)
117
- restoredPolicies.delete(closedSessionName);
120
+ restoredPolicies.delete(getSessionContextKey(closedSessionName, namespace) ?? closedSessionName);
118
121
  }
119
122
  const electron = isRecord(details.electron) ? details.electron : undefined;
120
123
  const cleanup = isRecord(electron?.cleanup) ? electron.cleanup : undefined;
@@ -126,22 +129,27 @@ function restoreAllowedDomainsBySessionFromBranch(branch) {
126
129
  const outcomeKeepsSessionCurrent = outcome?.activeAfter === true
127
130
  && (outcomeStatus === "created" || outcomeStatus === "replaced" || outcomeStatus === "unchanged")
128
131
  && outcomeCurrentSessionName === sessionName;
129
- const policy = (succeeded || outcomeKeepsSessionCurrent) && sessionName && !isCloseCommand(command) ? parseAllowedDomainsPolicyFromArgs(args) : undefined;
130
- if (policy && sessionName)
131
- restoredPolicies.set(sessionName, policy);
132
+ const policy = (succeeded || outcomeKeepsSessionCurrent) && sessionKey && !isCloseCommand(command) ? parseAllowedDomainsPolicyFromArgs(args) : undefined;
133
+ if (policy && sessionKey)
134
+ restoredPolicies.set(sessionKey, policy);
132
135
  }
133
136
  return restoredPolicies;
134
137
  }
135
138
  function trackOwnedManagedSession(sessions, sessionName, cwd, options = {}) {
136
139
  if (!sessionName)
137
140
  return;
138
- const existing = sessions.get(sessionName);
141
+ const key = getSessionContextKey(sessionName, options.namespace) ?? sessionName;
142
+ const existing = sessions.get(key);
139
143
  const branchOwned = existing && !existing.branchOwned ? false : options.branchOwned === true;
140
- sessions.set(sessionName, { branchOwned, cwd });
144
+ sessions.set(key, { branchOwned, cwd, namespace: options.namespace, sessionName });
141
145
  }
142
- function untrackOwnedManagedSession(sessions, sessionName) {
143
- if (sessionName)
146
+ function untrackOwnedManagedSession(sessions, sessionName, namespace) {
147
+ if (!sessionName)
148
+ return;
149
+ if (sessionName.includes("\u0000"))
144
150
  sessions.delete(sessionName);
151
+ else
152
+ sessions.delete(getSessionContextKey(sessionName, namespace) ?? sessionName);
145
153
  }
146
154
  function untrackOwnedManagedSessionFromBranchClose(sessions, sessionName, activeBranchRank, closeBranchRank) {
147
155
  if (!sessionName || closeBranchRank === undefined)
@@ -163,7 +171,8 @@ function syncOwnedManagedSessionsFromResult(sessions, result, cwd) {
163
171
  const currentSessionName = typeof outcome.currentSessionName === "string" ? outcome.currentSessionName : undefined;
164
172
  const attemptedSessionName = typeof outcome.attemptedSessionName === "string" ? outcome.attemptedSessionName : undefined;
165
173
  if (outcome.activeAfter === true && (status === "created" || status === "replaced" || status === "unchanged")) {
166
- trackOwnedManagedSession(sessions, currentSessionName, cwd);
174
+ const namespace = isRecord(details) && typeof details.namespace === "string" ? details.namespace : undefined;
175
+ trackOwnedManagedSession(sessions, currentSessionName, cwd, { namespace });
167
176
  }
168
177
  if (succeeded && status === "closed") {
169
178
  untrackOwnedManagedSession(sessions, attemptedSessionName ?? currentSessionName);
@@ -320,6 +329,7 @@ function collectBranchManagedResourceEvents(branch) {
320
329
  const args = Array.isArray(details.args) && details.args.every((arg) => typeof arg === "string") ? details.args : [];
321
330
  const command = typeof details.command === "string" ? details.command : extractCommandTokens(args)[0];
322
331
  const sessionName = typeof details.sessionName === "string" ? details.sessionName : undefined;
332
+ const namespace = typeof details.namespace === "string" ? details.namespace : undefined;
323
333
  const sessionMode = details.sessionMode === "fresh" || details.sessionMode === "auto" ? details.sessionMode : undefined;
324
334
  const usedImplicitSession = details.usedImplicitSession === true;
325
335
  const explicitSessionName = extractExplicitSessionName(args);
@@ -329,19 +339,20 @@ function collectBranchManagedResourceEvents(branch) {
329
339
  const outcomeCurrentSessionName = typeof outcome?.currentSessionName === "string" ? outcome.currentSessionName : undefined;
330
340
  const outcomeAttemptedSessionName = typeof outcome?.attemptedSessionName === "string" ? outcome.attemptedSessionName : undefined;
331
341
  if (outcomeSucceeded && outcome.activeAfter === true && (outcomeStatus === "created" || outcomeStatus === "replaced" || outcomeStatus === "unchanged")) {
332
- setBranchRankForString(events.managedSessionActiveRanks, outcomeCurrentSessionName, eventRank);
342
+ setBranchRankForString(events.managedSessionActiveRanks, getSessionContextKey(outcomeCurrentSessionName, namespace), eventRank);
333
343
  }
334
344
  if (outcomeSucceeded && outcomeStatus === "closed") {
335
- setBranchRankForString(events.managedSessionCloseRanks, outcomeAttemptedSessionName ?? outcomeCurrentSessionName ?? sessionName, eventRank);
345
+ setBranchRankForString(events.managedSessionCloseRanks, getSessionContextKey(outcomeAttemptedSessionName ?? outcomeCurrentSessionName ?? sessionName, namespace), eventRank);
336
346
  }
337
347
  if (outcomeSucceeded && outcomeStatus === "replaced") {
338
- setBranchRankForString(events.managedSessionCloseRanks, outcome.replacedSessionName, eventRank);
348
+ const replacedSessionNamespace = typeof outcome.replacedSessionNamespace === "string" ? outcome.replacedSessionNamespace : namespace;
349
+ setBranchRankForString(events.managedSessionCloseRanks, getSessionContextKey(typeof outcome.replacedSessionName === "string" ? outcome.replacedSessionName : undefined, replacedSessionNamespace), eventRank);
339
350
  }
340
351
  if (succeeded && !isCloseCommand(command) && sessionName && (usedImplicitSession || sessionMode === "fresh")) {
341
- events.managedSessionActiveRanks.set(sessionName, eventRank);
352
+ events.managedSessionActiveRanks.set(getSessionContextKey(sessionName, namespace) ?? sessionName, eventRank);
342
353
  }
343
354
  if (succeeded && isCloseCommand(command)) {
344
- setBranchRankForString(events.managedSessionCloseRanks, explicitSessionName ?? sessionName ?? outcomeAttemptedSessionName ?? outcomeCurrentSessionName, eventRank);
355
+ setBranchRankForString(events.managedSessionCloseRanks, getSessionContextKey(explicitSessionName ?? sessionName ?? outcomeAttemptedSessionName ?? outcomeCurrentSessionName, namespace), eventRank);
345
356
  }
346
357
  const electron = isRecord(details.electron) ? details.electron : undefined;
347
358
  const launch = electron && isElectronLaunchRecord(electron.launch) ? electron.launch : undefined;
@@ -384,13 +395,14 @@ function syncElectronCleanupManagedSessions(sessions, cleanupResults) {
384
395
  untrackOwnedManagedSession(sessions, sessionName);
385
396
  }
386
397
  }
387
- async function closeOwnedManagedSessionsExcept(sessions, keepSessionName, timeoutMs) {
388
- for (const [sessionName, owner] of [...sessions]) {
389
- if (sessionName === keepSessionName)
398
+ async function closeOwnedManagedSessionsExcept(sessions, keepSessionName, timeoutMs, keepNamespace) {
399
+ const keepKey = getSessionContextKey(keepSessionName, keepNamespace);
400
+ for (const [key, owner] of [...sessions]) {
401
+ if (key === keepKey)
390
402
  continue;
391
- const error = await closeManagedSession({ cwd: owner.cwd, sessionName, timeoutMs });
403
+ const error = await closeManagedSession({ cwd: owner.cwd, namespace: owner.namespace, sessionName: owner.sessionName, timeoutMs });
392
404
  if (!error)
393
- sessions.delete(sessionName);
405
+ sessions.delete(key);
394
406
  }
395
407
  }
396
408
  async function closeOwnedManagedSessions(sessions, timeoutMs) {
@@ -410,7 +422,7 @@ function shouldSerializeBrowserCommand(options) {
410
422
  return true;
411
423
  if (options.explicitSessionName === options.managedSessionName)
412
424
  return true;
413
- if (options.ownedManagedSessions.has(options.explicitSessionName))
425
+ if (options.ownedManagedSessions.has(getSessionContextKey(options.explicitSessionName, options.explicitNamespace) ?? options.explicitSessionName))
414
426
  return true;
415
427
  return getActiveElectronRecords(options.ownedElectronLaunchRecords).some((record) => record.sessionName === options.explicitSessionName);
416
428
  }
@@ -470,6 +482,7 @@ export default function agentBrowserExtension(pi) {
470
482
  let managedSessionBaseName = createImplicitSessionName(undefined, process.cwd(), ephemeralSessionSeed);
471
483
  let managedSessionName = managedSessionBaseName;
472
484
  let managedSessionCwd = process.cwd();
485
+ let managedSessionNamespace;
473
486
  let freshSessionOrdinal = 0;
474
487
  let sessionPageState = new SessionPageState();
475
488
  let traceOwners = new Map();
@@ -483,10 +496,11 @@ export default function agentBrowserExtension(pi) {
483
496
  const ownedManagedSessions = new Map();
484
497
  const managedSessionExecutionQueue = new AsyncExecutionQueue();
485
498
  let branchStateGeneration = 0;
486
- const clearSessionScopedBrowserState = (sessionName) => {
487
- allowedDomainsBySession.delete(sessionName);
488
- networkRoutesBySession.delete(sessionName);
489
- sessionPageState.clearSession(sessionName);
499
+ const clearSessionScopedBrowserState = (sessionName, namespace) => {
500
+ const key = getSessionContextKey(sessionName, namespace) ?? sessionName;
501
+ allowedDomainsBySession.delete(key);
502
+ networkRoutesBySession.delete(key);
503
+ sessionPageState.clearSession(key);
490
504
  };
491
505
  const restoreBranchBackedState = (ctx, options) => {
492
506
  branchStateGeneration += 1;
@@ -516,6 +530,7 @@ export default function agentBrowserExtension(pi) {
516
530
  ? previousManagedSessionName
517
531
  : createFreshSessionName(managedSessionBaseName, ephemeralSessionSeed, nextFreshSessionOrdinal)
518
532
  : restoredState.sessionName;
533
+ managedSessionNamespace = shouldReservePostCloseSession ? undefined : restoredState.namespace;
519
534
  managedSessionCwd = ctx.cwd;
520
535
  freshSessionOrdinal = nextFreshSessionOrdinal;
521
536
  sessionPageState = SessionPageState.fromBranch(branch);
@@ -536,7 +551,7 @@ export default function agentBrowserExtension(pi) {
536
551
  removeInactiveOwnedElectronLaunchRecords(ownedElectronLaunchRecords, branchOwnedElectronLaunchIds, electronLaunchRecords, branchResourceEvents.electronLaunchActiveRanks, branchResourceEvents.electronLaunchCleanupRanks);
537
552
  }
538
553
  if (restoredState.active) {
539
- trackOwnedManagedSession(ownedManagedSessions, restoredState.sessionName, ctx.cwd, { branchOwned: true });
554
+ trackOwnedManagedSession(ownedManagedSessions, restoredState.sessionName, ctx.cwd, { branchOwned: true, namespace: restoredState.namespace });
540
555
  }
541
556
  mergeActiveElectronLaunchRecords(ownedElectronLaunchRecords, electronLaunchRecords, {
542
557
  branchOwnedLaunchIds: branchOwnedElectronLaunchIds,
@@ -595,10 +610,11 @@ export default function agentBrowserExtension(pi) {
595
610
  await closeOwnedManagedSessions(ownedManagedSessions, implicitSessionCloseTimeoutMs);
596
611
  }
597
612
  else {
598
- await closeOwnedManagedSessionsExcept(ownedManagedSessions, managedSessionActive ? managedSessionName : undefined, implicitSessionCloseTimeoutMs);
613
+ await closeOwnedManagedSessionsExcept(ownedManagedSessions, managedSessionActive ? managedSessionName : undefined, implicitSessionCloseTimeoutMs, managedSessionActive ? managedSessionNamespace : undefined);
599
614
  }
600
615
  });
601
616
  managedSessionActive = false;
617
+ managedSessionNamespace = undefined;
602
618
  sessionPageState.reset();
603
619
  traceOwners = new Map();
604
620
  artifactManifest = undefined;
@@ -721,6 +737,7 @@ export default function agentBrowserExtension(pi) {
721
737
  clearSessionScopedBrowserState(closedSessionName);
722
738
  if (closedSessionName === managedSessionName) {
723
739
  managedSessionActive = false;
740
+ managedSessionNamespace = undefined;
724
741
  freshSessionOrdinal += 1;
725
742
  managedSessionName = createFreshSessionName(managedSessionBaseName, ephemeralSessionSeed, freshSessionOrdinal);
726
743
  }
@@ -735,7 +752,9 @@ export default function agentBrowserExtension(pi) {
735
752
  return applyAgentBrowserOutputPath({ cwd: ctx.cwd, outputPath, result: electronHostResult });
736
753
  }
737
754
  const explicitSessionName = extractExplicitSessionName(toolArgs);
755
+ const explicitNamespace = extractExplicitNamespace(toolArgs);
738
756
  const serializeBrowserCommand = shouldSerializeBrowserCommand({
757
+ explicitNamespace,
739
758
  explicitSessionName,
740
759
  managedSessionName,
741
760
  ownedElectronLaunchRecords,
@@ -756,6 +775,7 @@ export default function agentBrowserExtension(pi) {
756
775
  managedSessionBaseName,
757
776
  managedSessionCwd,
758
777
  managedSessionName,
778
+ managedSessionNamespace,
759
779
  networkRoutesBySession,
760
780
  sessionPageState,
761
781
  traceOwners,
@@ -784,6 +804,7 @@ export default function agentBrowserExtension(pi) {
784
804
  managedSessionActive = browserRunState.managedSessionActive;
785
805
  managedSessionCwd = browserRunState.managedSessionCwd;
786
806
  managedSessionName = browserRunState.managedSessionName;
807
+ managedSessionNamespace = browserRunState.managedSessionNamespace;
787
808
  for (const closedSessionName of browserRunState.closedManagedSessionNames) {
788
809
  untrackOwnedManagedSession(ownedManagedSessions, closedSessionName);
789
810
  }
@@ -3,7 +3,7 @@
3
3
  * Responsibilities: Own command-token extraction, command/subcommand identification, and descriptor construction.
4
4
  * Scope: Pure argv parsing; runtime planning and session policy consume descriptors instead of re-parsing tokens.
5
5
  */
6
- import { GLOBAL_BOOLEAN_FLAGS_WITH_OPTIONAL_VALUES, VALUE_FLAGS } from "./argv-grammar.js";
6
+ import { GLOBAL_BOOLEAN_FLAGS_WITH_OPTIONAL_VALUES, VALUE_FLAGS, optionalGlobalValueFlagConsumesNext } from "./argv-grammar.js";
7
7
  import { isOpenNavigationCommand } from "./command-taxonomy.js";
8
8
  function isBooleanLiteral(token) {
9
9
  const normalized = token?.trim().toLowerCase();
@@ -12,12 +12,15 @@ function isBooleanLiteral(token) {
12
12
  export function findCommandStartIndex(args) {
13
13
  for (let index = 0; index < args.length; index += 1) {
14
14
  const token = args[index];
15
- if (token.startsWith("--session=")) {
15
+ if (token.startsWith("--session=") || token.startsWith("--namespace=") || token.startsWith("--restore=")) {
16
16
  continue;
17
17
  }
18
18
  if (token.startsWith("-")) {
19
19
  const normalizedToken = token.split("=", 1)[0] ?? token;
20
- if (VALUE_FLAGS.has(normalizedToken) && !token.includes("=")) {
20
+ if (optionalGlobalValueFlagConsumesNext(normalizedToken, args[index + 1])) {
21
+ index += 1;
22
+ }
23
+ else if (VALUE_FLAGS.has(normalizedToken) && !token.includes("=")) {
21
24
  index += 1;
22
25
  }
23
26
  else if (GLOBAL_BOOLEAN_FLAGS_WITH_OPTIONAL_VALUES.has(normalizedToken) &&
@@ -3,12 +3,18 @@
3
3
  * Responsibilities: Own global/command value-flag sets and boolean/value-flag validation used during argv parsing.
4
4
  * Scope: Pure token grammar; command semantics and subprocess execution live elsewhere.
5
5
  */
6
+ import { isKnownCommandToken } from "./command-taxonomy.js";
6
7
  export const GLOBAL_VALUE_FLAGS = [
7
8
  "--session",
9
+ "--namespace",
8
10
  "--cdp",
9
11
  "--config",
10
12
  "--profile",
11
13
  "--session-name",
14
+ "--restore-save",
15
+ "--restore-check-url",
16
+ "--restore-check-text",
17
+ "--restore-check-fn",
12
18
  "--proxy",
13
19
  "--proxy-bypass",
14
20
  "--headers",
@@ -52,11 +58,13 @@ export const COMMAND_VALUE_FLAGS = [
52
58
  "--name",
53
59
  "--older-than",
54
60
  "--output",
61
+ "--prefix",
55
62
  "--path",
56
63
  "--port",
57
64
  "--resource-type",
58
65
  "--resource-types",
59
66
  "--sameSite",
67
+ "--scope",
60
68
  "--selector",
61
69
  "-s",
62
70
  "--status",
@@ -69,6 +77,7 @@ export const COMMAND_VALUE_FLAGS = [
69
77
  "--password",
70
78
  "--wait-until",
71
79
  ];
80
+ export const OPTIONAL_GLOBAL_VALUE_FLAGS = new Set(["--restore"]);
72
81
  export const VALUE_FLAGS = new Set([...GLOBAL_VALUE_FLAGS, ...COMMAND_VALUE_FLAGS]);
73
82
  export const PREVALIDATED_VALUE_FLAGS = new Set(GLOBAL_VALUE_FLAGS);
74
83
  export const GLOBAL_VALUE_FLAGS_ALLOWING_DASH_VALUE = new Set(["--args"]);
@@ -116,6 +125,25 @@ export function hasOnlyOptionFlags(tokens, allowedBooleanFlags, allowedValueFlag
116
125
  }
117
126
  return true;
118
127
  }
128
+ export function optionalGlobalValueFlagConsumesNext(flag, nextToken) {
129
+ if (!OPTIONAL_GLOBAL_VALUE_FLAGS.has(flag) || nextToken === undefined || nextToken.startsWith("-"))
130
+ return false;
131
+ return !isKnownCommandToken(nextToken);
132
+ }
119
133
  export function stripSessionlessShapeGlobalFlags(commandTokens) {
120
- return commandTokens.filter((token) => token !== "--json");
134
+ const stripped = [];
135
+ for (let index = 0; index < commandTokens.length; index += 1) {
136
+ const token = commandTokens[index];
137
+ const flagName = getFlagName(token);
138
+ if (token === "--json")
139
+ continue;
140
+ if ((flagName === "--session" || flagName === "--namespace") && !token.includes("=")) {
141
+ index += 1;
142
+ continue;
143
+ }
144
+ if (token.startsWith("--session=") || token.startsWith("--namespace="))
145
+ continue;
146
+ stripped.push(token);
147
+ }
148
+ return stripped;
121
149
  }
@@ -16,6 +16,7 @@ const DOCTOR_BOOLEAN_FLAGS = new Set(["--fix", "--json", "--offline", "--quick"]
16
16
  const INSTALL_BOOLEAN_FLAGS = new Set(["--with-deps", "-d"]);
17
17
  const STATE_SESSIONLESS_SUBCOMMANDS = new Set(["list", "show", "clear", "clean", "rename"]);
18
18
  const STATE_CLEAN_VALUE_FLAGS = new Set(["--older-than"]);
19
+ const SESSION_ID_VALUE_FLAGS = new Set(["--scope", "--prefix"]);
19
20
  function isSessionlessAuthCommand(commandTokens) {
20
21
  const [, subcommand, target, ...rest] = commandTokens;
21
22
  if (!SESSIONLESS_AUTH_SUBCOMMANDS.has(subcommand ?? ""))
@@ -64,6 +65,14 @@ function isSessionlessPluginCommand(commandTokens) {
64
65
  return true;
65
66
  return PLUGIN_SESSIONLESS_SUBCOMMANDS.has(subcommand);
66
67
  }
68
+ function isSessionlessSessionCommand(commandTokens) {
69
+ const [, subcommand, ...rest] = commandTokens;
70
+ if (subcommand === "list" || subcommand === "info")
71
+ return rest.length === 0;
72
+ if (subcommand === "id")
73
+ return hasOnlyOptionFlags(rest, JSON_BOOLEAN_FLAGS, SESSION_ID_VALUE_FLAGS);
74
+ return false;
75
+ }
67
76
  function isSessionlessCommand(commandTokens) {
68
77
  const normalizedTokens = stripSessionlessShapeGlobalFlags(commandTokens);
69
78
  const [command, subcommand] = normalizedTokens;
@@ -86,7 +95,7 @@ function isSessionlessCommand(commandTokens) {
86
95
  if (command === "profiles" || command === "upgrade")
87
96
  return normalizedTokens.length === 1;
88
97
  if (command === "session")
89
- return normalizedTokens.length === 2 && subcommand === "list";
98
+ return isSessionlessSessionCommand(normalizedTokens);
90
99
  if (command === "state")
91
100
  return isSessionlessStateCommand(normalizedTokens);
92
101
  return false;
@@ -4,6 +4,9 @@
4
4
  * session/ref guards, result recommendations, and presentation summaries without coupling unrelated behaviors.
5
5
  * Scope: Static command capability taxonomy only; command-shape parsing, spawning, and formatting live elsewhere.
6
6
  */
7
+ const ADDITIONAL_COMMAND_TOKENS = [
8
+ "auth", "chat", "clipboard", "confirm", "connect", "dashboard", "deny", "device", "dialog", "diff", "doctor", "errors", "eval", "find", "frame", "get", "highlight", "inspect", "install", "is", "mcp", "plugin", "plugins", "profiles", "profiler", "react", "read", "record", "removeinitscript", "session", "set", "skills", "snapshot", "state", "stream", "trace", "upgrade", "vitals", "wait", "web-vitals", "window",
9
+ ];
7
10
  const COMMAND_CAPABILITIES = [
8
11
  {
9
12
  command: "back",
@@ -258,6 +261,10 @@ for (const entry of COMMAND_CAPABILITIES) {
258
261
  COMMAND_CAPABILITY_BY_NAME.set(alias, entry);
259
262
  }
260
263
  }
264
+ const KNOWN_COMMAND_TOKENS = new Set([...COMMAND_CAPABILITY_BY_NAME.keys(), ...ADDITIONAL_COMMAND_TOKENS]);
265
+ export function isKnownCommandToken(token) {
266
+ return KNOWN_COMMAND_TOKENS.has(token);
267
+ }
261
268
  function getCommandCapability(command) {
262
269
  return command === undefined ? undefined : COMMAND_CAPABILITY_BY_NAME.get(command);
263
270
  }
@@ -68,28 +68,6 @@ const JOB_STEP_ALLOWED_FIELDS = {
68
68
  wait: new Set(["action", "milliseconds"]),
69
69
  waitForDownload: new Set(["action", "path"]),
70
70
  };
71
- function globUrlPatternToRegexSource(pattern) {
72
- let source = "^";
73
- for (let index = 0; index < pattern.length; index += 1) {
74
- const char = pattern.charAt(index);
75
- if (char === "*") {
76
- let runLength = 1;
77
- while (pattern[index + runLength] === "*")
78
- runLength += 1;
79
- source += runLength === 1 ? "[^/]*" : ".*";
80
- index += runLength - 1;
81
- }
82
- else {
83
- source += char.replace(/[\\^$+?.()|[\]{}]/g, "\\$&");
84
- }
85
- }
86
- return `${source}$`;
87
- }
88
- function compileJobAssertUrlArgs(url) {
89
- if (!url.includes("*"))
90
- return ["wait", "--url", url];
91
- return ["wait", "--fn", `new RegExp(${JSON.stringify(globUrlPatternToRegexSource(url))}).test(location.href)`];
92
- }
93
71
  function compileJobTypeSteps(step) {
94
72
  const text = getRequiredJobString(step, "text", "type");
95
73
  if (text.error)
@@ -182,7 +160,7 @@ function compileAssertUrlJobStep(step) {
182
160
  const result = getRequiredJobString(step, "url", "assertUrl");
183
161
  if (result.error)
184
162
  return { error: result.error };
185
- return { args: compileJobAssertUrlArgs(result.value) };
163
+ return { args: ["wait", "--url", result.value] };
186
164
  }
187
165
  function compilePathArtifactJobStep(step, action) {
188
166
  const result = getRequiredJobString(step, "path", action);
@@ -259,12 +259,15 @@ export function compileAgentBrowserNetworkSourceLookup(input) {
259
259
  return { error: "networkSourceLookup must be an object." };
260
260
  const filter = input.filter;
261
261
  const requestId = input.requestId;
262
+ const namespace = input.namespace;
262
263
  const session = input.session;
263
264
  const url = input.url;
264
265
  if (filter !== undefined && (typeof filter !== "string" || filter.trim().length === 0))
265
266
  return { error: "networkSourceLookup.filter must be a non-empty string when provided." };
266
267
  if (requestId !== undefined && (typeof requestId !== "string" || requestId.trim().length === 0))
267
268
  return { error: "networkSourceLookup.requestId must be a non-empty string when provided." };
269
+ if (namespace !== undefined && (typeof namespace !== "string" || namespace.trim().length === 0))
270
+ return { error: "networkSourceLookup.namespace must be a non-empty string when provided." };
268
271
  if (session !== undefined && (typeof session !== "string" || session.trim().length === 0))
269
272
  return { error: "networkSourceLookup.session must be a non-empty string when provided." };
270
273
  if (url !== undefined && (typeof url !== "string" || url.trim().length === 0))
@@ -282,8 +285,8 @@ export function compileAgentBrowserNetworkSourceLookup(input) {
282
285
  if (effectiveFilter) {
283
286
  steps.push({ action: "network", args: ["network", "requests", "--filter", effectiveFilter] });
284
287
  }
285
- const args = typeof session === "string" ? ["--session", session, "batch"] : ["batch"];
286
- return { compiled: { args, query: { filter, maxWorkspaceFiles: maxWorkspaceFiles.value, requestId, session, url }, stdin: JSON.stringify(steps.map((step) => step.args)), steps } };
288
+ const args = [...(typeof namespace === "string" ? ["--namespace", namespace] : []), ...(typeof session === "string" ? ["--session", session] : []), "batch"];
289
+ return { compiled: { args, query: { filter, maxWorkspaceFiles: maxWorkspaceFiles.value, namespace, requestId, session, url }, stdin: JSON.stringify(steps.map((step) => step.args)), steps } };
287
290
  }
288
291
  function getResultPayload(item) {
289
292
  return isRecord(item.result) && "data" in item.result ? item.result.data : item.result;