pi-agent-browser-native 0.2.70 → 0.2.72

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 CHANGED
@@ -1,5 +1,42 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.72 - 2026-07-23
4
+
5
+ ### Changed
6
+
7
+ - Rebaselined the command/help inventory, source evidence, prompt guidance, and package docs to `agent-browser 0.33.0` / vercel-labs/agent-browser@1ed371f3af472cc0d6cd8fdaea75d1a085ff7534 (includes 0.32.3–0.32.4 HAR/`find`/`derive-client` surfaces).
8
+ - Documented HAR response-body capture modes (`network har start --content text|all|none`), `skills get derive-client`, and the new `a11y [url]` axe-core accessibility audit (`--tags`, `--selector`).
9
+ - Documented upstream 0.32.4 `find role` implicit ARIA / accessible-name matching, locator-detail miss text, and the aligned `find` action list (`click, fill, check, hover, text`).
10
+ - Added compact model-facing presentation for `a11y` violation/incomplete summaries.
11
+
12
+ ### Fixed
13
+
14
+ - Classified upstream 0.32.4+ locator-detail misses (`Names seen:`, `No element found: getByRole(...)`, `Element not found: … Verify the selector, role, or name`) as `failureCategory: "selector-not-found"` so snapshot-ref recovery still runs, without treating bare accessible-name text containing `timeout` or `Confirmation required` as unrelated categories.
15
+ - Treated command-scoped `--content` and `--tags` as value-taking flags during argv planning so `network har start --content all` and `a11y --tags wcag2a,wcag2aa` keep mode/tag tokens with their flags.
16
+
17
+ ### Validation
18
+
19
+ - Passed `npm run verify` (590 tests passed, 2 opt-in skips), live command-reference verification, and `npm run verify -- real-upstream` (2/2 tests) against installed `agent-browser 0.33.0`.
20
+ - Passed `npm run verify -- release`, including configured-source lifecycle, packaged Pi smoke, and macOS/Ubuntu/native-Windows Crabbox `platform-build` plus `browser-dogfood-smoke` on `agent-browser 0.33.0` (Windows snapshot `crabbox-ready-ab-0.33.0`, Ubuntu image `node24-agent-browser0.33.0`).
21
+
22
+ ## 0.2.71 - 2026-07-18
23
+
24
+ ### Fixed
25
+
26
+ - Applied the same managed-session `AGENT_BROWSER_IDLE_TIMEOUT_MS` to top-level commands and every wrapper helper subprocess. With upstream 0.32.2, missing the value on hidden snapshots, tab lists, navigation summaries, and diagnostics could restart the background browser, replace the active page with `about:blank`, and make a freshly captured `@ref` fail on the next click or select.
27
+ - Refreshed the remaining active tab URL/title after `tab close` so subsequent snapshots, ref guards, and interactions no longer inherit the closed tab's target.
28
+ - Made ordinary document `scroll <direction> [amount]` deterministic before upstream wheel fallback, including pages such as Artificial Analysis whose smooth-scroll CSS previously left large scroll commands at offset zero.
29
+
30
+ ### Changed
31
+
32
+ - Extended the real-upstream contract with snapshot-ref native selection and stable-id/label tab lifecycle coverage, and corrected fake/docs tab examples from unsupported positional `tab 0` to stable `tab t1`.
33
+
34
+ ### Validation
35
+
36
+ - Passed `npm run verify` (590 tests passed, 2 opt-in skips), live command-reference verification, the expanded 2/2 real-upstream contract, deterministic dogfood, benchmark, packaged Pi smoke, and three-sample startup profiling (50.1 ms maximum against the 250 ms budget).
37
+ - Passed isolated checkout-loaded Pi dogfood on Artificial Analysis, React, GitHub, and a deterministic select/tab fixture. Artificial Analysis document scroll moved from offset 0 to 700; snapshot-ref select/click, stable-id/label tab switching, post-close target refresh, and post-close interaction completed with no background restarts, `about:blank` resets, or spurious stale-ref failures.
38
+ - Passed `npm run verify -- release`, including configured-source reload/relaunch lifecycle, packaged Pi smoke, and macOS/Ubuntu/native-Windows Crabbox `platform-build` plus `browser-dogfood-smoke`; all provider leases and browser sessions were cleaned.
39
+
3
40
  ## 0.2.70 - 2026-07-18
4
41
 
5
42
  ### Changed
package/README.md CHANGED
@@ -74,13 +74,13 @@ The result is optimized for agent work:
74
74
  | Agents build fragile shell commands | Exposes `agent_browser` with exact `args`, an optional `semanticAction` shorthand for common `find` flows and native `select`, constrained `job` / `qa` presets, experimental `sourceLookup` / `networkSourceLookup` that compile short workflows to `batch`, top-level `electron` for desktop lifecycle, plus controlled `stdin` and `sessionMode` | `extensions/agent-browser/index.ts`, `extensions/agent-browser/lib/input-modes/`, [`docs/TOOL_CONTRACT.md`](docs/TOOL_CONTRACT.md) |
75
75
  | Page snapshots are too large or viewport-blind | Shows compact, main-content-first summaries, surfaces an `Omitted high-value controls` section (plus `details.data.highValueControlRefIds`) when dense pages or desktop host screens hide editables, named surfaces/tabs, primary action buttons, and high-signal named links such as repository results from the trimmed ref lists, supports wrapper-side `snapshot -i --search <text>` / `--filter role=<role>` to trim dense pages while preserving full `details.refSnapshot`, supports `snapshot --viewport` for scroll/viewport metadata, supports `snapshot --diff` for quick ref-map deltas versus the prior tracked snapshot, and stores full raw output in spill files when needed | `extensions/agent-browser/lib/results/snapshot.ts`, `extensions/agent-browser/lib/orchestration/browser-run/prepare.ts`, `test/agent-browser.presentation.test.ts`, `test/agent-browser.extension-validation.test.ts` |
76
76
  | Screenshots/downloads get lost in text | Normalizes artifact paths, creates missing parent directories, saves simple loopback anchor downloads to the requested path when possible, and reports existence, size, cwd, session, and repair status | [`docs/COMMAND_REFERENCE.md`](docs/COMMAND_REFERENCE.md#download-screenshot-and-pdf-files) |
77
- | Profile restores and tab drift confuse agents | Tracks managed sessions, re-selects target tabs after observed drift, rehydrates branch-backed session state on Pi session-tree changes, and pins later commands only for sessions with drift/restored-session risk | generated tab-recovery notes below; `test/agent-browser.extension-tab-recovery.test.ts` (drift and about:blank recovery), `test/agent-browser.resume-state.test.ts` (persisted session / resume planning), `test/agent-browser.extension-ref-guards.test.ts` (session_tree rehydration) |
77
+ | Profile restores and tab drift confuse agents | Tracks managed sessions, keeps every upstream helper probe on the same idle-timeout launch configuration so the background browser is not restarted between a snapshot and action, re-selects target tabs after observed drift, refreshes the active target after `tab close`, rehydrates branch-backed session state on Pi session-tree changes, and pins later commands only for sessions with drift/restored-session risk | generated tab-recovery notes below; `test/agent-browser.extension-tab-recovery.test.ts` (drift and about:blank recovery), `test/agent-browser.extension-tabs.test.ts` (post-close target), `test/agent-browser.extension-ref-guards.test.ts` (snapshot/action environment and session-tree rehydration), `test/agent-browser.resume-state.test.ts` (persisted session / resume planning) |
78
78
  | Auth/profile workflows can leak secrets | Supports `auth save --password-stdin` and redacts sensitive args, URLs, stdout/stderr, details, and parse-failure spills | `test/agent-browser.extension-security-redaction.test.ts` |
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
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` |
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 produce `details.scrollNoop`, visible recovery guidance, and exact snapshot/screenshot checks | [`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` |
@@ -45,6 +45,7 @@ export const COMMAND_VALUE_FLAGS = [
45
45
  "--baseline",
46
46
  "--body",
47
47
  "--categories",
48
+ "--content",
48
49
  "--curl",
49
50
  "--depth",
50
51
  "-d",
@@ -68,6 +69,7 @@ export const COMMAND_VALUE_FLAGS = [
68
69
  "--selector",
69
70
  "-s",
70
71
  "--status",
72
+ "--tags",
71
73
  "--text",
72
74
  "--threshold",
73
75
  "--timeout",
@@ -5,7 +5,7 @@
5
5
  * Scope: Static command capability taxonomy only; command-shape parsing, spawning, and formatting live elsewhere.
6
6
  */
7
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", "record", "removeinitscript", "session", "set", "skills", "snapshot", "state", "stream", "trace", "upgrade", "vitals", "wait", "web-vitals", "window",
8
+ "a11y", "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", "record", "removeinitscript", "session", "set", "skills", "snapshot", "state", "stream", "trace", "upgrade", "vitals", "wait", "web-vitals", "window",
9
9
  ];
10
10
  const COMMAND_CAPABILITIES = [
11
11
  {
@@ -36,6 +36,10 @@ export const LAUNCH_SCOPED_FLAG_DEFINITIONS = [
36
36
  flag: "--init-script",
37
37
  reason: "registers page init scripts before the upstream browser session is launched",
38
38
  },
39
+ {
40
+ flag: "--idle-timeout",
41
+ reason: "configures background browser lifecycle for the launched session",
42
+ },
39
43
  {
40
44
  flag: "--device",
41
45
  reason: "selects the provider device for the upstream launch",
@@ -53,6 +53,7 @@ function buildScrollResult(options) {
53
53
  compatibilityWorkaround: options.compatibilityWorkaround,
54
54
  data: options.result,
55
55
  effectiveArgs: options.effectiveArgs,
56
+ exitCode: options.succeeded ? 0 : 1,
56
57
  nextActions: options.succeeded ? undefined : applyNamespaceToNextActions(buildScrollNoopNextActions(options.sessionName), options.namespace),
57
58
  [options.scrollField]: options.scrollValue,
58
59
  sessionMode: options.sessionMode,
@@ -79,24 +80,52 @@ export async function tryContainerScroll(options) {
79
80
  return buildScrollResult({ ...options, command: "scroll", message, result, scrollField: "scrollContainer", scrollValue: { request, result }, succeeded });
80
81
  }
81
82
  function getPageScrollToRequest(commandTokens) {
82
- if (commandTokens[0] !== "scroll" || commandTokens[1]?.toLowerCase() !== "to")
83
+ if (commandTokens[0] !== "scroll")
83
84
  return undefined;
84
- const target = commandTokens[2]?.toLowerCase();
85
- return target === "end" || target === "top" ? { target } : undefined;
85
+ if (commandTokens[1]?.toLowerCase() === "to") {
86
+ const target = commandTokens[2]?.toLowerCase();
87
+ return target === "end" || target === "top" ? { target } : undefined;
88
+ }
89
+ const direction = commandTokens[1]?.toLowerCase();
90
+ if (!SCROLL_CONTAINER_DIRECTIONS.has(direction) || commandTokens.length > 3)
91
+ return undefined;
92
+ const amount = commandTokens[2];
93
+ if (amount && (!/^\d+(?:\.\d+)?(?:px|%)?$/.test(amount) || Number(amount.replace(/(?:px|%)$/, "")) <= 0))
94
+ return undefined;
95
+ return { amount, direction: direction };
86
96
  }
87
97
  function buildPageScrollToScript(request) {
88
98
  return `(() => {
89
- const target = ${JSON.stringify(request.target)};
99
+ const target = ${JSON.stringify("target" in request ? request.target : undefined)};
100
+ const direction = ${JSON.stringify("direction" in request ? request.direction : undefined)};
101
+ const amountToken = ${JSON.stringify("amount" in request ? request.amount ?? "" : "")};
102
+ const request = target ? { target } : { direction, amount: amountToken || undefined };
90
103
  const scroller = document.scrollingElement || document.documentElement || document.body;
91
- if (!scroller) return { status: "no-scroller", target };
104
+ if (!scroller) return { status: "no-scroller", ...request };
92
105
  const before = { scrollLeft: scroller.scrollLeft, scrollTop: scroller.scrollTop, scrollHeight: scroller.scrollHeight, scrollWidth: scroller.scrollWidth, clientHeight: scroller.clientHeight, clientWidth: scroller.clientWidth };
93
- const nextTop = target === "top" ? 0 : Math.max(0, scroller.scrollHeight - scroller.clientHeight);
94
- const nextLeft = scroller.scrollLeft;
95
- scroller.scrollTop = nextTop;
106
+ const axis = direction === "left" || direction === "right" ? "x" : "y";
107
+ const extent = axis === "x" ? scroller.clientWidth : scroller.clientHeight;
108
+ const parseAmount = () => {
109
+ if (!amountToken) return Math.max(1, Math.floor(extent * 0.8));
110
+ if (amountToken.endsWith("%")) {
111
+ const value = Number(amountToken.slice(0, -1));
112
+ return Number.isFinite(value) ? Math.max(1, Math.floor(extent * value / 100)) : Math.max(1, Math.floor(extent * 0.8));
113
+ }
114
+ const pixels = Number(amountToken.replace(/px$/, ""));
115
+ return Number.isFinite(pixels) && pixels > 0 ? Math.floor(pixels) : Math.max(1, Math.floor(extent * 0.8));
116
+ };
117
+ const delta = parseAmount() * (direction === "up" || direction === "left" ? -1 : 1);
118
+ const nextTop = target === "top" ? 0 : target === "end" ? Math.max(0, scroller.scrollHeight - scroller.clientHeight) : axis === "y" ? scroller.scrollTop + delta : scroller.scrollTop;
119
+ const nextLeft = axis === "x" ? scroller.scrollLeft + delta : scroller.scrollLeft;
120
+ const priorBehavior = scroller.style.scrollBehavior;
121
+ scroller.style.scrollBehavior = "auto";
96
122
  window.scrollTo(nextLeft, nextTop);
123
+ scroller.scrollLeft = nextLeft;
124
+ scroller.scrollTop = nextTop;
97
125
  const after = { scrollLeft: scroller.scrollLeft, scrollTop: scroller.scrollTop, scrollHeight: scroller.scrollHeight, scrollWidth: scroller.scrollWidth, clientHeight: scroller.clientHeight, clientWidth: scroller.clientWidth };
126
+ scroller.style.scrollBehavior = priorBehavior;
98
127
  const moved = before.scrollLeft !== after.scrollLeft || before.scrollTop !== after.scrollTop;
99
- return { status: moved ? "scrolled" : "no-movement", target, before, after };
128
+ return { status: moved ? "scrolled" : "no-movement", ...request, before, after };
100
129
  })()`;
101
130
  }
102
131
  export async function tryPageScrollTo(options) {
@@ -108,6 +137,9 @@ export async function tryPageScrollTo(options) {
108
137
  if (!isRecord(result) || typeof result.status !== "string")
109
138
  return undefined;
110
139
  const succeeded = result.status === "scrolled";
111
- const message = succeeded ? `Scrolled page to ${request.target}.` : `Scroll to ${request.target} completed with no observed movement (${result.status}).`;
140
+ if (!succeeded && "direction" in request)
141
+ return undefined;
142
+ const description = "target" in request ? `to ${request.target}` : `${request.direction}${request.amount ? ` by ${request.amount}` : ""}`;
143
+ const message = succeeded ? `Scrolled page ${description}.` : `Scroll ${description} completed with no observed movement (${result.status}).`;
112
144
  return buildScrollResult({ ...options, command: "scroll", message, result, scrollField: "scrollPage", scrollValue: { request, result }, succeeded });
113
145
  }
@@ -253,6 +253,18 @@ async function collectSamePageRefFreshnessPreflight(options) {
253
253
  return undefined;
254
254
  return { message: mismatch.message, refIds: mismatch.refIds, snapshot: snapshotWithTarget };
255
255
  }
256
+ function getIdleTimeoutMismatch(args, configuredValue) {
257
+ for (let index = 0; index < args.length; index += 1) {
258
+ const token = args[index];
259
+ if (token !== "--idle-timeout" && !token.startsWith("--idle-timeout="))
260
+ continue;
261
+ const requestedToken = token.includes("=") ? token.slice(token.indexOf("=") + 1) : args[++index];
262
+ if (!requestedToken || !/^\d+$/.test(requestedToken) || Number(requestedToken) === Number(configuredValue))
263
+ continue;
264
+ return `--idle-timeout ${requestedToken} conflicts with this Pi process's managed-session idle timeout (${configuredValue} ms). Restart Pi with PI_AGENT_BROWSER_IMPLICIT_SESSION_IDLE_TIMEOUT_MS=${requestedToken} and omit --idle-timeout; changing the launch value for one call can restart the upstream browser and discard the active tab.`;
265
+ }
266
+ return undefined;
267
+ }
256
268
  function isPasswordStdinAuthSave(options) {
257
269
  return options.command === "auth" && options.commandTokens[1] === "save" && options.commandTokens.includes("--password-stdin");
258
270
  }
@@ -340,6 +352,9 @@ export async function prepareBrowserRun(options) {
340
352
  managedSessionNamespace: state.managedSessionNamespace,
341
353
  sessionMode,
342
354
  });
355
+ const idleTimeoutMismatch = getIdleTimeoutMismatch(preparedArgs.args, options.implicitSessionIdleTimeoutMs);
356
+ if (idleTimeoutMismatch)
357
+ executionPlan = { ...executionPlan, recoveryHint: undefined, validationError: idleTimeoutMismatch };
343
358
  const sessionStateKey = getSessionContextKey(executionPlan.sessionName, executionPlan.namespace);
344
359
  const priorSessionPageState = sessionPageState.get(sessionStateKey);
345
360
  const priorSessionTabTarget = priorSessionPageState.tabTarget;
@@ -581,34 +596,36 @@ export async function prepareBrowserRun(options) {
581
596
  });
582
597
  if (networkRequestsPageFilter)
583
598
  return { kind: "early-result", statePatch, result: networkRequestsPageFilter };
584
- const containerScroll = await tryContainerScroll({
585
- commandTokens,
586
- compatibilityWorkaround,
587
- cwd,
588
- effectiveArgs: redactedEffectiveArgs,
589
- redactedArgs,
590
- sessionMode,
591
- namespace: executionPlan.namespace,
592
- sessionName: executionPlan.sessionName,
593
- signal,
594
- usedImplicitSession: executionPlan.usedImplicitSession,
595
- });
596
- if (containerScroll)
597
- return { kind: "early-result", statePatch, result: containerScroll };
598
- const pageScrollTo = await tryPageScrollTo({
599
- commandTokens,
600
- compatibilityWorkaround,
601
- cwd,
602
- effectiveArgs: redactedEffectiveArgs,
603
- redactedArgs,
604
- sessionMode,
605
- namespace: executionPlan.namespace,
606
- sessionName: executionPlan.sessionName,
607
- signal,
608
- usedImplicitSession: executionPlan.usedImplicitSession,
609
- });
610
- if (pageScrollTo)
611
- return { kind: "early-result", statePatch, result: pageScrollTo };
599
+ if (executionPlan.startupScopedFlags.length === 0) {
600
+ const containerScroll = await tryContainerScroll({
601
+ commandTokens,
602
+ compatibilityWorkaround,
603
+ cwd,
604
+ effectiveArgs: redactedEffectiveArgs,
605
+ redactedArgs,
606
+ sessionMode,
607
+ namespace: executionPlan.namespace,
608
+ sessionName: executionPlan.sessionName,
609
+ signal,
610
+ usedImplicitSession: executionPlan.usedImplicitSession,
611
+ });
612
+ if (containerScroll)
613
+ return { kind: "early-result", statePatch, result: containerScroll };
614
+ const pageScrollTo = await tryPageScrollTo({
615
+ commandTokens,
616
+ compatibilityWorkaround,
617
+ cwd,
618
+ effectiveArgs: redactedEffectiveArgs,
619
+ redactedArgs,
620
+ sessionMode,
621
+ namespace: executionPlan.namespace,
622
+ sessionName: executionPlan.sessionName,
623
+ signal,
624
+ usedImplicitSession: executionPlan.usedImplicitSession,
625
+ });
626
+ if (pageScrollTo)
627
+ return { kind: "early-result", statePatch, result: pageScrollTo };
628
+ }
612
629
  const directAnchorDownload = await tryDirectAnchorDownload({
613
630
  artifactManifest: state.artifactManifest,
614
631
  commandTokens,
@@ -188,7 +188,8 @@ export async function processBrowserOutput(input) {
188
188
  !navigationSummary &&
189
189
  (shouldCaptureNavigationSummary(prepared.executionPlan.commandInfo.command, presentationEnvelope?.data) ||
190
190
  shouldCaptureSemanticActionNavigationSummary(prepared.compiledSemanticAction, presentationEnvelope?.data) ||
191
- shouldCaptureAllowedDomainNavigationSummary)) {
191
+ shouldCaptureAllowedDomainNavigationSummary ||
192
+ (prepared.executionPlan.commandInfo.command === "tab" && prepared.executionPlan.commandInfo.subcommand === "close"))) {
192
193
  navigationSummary = await collectNavigationSummary({ cwd, namespace: prepared.executionPlan.namespace, sessionName: prepared.executionPlan.sessionName, signal });
193
194
  }
194
195
  if (navigationSummary && presentationEnvelope && !Array.isArray(presentationEnvelope.data))
@@ -20,7 +20,7 @@ export const QUICK_START_GUIDELINES = [
20
20
  "Locator-first clicks/fills and native select changes without hand-building argv: { semanticAction: { action: \"click\", locator: \"text\", value: \"Close\" } }, { semanticAction: { action: \"fill\", locator: \"label\", value: \"Email\", text: \"user@example.com\" } }, direct current targets such as { semanticAction: { action: \"fill\", selector: \"@e1\", text: \"prompt\" } }, or { semanticAction: { action: \"select\", selector: \"#flavor\", value: \"chocolate\" } }; add semanticAction.session when targeting a named upstream browser session; details.compiledSemanticAction shows the semantic target, while details.effectiveArgs may show a resolved current @ref for active-session role/name click/check/fill actions to avoid hidden duplicate matches; semanticAction does not expose uncheck while upstream find ... uncheck is not runtime-supported, so use raw uncheck with a stable selector or current ref; selector-not-found failures may append bounded click try-*-candidate next actions or, for fill misses with current editable refs, details.richInputRecovery with focus/click actions that do not copy fill text; stale-ref failures can return retry-semantic-action-after-stale-ref for compiled find actions when retry safety is provable.",
21
21
  `Common advanced calls: { args: ["batch"], stdin: "[[\"open\",\"https://example.com\"],[\"snapshot\",\"-i\"]]" }, { job: { steps: [{ action: "open", url: "https://example.com" }, { action: "assertText", text: "Example Domain" }, { action: "screenshot", path: ".dogfood/example.png" }] } }, { qa: { url: "https://example.com", expectedText: "Example Domain", screenshotPath: ".dogfood/qa-example.png" } } (example.com smoke only; elsewhere match exact visible text from snapshot -i), { electron: { action: "list", query: "code" } }, { electron: { action: "launch", appName: "Visual Studio Code", handoff: "snapshot" } }, { electron: { action: "probe" } }, { qa: { attached: true, expectedText: "Explorer" } }, { args: ["eval", "--stdin"], stdin: "document.title", outputPath: "logs/page-title.json" }, { args: ["auth", "save", "name", "--password-stdin"], stdin: "<password from user-approved secret source>" }, { args: ["--profile", "Default", "open", "https://example.com/account"], sessionMode: "fresh" }, and { args: ["open", "--enable", "react-devtools", "https://example.com"], sessionMode: "fresh" }. For app pages with a native dropdown, job steps can include { action: "select", selector: "#flavor", value: "chocolate" } before the dependent assertion; for locator-friendly pages, job click/fill steps can use semantic locator fields such as { action: "fill", locator: "role", role: "searchbox", name: "Search", text: "agent browser" }; for human-paced input, job type steps can use { action: "type", selector: "#prompt", text: "hello", delayMs: 20, press: "Enter" }; delayed typing is capped at 200 characters per step, and generated per-character rows are compacted in visible batch prose while full rows remain in details.batchSteps.`,
22
22
  "Constrained job navigation is explicit only: click (and select/submit flows that may navigate) does not prove the next page loaded; add assertUrl and/or assertText after navigation-prone steps before screenshot or later interactions. Keep jobs short around navigation, click, and rerender boundaries on dynamic React/product apps; avoid a whole checkout in one job. If a long job times out and details.timeoutPartialProgress shows a mutating incomplete step, inspect current page state and continue with a shorter job or single action instead of blindly retrying the mutating step. Example: { job: { steps: [{ action: \"open\", url: \"https://shop.example/checkout\" }, { action: \"fill\", selector: \"#email\", text: \"user@example.com\" }, { action: \"click\", selector: \"#continue\" }, { action: \"assertUrl\", url: \"**/shipping\" }, { action: \"assertText\", text: \"Shipping address\" }, { action: \"screenshot\", path: \".dogfood/shipping.png\" }] } }. Top-level click may add pageChangeSummary hints, but job never auto-inserts post-click asserts.",
23
- "High-value command reference: click <selector> --new-tab opens link-like targets in a new tab; select <selector> <value...> changes native dropdown values; scroll <dir> [px] --selector <sel>, wrapper-handled scroll <selector> <dir> [px|percent] targets nested scrollers, and wrapper-handled scroll to end/top targets document scrolling; download <selector> <path> saves a file triggered by a click; read [url] returns agent-readable text (explicit URLs prefer markdown without launching Chrome; omit the URL for rendered active-tab DOM); get title/url need no selector; get text/html/value/count <selector> and get attr <selector> <name> read elements/page state (use body for whole-page text/html); screenshot [selector] [path] captures a page or element image; pdf <path> saves a PDF; tab list and tab <tab-id-or-label> inspect or recover the active tab; react tree, react inspect <fiberId>, react renders start/stop, and react suspense introspect React after --enable react-devtools; vitals [url] measures Core Web Vitals; pushstate <url> performs SPA navigation; tap <selector> and swipe <direction> [distance] support iOS/provider touch flows.",
23
+ "High-value command reference: click <selector> --new-tab opens link-like targets in a new tab; select <selector> <value...> changes native dropdown values; wrapper-handled scroll <dir> [px|percent] and scroll to end/top target document scrolling before upstream fallback, while scroll <selector> <dir> [px|percent] targets nested scrollers; download <selector> <path> saves a file triggered by a click; read [url] returns agent-readable text (explicit URLs prefer markdown without launching Chrome; omit the URL for rendered active-tab DOM); get title/url need no selector; get text/html/value/count <selector> and get attr <selector> <name> read elements/page state (use body for whole-page text/html); screenshot [selector] [path] captures a page or element image; pdf <path> saves a PDF; tab list and tab <tab-id-or-label> inspect or recover the active tab; react tree, react inspect <fiberId>, react renders start/stop, and react suspense introspect React after --enable react-devtools; vitals [url] measures Core Web Vitals; pushstate <url> performs SPA navigation; tap <selector> and swipe <direction> [distance] support iOS/provider touch flows.",
24
24
  "For artifact-producing commands, read the visible artifact block and details.artifactVerification before using files: check requested path, absolute path, existence, size bytes, artifact kind, optional mediaType, status, optional limitation, and verified/missing/pending/unverified counts. details.artifacts contains per-file metadata; record start rows are pending/openRecording until record stop writes the target. The wrapper creates parent directories for direct artifact paths and can save simple loopback HTTP(S) anchor downloads directly to the requested path before upstream download fallback. Browser close does not delete explicit saved files; if close reports details.artifactCleanup, use host file tools to remove paths listed in explicitArtifactPaths (when non-empty) after inspection. If close fails with details.promptGuard.reason=requested-artifacts-missing-before-close, save the exact required artifact path before closing. For annotated screenshots inside batch, put --annotate in top-level args (for example { args: [\"--annotate\", \"batch\"], stdin: \"[[\\\"screenshot\\\",\\\"/tmp/page.png\\\"]]\" }) rather than inside the screenshot step; if annotation labels crowd a dense page, use a scoped or non-annotated screenshot plus snapshot refs instead.",
25
25
  "When details.nextActions is present, prefer those exact native agent_browser follow-up payloads over prose guidance; they may include args, stdin, sessionMode, networkSourceLookup, safety notes, or artifactPath for saved files.",
26
26
  ];
@@ -46,7 +46,7 @@ export const SHARED_BROWSER_PLAYBOOK_GUIDELINES = [
46
46
  "For batch chains that touch cookies, storage, auth, or other secret-bearing commands, use details.batchSteps for per-step artifacts, categories, spill paths, and full structured errors; top-level details.data on batch is only a compact redacted step matrix (success, argv-redacted command, redacted result or scrubbed error text) built from the same presentation rules as standalone calls.",
47
47
  "For non-core families, pass current upstream commands through the native tool directly: network requests, network route <url>, network har start/stop [path], diff snapshot, diff screenshot --baseline <file>, diff url <u1> <u2>, trace start, trace stop [path], profiler start, profiler stop [path], record start <path>, record stop, console/errors [--clear], highlight <selector>, inspect, clipboard read, clipboard write <text>, clipboard copy/paste, stream enable/disable/status, dashboard start/stop, device list for iOS simulator inventory, and chat <message>. For compact network requests output, prefer details.nextActions for request detail, route-mock diagnostics, actionable failed-request networkSourceLookup, filtering, clearing the aggregate buffer before repro, or HAR capture follow-ups instead of guessing request-id syntax. Artifact-producing commands report details.artifacts and verification state; long-running starts such as stream, dashboard, trace/profiler, and record should be paired with the matching stop/disable command when the task is done; stream enable already-enabled outcomes are treated as idempotent success with status/disable follow-ups.",
48
48
  "For Electron desktop apps, prefer top-level electron for wrapper-owned discovery, isolated launch, status, compact probe, and cleanup: list first, treat likely-sensitive annotations as hints rather than enforcement, launch with the default snapshot handoff unless handoff: \"tabs\" is the safer diagnostic starting point, use electron.probe or snapshot -i/qa.attached for current-session state, and always cleanup the returned launchId when done. electron.launch uses an isolated temporary profile; it does not reuse the app's normal signed-in profile or attach to an already-running authenticated app. For signed-in local app state, host-launch the normal app with --remote-debugging-port when appropriate, then use raw args connect <port|url>; after connect, inspect tab list, select the stable tab id such as tab t2, then run a condition wait or snapshot -i before using refs. close commands (`close`, `quit`, or `exit`) only close the browser/CDP session; leave manually launched app shutdown, profile cleanup, and explicit artifacts to the host owner.",
49
- "For provider or specialized app workflows, load version-matched upstream guidance with skills get agentcore|electron|slack|dogfood|vercel-sandbox through the native tool; add --full when you need references/templates, and use skills get --all only for broad skill audits. Hosted sandbox workflows should use upstream @agent-browser/sandbox helpers outside this wrapper. Provider launches such as -p ios, --provider browserbase/kernel/browseruse/browserless/agentcore, and iOS --device are upstream-owned setup paths; use sessionMode fresh when switching providers and expect external credentials or local Appium/Xcode setup to be required.",
49
+ "For provider or specialized app workflows, load version-matched upstream guidance with skills get agentcore|electron|slack|dogfood|vercel-sandbox|derive-client through the native tool; add --full when you need references/templates, and use skills get --all only for broad skill audits. Use derive-client when recording HAR traffic to generate a standalone API client; prefer network har start (text bodies by default) or network har start --content all|none before multi-step capture. For accessibility audits use a11y or a11y --tags wcag2a,wcag2aa (CDP browsers only). Hosted sandbox workflows should use upstream @agent-browser/sandbox helpers outside this wrapper. Provider launches such as -p ios, --provider browserbase/kernel/browseruse/browserless/agentcore, and iOS --device are upstream-owned setup paths; use sessionMode fresh when switching providers and expect external credentials or local Appium/Xcode setup to be required.",
50
50
  "For dialogs and frames, use dialog status/accept/dismiss and frame <selector|main> through native args; dialog commands and eval snippets that look like alert/confirm/prompt/dialog triggers are shorter-bounded than normal browser calls, and timed-out dialog-like interactions may add inspect-dialog-after-timeout, dismiss-dialog-after-timeout, or recover-fresh-session-after-dialog-timeout nextActions. When --confirm-actions produces a pending confirmation, use details.nextActions or exact confirm <id> / deny <id> calls instead of inventing ids.",
51
51
  "If a session lands on the wrong page or tab, an interaction changes origin unexpectedly, or an open call returns blocked, blank, or otherwise unexpected results, use tab list / tab <tab-id-or-label> / snapshot -i to recover state before retrying different URLs or fallback strategies. For headed demos, put --headed on the first launch with sessionMode=fresh and verify with screenshot/tab/get-url evidence because tool success cannot prove the OS window is visible to the user. For desktop readiness, prefer real conditions first: wait --text, wait --url, wait --fn, wait --load <state>, wait --download, or qa.attached; for disappearance checks, use wait --fn predicates instead of stale upstream-help examples like wait <selector> --state hidden. Use electron.probe/status for wrapper-owned launch health or target mismatch. Fixed waits are a last resort: use explicit --timeout or top-level timeoutMs for legitimately slow waits, and treat a successful payload like \"waited\":\"timeout\" as elapsed time only—verify completion with an observed condition, fresh snapshot, or screenshot.",
52
52
  "For feed, timeline, or inbox reading tasks, focus on the main timeline/list region and read the first item there rather than unrelated composer or sidebar content.",
@@ -9,6 +9,7 @@ import { spawn } from "node:child_process";
9
9
  import { chmod, mkdir } from "node:fs/promises";
10
10
  import { env as processEnv, platform as processPlatform } from "node:process";
11
11
  import { GLOBAL_BOOLEAN_FLAGS_WITH_OPTIONAL_VALUES, GLOBAL_VALUE_FLAGS, getFlagName } from "./argv-grammar.js";
12
+ import { getImplicitSessionIdleTimeoutMs } from "./runtime.js";
12
13
  import { openSecureTempFile, writeSecureTempChunk } from "./temp.js";
13
14
  const MAX_BUFFERED_STDOUT_BYTES = 512 * 1_024;
14
15
  const MAX_BUFFERED_STDERR_CHARS = 32_000;
@@ -16,6 +17,7 @@ const MAX_BUFFERED_STDOUT_TAIL_CHARS = 32_000;
16
17
  const PROCESS_STDOUT_SPILL_FILE_PREFIX = "process-stdout";
17
18
  const AGENT_BROWSER_SOCKET_DIR_ENV = "AGENT_BROWSER_SOCKET_DIR";
18
19
  const AGENT_BROWSER_DEFAULT_TIMEOUT_ENV = "AGENT_BROWSER_DEFAULT_TIMEOUT";
20
+ const AGENT_BROWSER_IDLE_TIMEOUT_ENV = "AGENT_BROWSER_IDLE_TIMEOUT_MS";
19
21
  const PI_AGENT_BROWSER_PROCESS_TIMEOUT_ENV = "PI_AGENT_BROWSER_PROCESS_TIMEOUT_MS";
20
22
  const DEFAULT_AGENT_BROWSER_SOCKET_DIR_PREFIX = "/tmp/piab";
21
23
  export const SAFE_AGENT_BROWSER_OPERATION_TIMEOUT_MS = 25_000;
@@ -184,11 +186,15 @@ export function buildAgentBrowserProcessEnv(baseEnv = processEnv, overrides = un
184
186
  export async function runAgentBrowserProcess(options) {
185
187
  const { args, cwd, env, signal, stdin } = options;
186
188
  const timeoutMs = options.timeoutMs ?? getAgentBrowserProcessTimeoutMs();
187
- const explicitSocketDir = env?.[AGENT_BROWSER_SOCKET_DIR_ENV];
188
- let effectiveEnv = explicitSocketDir === undefined ? { ...env, [AGENT_BROWSER_SOCKET_DIR_ENV]: undefined } : env;
189
+ const processOverrides = {
190
+ [AGENT_BROWSER_IDLE_TIMEOUT_ENV]: String(getImplicitSessionIdleTimeoutMs()),
191
+ ...env,
192
+ };
193
+ const explicitSocketDir = processOverrides[AGENT_BROWSER_SOCKET_DIR_ENV];
194
+ let effectiveEnv = explicitSocketDir === undefined ? { ...processOverrides, [AGENT_BROWSER_SOCKET_DIR_ENV]: undefined } : processOverrides;
189
195
  const requestedSocketDir = explicitSocketDir ?? getAgentBrowserSocketDir();
190
196
  if (requestedSocketDir && (await ensureAgentBrowserSocketDir(requestedSocketDir))) {
191
- effectiveEnv = { ...env, [AGENT_BROWSER_SOCKET_DIR_ENV]: requestedSocketDir };
197
+ effectiveEnv = { ...effectiveEnv, [AGENT_BROWSER_SOCKET_DIR_ENV]: requestedSocketDir };
192
198
  }
193
199
  return await new Promise((resolve) => {
194
200
  let aborted = false;
@@ -22,10 +22,29 @@ export function classifyAgentBrowserFailureCategory(options) {
22
22
  const text = [options.errorText, options.validationError, options.parseError, options.spawnError, options.stderr].filter(Boolean).join("\n");
23
23
  const command = options.command ?? "";
24
24
  const usedRef = options.args?.some((arg) => /^@e\d+\b/.test(arg)) ?? false;
25
- if (options.confirmationRequired || /confirmation required|pending confirmation|requires confirmation/i.test(text))
25
+ // Explicit confirmation flag wins. Text-derived confirmation phrases come after locator-miss detection so a
26
+ // missed control named "Confirmation required" still gets selector recovery.
27
+ if (options.confirmationRequired)
26
28
  return "confirmation-required";
27
- if (options.timedOut || /timeout|timed out|watchdog|IPC read timeout|must stay under its 30s IPC read timeout/i.test(text))
29
+ // Upstream 0.32.4+ locator misses keep detail and may echo getByRole/getByText or Names seen lists.
30
+ // Evaluate before text-derived timeout/confirmation so accessible-name substrings cannot suppress recovery.
31
+ const isUpstreamLocatorMiss = /\bNo element found:\s*(?:getBy[A-Za-z]+|role=|text=|label=|placeholder=|alt=|title=|testid=)/i.test(text) ||
32
+ // No trailing \b after ":" — colon is non-word, so "Element not found: text=…" would not match.
33
+ (/\bElement not found:/i.test(text) && /\bVerify the selector, role, or name\b/i.test(text)) ||
34
+ /\bnone match name\b/i.test(text) ||
35
+ // Scope Names seen to role/name miss context (or find) so unrelated prose cannot trip selector-not-found.
36
+ (/\bNames seen:/i.test(text) && (command === "find" || /\belement has role\b|\bnone match name\b|\bgetByRole\b/i.test(text))) ||
37
+ /\belement has role\b[\s\S]*\bnone match\b/i.test(text);
38
+ if (isUpstreamLocatorMiss)
39
+ return "selector-not-found";
40
+ if (/confirmation required|pending confirmation|requires confirmation/i.test(text))
41
+ return "confirmation-required";
42
+ // Match real timeout phrasing only. Do not treat bare "timeout" as a hit — accessible names can include that word,
43
+ // and `timed?\s*out` would also match the substring "timeout" as time+out.
44
+ if (options.timedOut ||
45
+ /\b(?:timed\s+out|timeout exceeded|watchdog|IPC read timeout)\b|must stay under its 30s IPC read timeout|Operation timed out/i.test(text)) {
28
46
  return "timeout";
47
+ }
29
48
  if (/ENOENT|not found on PATH|could not find.*agent-browser|agent-browser is required but was not found/i.test(text))
30
49
  return "missing-binary";
31
50
  if (options.parseError || /invalid JSON|missing boolean success|success field must be boolean|returned no JSON output/i.test(text))
@@ -617,6 +617,49 @@ function formatConsoleText(data, commandInfo) {
617
617
  }
618
618
  return shown.join("\n");
619
619
  }
620
+ function formatA11yText(data) {
621
+ const counts = isRecord(data.counts) ? data.counts : undefined;
622
+ const violations = getArrayField(data, "violations") ?? [];
623
+ const incomplete = getArrayField(data, "incomplete") ?? [];
624
+ if (!counts && violations.length === 0 && incomplete.length === 0)
625
+ return undefined;
626
+ const lines = [];
627
+ const axeVersion = getStringField(data, "axeVersion");
628
+ if (axeVersion)
629
+ lines.push(`axe-core ${redactModelFacingText(axeVersion)}`);
630
+ const url = getStringField(data, "url");
631
+ if (url)
632
+ lines.push(`URL: ${redactModelFacingText(url)}`);
633
+ const violationCount = typeof counts?.violations === "number" ? counts.violations : violations.length;
634
+ const incompleteCount = typeof counts?.incomplete === "number" ? counts.incomplete : incomplete.length;
635
+ const passCount = typeof counts?.passes === "number" ? counts.passes : undefined;
636
+ const inapplicableCount = typeof counts?.inapplicable === "number" ? counts.inapplicable : undefined;
637
+ const countParts = [`${violationCount} violation${violationCount === 1 ? "" : "s"}`, `${incompleteCount} incomplete`];
638
+ if (passCount !== undefined)
639
+ countParts.push(`${passCount} passes`);
640
+ if (inapplicableCount !== undefined)
641
+ countParts.push(`${inapplicableCount} inapplicable`);
642
+ lines.push(`A11y audit: ${countParts.join(", ")}.`);
643
+ const previewLimit = Math.min(10, DIAGNOSTIC_LOG_PREVIEW_LIMIT);
644
+ const preview = violations.slice(0, previewLimit).map((item, index) => {
645
+ if (!isRecord(item))
646
+ return `${index + 1}. ${stringifyModelFacing(item)}`;
647
+ const id = redactModelFacingText(getStringField(item, "id") ?? "rule");
648
+ const impact = redactModelFacingText(getStringField(item, "impact") ?? "unknown");
649
+ const help = firstLine(redactModelFacingText(getStringField(item, "help") ?? "").replace(/\s+/g, " ").trim(), 160);
650
+ const nodeCount = typeof item.nodeCount === "number" ? item.nodeCount : getArrayField(item, "nodes")?.length;
651
+ const nodePart = typeof nodeCount === "number" ? `, ${nodeCount} node${nodeCount === 1 ? "" : "s"}` : "";
652
+ return `${index + 1}. [${impact}] ${id}${nodePart}${help ? ` — ${help}` : ""}`;
653
+ });
654
+ lines.push(...preview);
655
+ if (violations.length > preview.length) {
656
+ lines.push(`... (${violations.length - preview.length} additional violations omitted from preview)`);
657
+ }
658
+ if (incompleteCount > 0) {
659
+ lines.push(`${incompleteCount} incomplete check${incompleteCount === 1 ? "" : "s"} need manual review (see details.data.incomplete).`);
660
+ }
661
+ return lines.join("\n");
662
+ }
620
663
  function formatErrorsText(data, commandInfo) {
621
664
  const errors = getArrayField(data, "errors");
622
665
  if (!errors)
@@ -966,6 +1009,8 @@ export function formatDiagnosticText(commandInfo, data) {
966
1009
  return formatConsoleText(data, commandInfo);
967
1010
  if (commandInfo.command === "errors")
968
1011
  return formatErrorsText(data, commandInfo);
1012
+ if (commandInfo.command === "a11y")
1013
+ return formatA11yText(data);
969
1014
  if (commandInfo.command === "dashboard")
970
1015
  return formatDashboardText(data);
971
1016
  if (commandInfo.command === "doctor")
@@ -139,7 +139,7 @@ V1 ownership rule:
139
139
  Practical policy:
140
140
  - preserve the current branch-visible extension-managed session across `/reload`, exact-session relaunch, `/resume`, and Pi 0.79 `session_tree` branch transitions so persisted sessions can keep following the live browser after lifecycle changes
141
141
  - close the active extension-managed session when the originating `pi` process quits, while leaving explicit caller-provided sessions alone
142
- - set an idle timeout on extension-managed sessions as a backstop for abnormal exits or cleanup failures
142
+ - set an idle timeout on extension-managed sessions as a backstop for abnormal exits or cleanup failures, and apply that same `AGENT_BROWSER_IDLE_TIMEOUT_MS` value to every upstream subprocess (including wrapper helper snapshots, tab lists, and navigation-summary reads) because changing the launch environment between calls can make upstream restart the background browser, discard the active tab, and invalidate fresh refs
143
143
  - clean up process-private temp spill artifacts on shutdown, but keep persisted-session snapshot spill files in a private session-scoped artifact directory with a bounded per-session budget so `details.fullOutputPath` stays usable after reload/resume without unbounded growth
144
144
  - keep explicit screenshots, downloads, PDFs, traces, HAR captures, and recordings written to caller-chosen paths on disk after a successful upstream close command (`close`, `quit`, or `exit`); before artifact-producing commands run, create missing parent directories for requested host paths, and for simple loopback HTML anchor downloads with resolvable HTTP(S) hrefs the wrapper may save directly to the requested path before upstream fallback. When the bounded `details.artifactManifest` has entries, successful close commands also surface `details.artifactCleanup` and a compact `Artifact lifecycle` note pointing to structured explicit paths so operators remove files with normal host tools—the native tool does not delete arbitrary user paths (`extensions/agent-browser/lib/orchestration/browser-run/diagnostics.ts`, `getArtifactCleanupGuidance`); contract in [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#details), checklist `RQ-0079` in [`SUPPORT_MATRIX.md`](SUPPORT_MATRIX.md)
145
145
  - reconstruct the current branch-visible extension-managed session, page-scoped refs, artifact manifest, and Electron launch records from the active transcript branch on `session_start` and `session_tree` so later default calls keep following the active managed browser after resume/reload or branch switching; restore also honors successful explicit `--session <wrapper-owned> close` rows and `electron.cleanup` managed-session steps so closed wrapper-owned sessions are not resurrected
@@ -152,6 +152,7 @@ Practical policy:
152
152
  - after profiled `open` / `goto` / `navigate` calls, verify the active tab still matches the returned page URL and best-effort switch back when restored profile tabs steal focus
153
153
  - once the wrapper observes tab-drift risk for a session (profile restore correction, overlapping stale opens, or restored session state), later active-tab commands may synthesize a tiny upstream `batch` that re-selects that tab and then runs the requested command in the same upstream invocation; routine same-session commands avoid `tab list` preflights to reduce probes that can perturb upstream click behavior
154
154
  - for sessions with observed tab-drift risk, after a successful command on a known tab target, the wrapper may best-effort restore that same target again if restored/background tabs steal focus after the command returns; routine same-session commands skip this post-command `tab list` probe
155
+ - after successful `tab close`, read the now-active URL/title before updating per-session page state because upstream's close payload identifies the closed tab but does not report the remaining active tab; this keeps subsequent ref guards and interactions aligned without requiring a recovery snapshot solely to repair wrapper state
155
156
  - keep a per-session `refSnapshot` aligned with the last successful `snapshot` (including refs merged from a successful `batch` by taking the last successful `snapshot` step in batch result order): restore it from persisted tool `details` when reloading, resuming, or moving to a different Pi session-tree branch, store bounded ref role/name metadata from the same snapshot for wrapper-side current-ref diagnostics, drop it on successful close commands (`close`, `quit`, or `exit`), and refuse mutation-prone `@e…` argv before spawn when the active tab URL no longer matches the snapshot URL, when a ref id was never in that snapshot, or when `batch` stdin would reuse `@e…` on a guarded step after an earlier invalidating step without a later `snapshot` step in the same stdin array. Same-snapshot `fill @e…` rows are guarded but do not themselves set that invalidation latch, so ordinary form fills can precede a click/submit row in one batch—see [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#details) for the agent-visible contract and failure text; typed per-session tab/ref/pinning state lives in `extensions/agent-browser/lib/session-page-state.ts` and is updated from `extensions/agent-browser/index.ts` after each tool result
156
157
  - for top-level non-Electron direct `click` commands with an eligible target, install a bounded in-page target-specific event probe before upstream runs; if upstream reports success but no trusted pointer/mouse/click event reached the resolved target, fail the tool and report `details.clickDispatch` with explicit retry/inspect next actions (the wrapper does not replay clicks in-page). The probe covers `xpath=` targets and current `@e…` / `ref=` refs whose latest stored `refSnapshot.refs` role is `button`, `checkbox`, `menuitem`, `radio`, `switch`, or `tab`; it uses that role/name metadata, including snapshot-order `duplicateIndex` for duplicate-name refs, instead of taking a fresh pre-click snapshot that could recycle upstream refs. The probe is intentionally skipped for CSS selector clicks, unresolved `find … click` locators, and `batch`/`job`/`qa` click steps
157
158
  - derive narrow prompt guards only for concrete evidence invariants: exact required screenshot paths block browser close until the artifact manifest verifies those paths. The wrapper intentionally does not infer broad business/user intent from prompt text such as order/payment/post boundaries; agents must follow those instructions themselves. The artifact guard is bounded preflight policy (`details.promptGuard`, `failureCategory: "policy-blocked"`), not a reusable browser recipe layer