pi-agent-browser-native 0.6.6 → 0.6.8

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 (42) hide show
  1. package/CHANGELOG.md +40 -1
  2. package/README.md +21 -6
  3. package/dist/extensions/agent-browser/index.js +138 -75
  4. package/dist/extensions/agent-browser/lib/command-taxonomy.js +6 -5
  5. package/dist/extensions/agent-browser/lib/electron/cleanup.js +10 -1
  6. package/dist/extensions/agent-browser/lib/input-modes/params.js +20 -7
  7. package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +0 -1
  8. package/dist/extensions/agent-browser/lib/managed-session-restore.js +13 -12
  9. package/dist/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.js +6 -25
  10. package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +2 -2
  11. package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +2 -3
  12. package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +10 -3
  13. package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +3 -0
  14. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/wait-timeouts.js +1 -1
  15. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +104 -117
  16. package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +64 -36
  17. package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +100 -135
  18. package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +3 -1
  19. package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +3 -1
  20. package/dist/extensions/agent-browser/lib/page-target-validation.js +10 -10
  21. package/dist/extensions/agent-browser/lib/parsing.js +7 -0
  22. package/dist/extensions/agent-browser/lib/playbook.js +6 -9
  23. package/dist/extensions/agent-browser/lib/process.js +28 -113
  24. package/dist/extensions/agent-browser/lib/recording-reservations.js +3 -1
  25. package/dist/extensions/agent-browser/lib/results/action-recommendations.js +5 -2
  26. package/dist/extensions/agent-browser/lib/results/envelope.js +9 -4
  27. package/dist/extensions/agent-browser/lib/results/next-actions.js +8 -0
  28. package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +45 -43
  29. package/dist/extensions/agent-browser/lib/results/presentation/batch.js +2 -1
  30. package/dist/extensions/agent-browser/lib/results/presentation/errors.js +10 -2
  31. package/dist/extensions/agent-browser/lib/results/presentation.js +9 -2
  32. package/dist/extensions/agent-browser/lib/results/recovery-actions.js +4 -4
  33. package/dist/extensions/agent-browser/lib/runtime.js +18 -2
  34. package/dist/extensions/agent-browser/lib/session-page-state.js +29 -10
  35. package/docs/ARCHITECTURE.md +8 -5
  36. package/docs/COMMAND_REFERENCE.md +25 -16
  37. package/docs/ELECTRON.md +6 -6
  38. package/docs/RELEASE.md +14 -5
  39. package/docs/REQUIREMENTS.md +1 -1
  40. package/docs/SUPPORT_MATRIX.md +18 -5
  41. package/docs/TOOL_CONTRACT.md +38 -25
  42. package/package.json +5 -1
@@ -1,5 +1,5 @@
1
1
  import { isOpenNavigationCommand } from "../../command-taxonomy.js";
2
- import { redactSensitiveText } from "../../runtime.js";
2
+ import { extractUpstreamCommandTokens, redactSensitiveText } from "../../runtime.js";
3
3
  import { buildBrowserProfileConfigRecovery } from "./browser-profile-recovery.js";
4
4
  import { redactModelFacingText } from "./common.js";
5
5
  import { buildAgentBrowserNextActions } from "../action-recommendations.js";
@@ -59,6 +59,13 @@ function getKeyboardPressHint(commandInfo, errorText) {
59
59
  return undefined;
60
60
  return KEYBOARD_PRESS_ERROR_HINT;
61
61
  }
62
+ export function isOverlayBlockedClickError(command, errorText, args) {
63
+ const tokens = args ? extractUpstreamCommandTokens(args) : [];
64
+ const action = tokens[0] === "find"
65
+ ? tokens[tokens[1] === "nth" ? 4 : 3] ?? "click"
66
+ : tokens[0] ?? command;
67
+ return action === "click" && errorText !== undefined && /\bis covered by\b[\s\S]*\bat its click point\b/i.test(errorText);
68
+ }
62
69
  export function redactClipboardPermissionEcho(commandInfo, errorText) {
63
70
  if (commandInfo.command !== "clipboard")
64
71
  return errorText;
@@ -162,7 +169,7 @@ export function appendSelectorRecoveryHint(errorText) {
162
169
  return `${errorText}\n\n${hint}`;
163
170
  }
164
171
  export function buildErrorPresentation(options) {
165
- const { args, commandInfo, errorText, sessionName } = options;
172
+ const { args, commandInfo, errorText, presentationCommand, sessionName } = options;
166
173
  const safeErrorText = redactModelFacingText(redactSensitiveText(redactClipboardPermissionEcho(commandInfo, errorText)));
167
174
  const selectorHintedErrorText = appendSelectorRecoveryHint(safeErrorText);
168
175
  const unknownCommandSuggestions = getUnknownCommandSuggestions(commandInfo.command, safeErrorText);
@@ -193,6 +200,7 @@ export function buildErrorPresentation(options) {
193
200
  args,
194
201
  command: commandInfo.command,
195
202
  failureCategory: categoryDetails.failureCategory,
203
+ overlayBlockedClick: isOverlayBlockedClickError(presentationCommand ?? commandInfo.command, safeErrorText, args ?? commandInfo.commandTokens),
196
204
  resultCategory: "failure",
197
205
  sessionName,
198
206
  }) ?? []),
@@ -9,7 +9,7 @@ import { applyArtifactManifest, attachInlineImage, buildArtifactVerificationSumm
9
9
  import { buildBatchPresentation, isAgentBrowserBatchResultArray, redactBatchStepErrorData } from "./presentation/batch.js";
10
10
  import { getPresentationPaths, isStringArray } from "./presentation/content.js";
11
11
  import { buildNetworkRequestsNextActions, buildStreamNextActions, enrichStreamStatusData, formatNetworkRouteDiagnosticsText, redactPresentationData, } from "./presentation/diagnostics.js";
12
- import { buildErrorPresentation } from "./presentation/errors.js";
12
+ import { buildErrorPresentation, isOverlayBlockedClickError } from "./presentation/errors.js";
13
13
  import { compactLargePresentationOutput } from "./presentation/large-output.js";
14
14
  import { buildPageChangeSummary } from "./presentation/navigation.js";
15
15
  import { formatPresentationContentText, formatPresentationSummary } from "./presentation/registry.js";
@@ -53,7 +53,13 @@ export async function buildToolPresentation(options) {
53
53
  const commandInfoWithTokens = commandInfo.commandTokens || !args ? commandInfo : { ...commandInfo, commandTokens: extractUpstreamCommandTokens(args) };
54
54
  const presentationCommandInfo = resolvePresentationCommandInfo(commandInfoWithTokens, compiledSemanticAction);
55
55
  if (errorText) {
56
- return buildErrorPresentation({ args, commandInfo, errorText, sessionName });
56
+ return buildErrorPresentation({
57
+ args,
58
+ commandInfo,
59
+ errorText,
60
+ presentationCommand: presentationCommandInfo.command,
61
+ sessionName,
62
+ });
57
63
  }
58
64
  const data = enrichStreamStatusData(commandInfoWithTokens, envelope?.data);
59
65
  const presentationData = commandInfo.command === "batch" && isAgentBrowserBatchResultArray(data)
@@ -179,6 +185,7 @@ export async function buildToolPresentation(options) {
179
185
  command: presentationCommandInfo.command,
180
186
  confirmationId: confirmationRequired?.id,
181
187
  failureCategory: presentationWithManifest.failureCategory,
188
+ overlayBlockedClick: isOverlayBlockedClickError(presentationCommandInfo.command, envelope?.success === false ? presentationWithManifest.summary : undefined, args ?? presentationCommandInfo.commandTokens),
182
189
  resultCategory: presentationWithManifest.resultCategory ?? "success",
183
190
  savedFilePath: presentationWithManifest.savedFilePath,
184
191
  sessionName,
@@ -45,11 +45,11 @@ function buildTabSnapshotRecoveryAction(options) {
45
45
  });
46
46
  }
47
47
  return buildNextToolAction({
48
- args: options.sessionArgs(["batch"]),
48
+ args: options.sessionArgs(["batch", "--bail"]),
49
49
  id: options.id,
50
- reason: `${options.reason} The batch selects the stable tab before snapshotting.`,
51
- safety: `${options.safety} The snapshot retry is atomic with tab selection, so it does not assume the intended tab is already active.`,
52
- stdin: JSON.stringify([["tab", options.tabId], ["snapshot", "-i"]]),
50
+ reason: `${options.reason} The batch selects and verifies the stable tab before snapshotting.`,
51
+ safety: `${options.safety} A failed tab selection or URL check stops before the snapshot.`,
52
+ stdin: JSON.stringify([["tab", options.tabId], ["get", "url"], ["snapshot", "-i"]]),
53
53
  });
54
54
  }
55
55
  export function buildRecoveryNextActions(recovery) {
@@ -2,7 +2,7 @@ import { createHash, randomUUID } from "node:crypto";
2
2
  import { basename } from "node:path";
3
3
  import { extractUpstreamCommandTokens, findCommandStartIndex, parseArgvDescriptor, parseCommandInfo, } from "./argv-descriptor.js";
4
4
  import { batchHasSuccessfulCloseAll, getSuccessfulBatchCloseLifecycle } from "./batch-lifecycle.js";
5
- import { canonicalizeAgentBrowserNamespace, extractExplicitNamespace, extractExplicitSessionName, getAgentBrowserSessionIdentityKey, getBooleanFlagValue, GLOBAL_BOOLEAN_FLAGS_WITH_OPTIONAL_VALUES, GLOBAL_VALUE_FLAGS_ALLOWING_DASH_VALUE, isAgentBrowserSessionIdentityKeyInNamespace, isUpstreamEnvFlagEnabled, PREVALIDATED_VALUE_FLAGS, resolveAgentBrowserNamespace, scanUpstreamGlobalFlagOccurrences, } from "./argv-grammar.js";
5
+ import { canonicalizeAgentBrowserNamespace, extractExplicitNamespace, extractExplicitSessionName, getAgentBrowserSessionIdentityKey, getBooleanFlagValue, GLOBAL_BOOLEAN_FLAGS_WITH_OPTIONAL_VALUES, GLOBAL_VALUE_FLAGS_ALLOWING_DASH_VALUE, isAgentBrowserSessionIdentityKeyInNamespace, isUpstreamEnvFlagEnabled, PREVALIDATED_VALUE_FLAGS, resolveAgentBrowserNamespace, scanUpstreamGlobalFlagOccurrences, stripUpstreamGlobalFlags, } from "./argv-grammar.js";
6
6
  import { needsManagedSession } from "./command-policy.js";
7
7
  import { isCloseAllCommand, isCloseCommand, isOpenNavigationCommand } from "./command-taxonomy.js";
8
8
  import { hasLaunchScopedFlagToken, LAUNCH_SCOPED_FLAG_DEFINITIONS, LAUNCH_SCOPED_FLAG_LABEL, } from "./launch-scoped-flags.js";
@@ -607,6 +607,18 @@ function getUnsupportedInlineWaitDownloadError(args) {
607
607
  return undefined;
608
608
  return `agent-browser ${TARGET_AGENT_BROWSER_VERSION} does not support \`wait --download=<path>\`. Pass the optional path as a separate argument: \`wait --download <path>\` (or \`wait -d <path>\`).`;
609
609
  }
610
+ function getBareNoSandboxValidationError(args, batchStep) {
611
+ // Native batch rows skip global parsing; --args is effective only on the outer CLI call.
612
+ const tokens = batchStep ? args : stripUpstreamGlobalFlags(args);
613
+ const command = tokens[0];
614
+ const leading = command === "--no-sandbox";
615
+ if (!leading && (!isOpenNavigationCommand(command) || !tokens.slice(1).includes("--no-sandbox")))
616
+ return undefined;
617
+ const explanation = leading
618
+ ? "`--no-sandbox` is not an agent-browser command."
619
+ : `\`--no-sandbox\` is ignored as an option by \`${command}\`.`;
620
+ return `${explanation} It is a Chromium launch argument. Put it in top-level \`--args\` and start a fresh session: { args: ["--args", "--no-sandbox", "open", "https://example.com"], sessionMode: "fresh" }. For batch, put --args before batch, not inside a step.`;
621
+ }
610
622
  export function validateToolArgs(args, options = {}) {
611
623
  if (args.length === 0) {
612
624
  return "`args` must contain at least one agent-browser command token.";
@@ -623,7 +635,8 @@ export function validateToolArgs(args, options = {}) {
623
635
  const invalidValueFlag = inspection ? undefined : getInvalidValueFlagDetails(args, !options.batchStep);
624
636
  if (invalidValueFlag?.reason === "unsupported-assignment")
625
637
  return formatInvalidValueFlagError(invalidValueFlag, options.batchStep);
626
- return getBareMcpValidationError(args) ?? getSingleKeyCommandValidationError(args) ?? getUnsupportedInlineWaitDownloadError(args);
638
+ return (inspection ? undefined : getBareNoSandboxValidationError(args, options.batchStep === true))
639
+ ?? getBareMcpValidationError(args) ?? getSingleKeyCommandValidationError(args) ?? getUnsupportedInlineWaitDownloadError(args);
627
640
  }
628
641
  function getInvalidValueFlagDetails(args, allowRestoreAssignment = true) {
629
642
  for (let index = 0; index < args.length; index += 1) {
@@ -885,6 +898,9 @@ export function buildExecutionPlan(args, options) {
885
898
  managedSessionName = options.freshSessionName;
886
899
  sessionName = options.freshSessionName;
887
900
  }
901
+ if (commandInfo.command !== undefined && !sessionName && !explicitNamespacePresent) {
902
+ namespace = resolveAgentBrowserNamespace(args, getAgentBrowserProcessEnvironment().AGENT_BROWSER_NAMESPACE);
903
+ }
888
904
  const targetsActiveManagedSession = options.managedSessionActive
889
905
  && sessionName
890
906
  && getAgentBrowserSessionIdentityKey(sessionName, namespace) === getAgentBrowserSessionIdentityKey(options.managedSessionName, options.managedSessionNamespace);
@@ -1,7 +1,7 @@
1
1
  import { extractUpstreamCommandTokens } from "./argv-descriptor.js";
2
2
  import { getAgentBrowserSessionIdentityKey, isAgentBrowserSessionIdentityKeyInNamespace } from "./argv-grammar.js";
3
3
  import { batchHasSuccessfulCloseAll, getSuccessfulBatchCloseLifecycle } from "./batch-lifecycle.js";
4
- import { isCloseAllCommand, isCloseCommand, isReadOnlyDiagnosticSessionTargetCommand, isRecordPageTransitionCommand, isUnverifiedPageTransitionCommand, isWebMcpPageMutationCommand } from "./command-taxonomy.js";
4
+ import { isCloseAllCommand, isCloseCommand, isReadOnlyDiagnosticSessionTargetCommand, isRecordPageTransitionCommand, isUnverifiedPageTransitionCommand, isWebMcpPageMutationCommand, isWindowOrDiffPageTransitionCommand } from "./command-taxonomy.js";
5
5
  import { isRecord } from "./parsing.js";
6
6
  import { getEditableRefEvidence } from "./results/editable-ref-evidence.js";
7
7
  import { enrichSnapshotRefEntries, getSnapshotRefEntries } from "./results/snapshot-refs.js";
@@ -24,8 +24,8 @@ export function normalizeSessionTabTarget(target) {
24
24
  if (!target) {
25
25
  return undefined;
26
26
  }
27
- const url = normalizeComparableUrl(target.url);
28
- if (!url) {
27
+ const url = target.url?.trim();
28
+ if (!url || normalizeComparableUrl(url) === undefined) {
29
29
  return undefined;
30
30
  }
31
31
  const title = target.title?.trim();
@@ -38,7 +38,7 @@ export function isAboutBlankSessionTabTarget(target) {
38
38
  return isAboutBlankUrl(target?.url);
39
39
  }
40
40
  export function commandExplicitlyTargetsAboutBlank(commandTokens) {
41
- return commandTokens.some((token) => isAboutBlankUrl(token));
41
+ return (commandTokens[0] === "window" && commandTokens[1] === "new") || commandTokens.some((token) => isAboutBlankUrl(token));
42
42
  }
43
43
  export function targetsMatch(left, right) {
44
44
  if (!left || !right)
@@ -90,10 +90,15 @@ export function extractSessionTabTargetFromBatchResults(data) {
90
90
  let currentTarget;
91
91
  let pendingTitle;
92
92
  for (const item of data) {
93
- if (!isRecord(item) || item.success === false) {
93
+ if (!isRecord(item))
94
94
  continue;
95
+ const [name, subcommand] = extractUpstreamCommandTokens(extractBatchResultCommand(item));
96
+ if (isWindowOrDiffPageTransitionCommand(name, subcommand)) {
97
+ currentTarget = undefined;
98
+ pendingTitle = undefined;
95
99
  }
96
- const [name, subcommand] = extractBatchResultCommand(item);
100
+ if (item.success === false)
101
+ continue;
97
102
  const result = item.result;
98
103
  if (isCloseCommand(name)) {
99
104
  currentTarget = undefined;
@@ -213,6 +218,9 @@ export function buildPageTransitionRefSnapshotInvalidation(summary) {
213
218
  export function getCommandRefSnapshotInvalidation(commandTokens) {
214
219
  if (isRecordPageTransitionCommand(commandTokens))
215
220
  return buildPageTransitionRefSnapshotInvalidation();
221
+ if (isWindowOrDiffPageTransitionCommand(commandTokens[0], commandTokens[1])) {
222
+ return buildPageTransitionRefSnapshotInvalidation("A window new or diff url command replaced or navigated the active page and invalidated prior refs. Run snapshot -i before using page-scoped refs.");
223
+ }
216
224
  if (isWebMcpPageMutationCommand(commandTokens)) {
217
225
  return buildPageTransitionRefSnapshotInvalidation("A WebMCP invoke, result, or cancel command can mutate, rerender, or navigate the page, so the prior snapshot refs were invalidated. Run snapshot -i before using page-scoped refs.");
218
226
  }
@@ -228,7 +236,7 @@ export function extractLatestRefSnapshotStateFromBatchResults(data) {
228
236
  for (const item of data) {
229
237
  if (!isRecord(item))
230
238
  continue;
231
- const commandTokens = extractBatchResultCommand(item);
239
+ const commandTokens = extractUpstreamCommandTokens(extractBatchResultCommand(item));
232
240
  const [name] = commandTokens;
233
241
  if (item.success !== false && isCloseCommand(name)) {
234
242
  latestState = undefined;
@@ -372,9 +380,10 @@ export class SessionPageState {
372
380
  }
373
381
  const tabTarget = getRestoredSessionTabTarget(details, command, subcommand);
374
382
  const tabTargetUnknown = details.sessionTabTargetUnknown === true;
383
+ const reopenPending = typeof details.sessionTabReopenPending === "boolean" ? details.sessionTabReopenPending : undefined;
375
384
  const refSnapshotInvalidation = getRestoredRefSnapshotInvalidation(details, command);
376
385
  const refSnapshot = refSnapshotInvalidation ? undefined : getRestoredRefSnapshot(details);
377
- if (!tabTarget && !tabTargetUnknown && !refSnapshotInvalidation && !refSnapshot)
386
+ if (!tabTarget && !tabTargetUnknown && !refSnapshotInvalidation && !refSnapshot && reopenPending === undefined)
378
387
  continue;
379
388
  restoredOrder += 1;
380
389
  if (tabTargetUnknown) {
@@ -389,8 +398,11 @@ export class SessionPageState {
389
398
  }
390
399
  if (tabTarget) {
391
400
  state.tabTargetUnknownOrders.delete(sessionKey);
392
- state.tabTargets.set(sessionKey, { order: restoredOrder, target: tabTarget });
401
+ state.tabTargets.set(sessionKey, { order: restoredOrder, reopenPending: state.tabTargets.get(sessionKey)?.reopenPending, target: tabTarget });
393
402
  }
403
+ const currentTarget = state.tabTargets.get(sessionKey);
404
+ if (currentTarget && reopenPending !== undefined)
405
+ currentTarget.reopenPending = reopenPending;
394
406
  if (refSnapshotInvalidation) {
395
407
  state.refSnapshots.delete(sessionKey);
396
408
  state.refSnapshotInvalidations.set(sessionKey, { ...refSnapshotInvalidation, order: restoredOrder });
@@ -421,6 +433,7 @@ export class SessionPageState {
421
433
  return {};
422
434
  return {
423
435
  pinningReason: this.tabPinningReasons.get(sessionName),
436
+ ...(this.tabTargets.get(sessionName)?.reopenPending !== undefined ? { tabReopenPending: this.tabTargets.get(sessionName)?.reopenPending } : {}),
424
437
  refSnapshot: stripRefSnapshotOrder(this.refSnapshots.get(sessionName)),
425
438
  refSnapshotInvalidation: stripRefSnapshotInvalidationOrder(this.refSnapshotInvalidations.get(sessionName)),
426
439
  ...(this.tabTargetUnknownOrders.has(sessionName) ? { tabTargetUnknown: true } : {}),
@@ -433,9 +446,15 @@ export class SessionPageState {
433
446
  return { ...this.get(options.sessionName), applied: false, stale: true };
434
447
  }
435
448
  this.tabTargetUnknownOrders.delete(options.sessionName);
436
- this.tabTargets.set(options.sessionName, { order: options.update, target: options.target });
449
+ this.tabTargets.set(options.sessionName, { order: options.update, reopenPending: current?.reopenPending, target: options.target });
437
450
  return { ...this.get(options.sessionName), applied: true };
438
451
  }
452
+ setTabReopenPending(options) {
453
+ const current = this.tabTargets.get(options.sessionName);
454
+ if (!current || !shouldApplyTabTargetUpdate(current, this.tabTargetUnknownOrders.get(options.sessionName), options.update))
455
+ return;
456
+ this.tabTargets.set(options.sessionName, { ...current, order: options.update, reopenPending: options.pending });
457
+ }
439
458
  applyRefSnapshot(options) {
440
459
  if (!shouldApplyRefStateUpdate({
441
460
  currentInvalidation: this.refSnapshotInvalidations.get(options.sessionName),
@@ -36,7 +36,7 @@ Why:
36
36
 
37
37
  The extension should:
38
38
  - resolve `agent-browser` from `PATH`
39
- - invoke it directly on POSIX; on Windows, route through PowerShell with single-quoted argv so npm launchers and the native `.exe` receive the same command tail that a user would type, and terminate the full PowerShell/agent-browser process tree with `taskkill /T /F` on timeout or abort before falling back to the direct child signal
39
+ - invoke it with native Node `spawn` on POSIX and `cross-spawn` on Windows. The latter handles Windows `PATH`/`PATHEXT`, `.cmd` shims and argument escaping without PowerShell or wrapper-owned command reordering. Caller commands and helper probes retain empty operands, literal doublequotes, command/subcommand adjacency and explicit default `--namespace ""`; the selected shim still owns upstream architecture selection. Keep piped stdin/stdout/stderr and terminate the full Windows shell/agent-browser process tree with `taskkill /T /F` on timeout or abort before falling back to the direct child signal. Missing commands use the spawner's `ENOENT`, not a parsed shell-error string
40
40
  - inject `--json`
41
41
  - complete each upstream invocation when the direct `agent-browser` child exits even if Node delays `"close"`: piped stdio can stay referenced by longer-lived descendant processes, so `runAgentBrowserProcess` watches `exit` and `close` together, leaves stdio intact during a short post-`exit` grace so normal `close` can still win, destroys streams only when the post-`exit` fallback fires, and prefers `close` codes then wrapper timeout (`124`) over signal-shaped `exit` codes (`watchSpawnedChildCompletion` / `resolveSpawnedChildExitCode` in `extensions/agent-browser/lib/process.ts`) so the tool cannot hang after the CLI process has already terminated
42
42
  - support optional stdin only for `eval --stdin`, `batch`, `auth save --password-stdin`, and wrapper-generated `batch` stdin from top-level `job`, `qa`, `sourceLookup`, or `networkSourceLookup`, rejecting other command/stdin combinations before launch; top-level `electron` never accepts caller `stdin` (see [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#electron))
@@ -69,6 +69,8 @@ Browser isolation is separate from language isolation. A pre-spawn Pi custom ent
69
69
 
70
70
  ### Agent-first UX
71
71
 
72
+ Artifact directory preparation has one filesystem-error boundary inside the existing cleanup/finally path: direct, stdin and raw-argv failures return structured validation and the attempted directory without launching the requested command. Raw batch strings and argv-over-stdin precedence stay native; absolute artifact paths avoid differing daemon/Pi working directories. Artifact metadata retains known requested and reported/resolved paths without a new canonicalization pass. A bounded 16-byte regular-file header read recognizes the existing PNG/JPEG/GIF/WebP formats; inline screenshots use the same classifier under their existing byte limit, and unknown MIME types are omitted. Recording page warnings use the shared transition predicate and confirmed CLI/reached-row evidence through the existing prose/JSON warning path, independently of conservative ref-state invalidation after an uncertain batch.
73
+
72
74
  The primary UX is the agent calling the tool directly.
73
75
 
74
76
  That means:
@@ -157,6 +159,7 @@ V1 ownership rule:
157
159
  Practical policy:
158
160
  - preserve the current branch-visible extension-managed session across `/reload`, exact-session relaunch, `/resume`, and Pi 0.84.0+ `session_tree` branch transitions so persisted sessions can keep following the live browser after lifecycle changes
159
161
  - close the active extension-managed session when the originating `pi` process quits, while leaving explicit caller-provided sessions alone
162
+ - after branch restore, use the existing locked daemon inspection to distinguish a confirmed inactive wrapper-owned daemon from a live, unknown, or unavailable one. For compatible automatic managed restore only, keep the pending reopen in ordered session page state and persist it as `sessionTabReopenPending`. Non-page calls such as `tab list` and explicit HTTP reads can start a daemon without fulfilling it, including across branch/reload replay. Before the first current-page operation (`get url`, history commands and relative `pushstate` included), reopen the complete recorded URL with native `open`, invalidate old refs with the existing `page-transition` state, and verify the actual tab. Native `open` resets frame scope. Consume the obligation on that attempt or an executed explicit context/navigation/close command, not an unreached batch row; a failed open does not permit repeated navigation of a now-live browser. After the reopen CLI starts, cancellation returns a structured `aborted` result through the ordinary result path with the exact namespace/session, consumed marker and ref invalidation; it does not throw away replay state or run later browser helpers. Cancellation before the CLI starts does not consume the pending reopen. Internal URLs retain their fragments while tab/ref comparisons remain fragment-insensitive and presentation keeps normal redaction. Older transcripts cannot recover a fragment they did not record. Caller-owned/attached browsers and restore-disabled sessions do not take this path; live wrong-tab recovery still only selects an existing target. Reopening reloads the URL, not unsaved forms, JavaScript memory, or history. There is no second restore store or lifecycle lock.
160
163
  - 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
161
164
  - for wrapper-owned implicit sessions only, set a transcript- and checkout-scoped `AGENT_BROWSER_RESTORE` key on compatible calls so cookies and web storage can survive idle shutdown, reload, and resume. Explicit caller sessions, restore/state choices, profiles, upstream config, file access, launch arguments, environment variables, local pages, output paths, and close arguments remain upstream-owned and pass through unchanged. `piab-*` names are not reserved; session/state lists and restore identifiers are not filtered or redacted. The wrapper validates only its automatic restore checkout/storage identity and coordinates same-daemon reuse so its own restore pools cannot mix. Ambiguous page-target transitions still require live `get url` verification before content calls. The current v3 ticket-claim lock is the only managed-daemon coordination protocol; no earlier lock bridge or compatibility path remains.
162
165
  - redact snapshot spill payloads before writing them, clean up process-private temp spill artifacts on shutdown, and keep persisted-session 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
@@ -172,16 +175,16 @@ Practical policy:
172
175
  - expose `details.browserWindow` and one visible login handoff only when a successful first/fresh local wrapper-managed headed result, including `batch`, is not an attachment and has upstream `lifecycle.effectiveLaunch.browserLaunched: true` and a `created`/`replaced` managed-session outcome. Keep `visibility: "unverified"`: this is launch evidence, never a claim about the user's OS desktop
173
176
  - leave explicit caller-provided `--session` choices alone unless the caller closes them explicitly, but before any content-bearing read or interaction against a caller-owned explicit session, live-probe that session with `get url` instead of trusting missing or stale transcript page state; hold the effective canonical namespace/session queue from that probe through semantic snapshot resolution and the main command so another same-instance call cannot change tabs in between. Non-bail batch analysis retains every possible page left by a failed transition up to a fixed bound and blocks later content only when the target is unverified; exceeding the bound also fails closed to exact `batch --bail` guidance. Nested `batch` steps remain unsupported, and raw batch command strings mirror upstream's ASCII-space tokenizer, including quote/backslash handling, rather than splitting on other Unicode whitespace.
174
177
  - 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
175
- - 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
178
+ - once the wrapper observes tab-drift risk for a session (profile restore correction, overlapping stale opens, or restored session state), later active-tab commands verify the intended tab under the existing session queue before semantic/ref helpers and user commands. Native selection runs only when the intended tab is not already active, because upstream selection clears refs and frame scope even on same-tab reselection. Missing targets, failed selection, and post-selection target mismatches fail before user commands. Caller argv/stdin and native `--pin-tab` / `--no-pin-tab` preferences remain unchanged. Local commands, live `get url`, explicit HTTP `read <url>` (including its flags), URL `a11y`/`vitals`/`web-vitals`, `diff url`, `window new`, URL-bearing recording commands, and explicit tab/navigation/`connect`/`state load` recovery do not require the old target; history back/forward/reload, `pushstate`, and page-content operations still do. The same classifier scans effective batch rows past non-page prefixes until a page dependency or explicit context change, without rewriting user rows or changing bail behavior. For `window new` and `diff url`, observe the resulting URL instead of retaining the old target or treating the requested second URL as redirect evidence. Fold only reached native batch rows when available, discard observations and refs from before those transitions, and let later successful snapshots rebuild refs even when another batch row fails. Retain an observed blank destination after either command instead of recovering the old page; if no final target is observed, use the existing unknown-target state. Caller batch arguments, stdin and bail behavior stay unchanged. Routine same-session commands avoid `tab list` preflights
176
179
  - 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
177
180
  - after successful standalone tab selection or `tab close`, read the now-active URL and fresh non-blank title—even when two tabs share a URL—before updating per-session page state because upstream selection/close payloads are not sufficient page-target evidence; retain an explicitly selected existing `about:blank` tab or a blank tab revealed by close instead of treating either as accidental drift
178
- - 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`), replace it with a persisted `page-transition` invalidation after any upstream-executed `record start` attempt (direct or batch; upstream swaps the session to a fresh active page before its already-active check, so failed starts count), a `record restart` with a URL operand, or WebMCP `invoke` / `result` / `cancel` (these page-provided tools can mutate, rerender, or navigate; when a spawned `batch` yields no parseable result rows, for example after a wrapper timeout, planned transition steps still record the invalidation), or after a failed non-batch transition command (`eval`, `back`, `forward`, `reload`, `connect`, `state load`, `tab` selection) whose live URL re-verification probe observed the page (a failed transition can still have mutated or replaced the document before throwing, so keeping the verified URL must not keep the prior refs; transcript replay preserves the persisted invalidation summary), and refuse page-scoped `@e…` argv before spawn when the active tab URL no longer matches the snapshot URL, when a ref id was never in that snapshot, when the snapshot state is invalidated, or when a `batch` step would reuse `@e…` on a guarded getter or mutation step after an earlier invalidating step (including `record start`, URL-bearing `record restart`, and WebMCP `invoke` / `result` / `cancel`) without a later `snapshot` step in the same plan; batch steps come from the source upstream actually executes (raw batch argument strings exclusively when any exist — filtering only the exact `--bail` token like upstream — stdin only otherwise, via `getUpstreamEffectiveBatchSteps` in `extensions/agent-browser/lib/orchestration/batch-stdin.ts`); the tab-pinned batch rewrite (which re-emits the caller's exact `--bail` token so fail-fast semantics survive the rewrite), artifact/recording preflight, batch screenshot path preparation (parent directories are created for effective raw rows too, without rewriting raw strings), and stale-ref echo args use that same selection so pinning and preflights cannot act on upstream-ignored stdin, while the pre-spawn state-policy validator deliberately keeps scanning parseable stdin alongside argv as a fail-closed content superset and treats stdin parse failures as fatal only when upstream would actually read stdin (its raw-token filter also uses the exact `--bail` token only). 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
181
+ - 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`), replace it with a persisted `page-transition` invalidation after any upstream-executed `record start` attempt (direct or batch; upstream swaps the session to a fresh active page before its already-active check, so failed starts count), a `record restart` with a URL operand, `window new`, `diff url`, or WebMCP `invoke` / `result` / `cancel` (these page-provided tools can mutate, rerender, or navigate; when a spawned `batch` yields no parseable result rows, for example after a wrapper timeout, planned transition steps still record the invalidation), or after a failed non-batch transition command (`eval`, `back`, `forward`, `reload`, `connect`, `state load`, `tab` selection) whose live URL re-verification probe observed the page (a failed transition can still have mutated or replaced the document before throwing, so keeping the verified URL must not keep the prior refs; transcript replay preserves the persisted invalidation summary), and refuse page-scoped `@e…` argv before spawn when the active tab URL no longer matches the snapshot URL, when a ref id was never in that snapshot, when the snapshot state is invalidated, or when a `batch` step would reuse `@e…` on a guarded getter or mutation step after an earlier invalidating step (including `record start`, URL-bearing `record restart`, and WebMCP `invoke` / `result` / `cancel`) without a later `snapshot` step in the same plan; batch steps come from the source upstream actually executes (raw batch argument strings exclusively when any exist — filtering only the exact `--bail` token like upstream — stdin only otherwise, via `getUpstreamEffectiveBatchSteps` in `extensions/agent-browser/lib/orchestration/batch-stdin.ts`); tab recovery (which leaves user argv/stdin and continue-on-error control flow unchanged), artifact/recording preflight, batch screenshot path preparation (parent directories are created for effective raw rows too, without rewriting raw strings), and stale-ref echo args use that same selection so pinning and preflights cannot act on upstream-ignored stdin, while the pre-spawn state-policy validator deliberately keeps scanning parseable stdin alongside argv as a fail-closed content superset and treats stdin parse failures as fatal only when upstream would actually read stdin (its raw-token filter also uses the exact `--bail` token only). 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
179
182
  - when a direct or batched WebMCP call returns `status: "pending"`, or `result` / `cancel` fails while that target is unknown, keep its tab target unknown and discard same-call snapshot evidence instead of treating an immediate post-dispatch URL probe as stable; `webmcp result` / `cancel`, `get url`, and explicit navigation remain available while unknown; replace the generic blocked snapshot action with `verify-page-target-after-pending-webmcp` (`get url`), and let a completed `batch --bail` use that verification before `snapshot -i` to re-establish both target and refs
180
- - 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
183
+ - 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 requires a unique role/name in the saved snapshot and the live candidates instead of taking a fresh pre-click snapshot that could recycle upstream refs. Duplicate-name refs pass through without a probe: their old ordinal is not target identity. The probe is intentionally skipped for CSS selector clicks, unresolved `find … click` locators, and `batch`/`job`/`qa` click steps
181
184
  - derive narrow prompt guards only for concrete evidence invariants: explicitly requested screenshot/recording output paths block browser close until the artifact manifest verifies those paths, while bare inbound attachment paths remain inputs. 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
182
185
  - reject direct and effective batch `scrollintoview text=...` / `scrollinto text=...` before dispatch because current upstream can falsely report success without movement, while leaving help forms untouched; return only native recovery (`find text ... hover` or fresh snapshot/ref), leaving CSS, XPath, and current-ref behavior upstream-owned
183
186
  - after successful `get text` on a qualifying non-ref CSS selector, optionally issue one read-only `eval --stdin` probe per selector when multiple DOM matches or a hidden first match with visible peers could misread tabbed or off-screen content; simple id selectors and sensitive-looking literals skip this probe. Merge `details.selectorTextVisibility` / `selectorTextVisibilityAll`, visible warning lines, and `inspect-visible-text-candidates*` next actions as documented in [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#details) and `RQ-0074` in [`SUPPORT_MATRIX.md`](SUPPORT_MATRIX.md)
184
- - for local Unix launches, set a short private socket directory so extension-generated session names do not fail on the upstream Unix socket-path length limit; require the selected path to be absolute, owned by the current uid, mode `0700`, under trusted non-replaceable ancestry, and free of symlink, foreign-owner, or special planted entries; reject pre-existing unsafe modes instead of repairing them, then recheck before spawn. Android/Termux uses `/data/data/<package>/piab`, treats the owner-only app-data directory as the trust anchor, permits the app's matching private uid/gid ancestry, compacts generated managed identities to one 80-bit digest so ordinary namespace plus fresh-session paths remain within the limit, and places policy-lock coordination under `os.tmpdir()` because Android `/tmp` is shell-owned and inaccessible
187
+ - for local Unix launches, set a short private socket directory so extension-generated session names do not fail on the upstream Unix socket-path length limit; require the selected path to be absolute, owned by the current uid, mode `0700`, under checked ancestry, and free of symlink, foreign-owner, or special planted entries; reject pre-existing unsafe modes instead of repairing them, then recheck before spawn. The actual filesystem root `/` is supplied by the trusted operating environment: a directory without group/other write bits is accepted regardless of its reported owner, which can be unmapped in a Linux user namespace. Existing root-owned sticky-directory acceptance is unchanged. This boundary does not protect against whoever controls the root filesystem. Every non-root ancestor still needs trusted ownership and permissions, including the destination ancestry of root-owned aliases; a matching overflow UID is not trusted. Android/Termux uses `/data/data/<package>/piab`, treats the owner-only app-data directory as the trust anchor, permits the app's matching private uid/gid ancestry, compacts generated managed identities to one 80-bit digest so ordinary namespace plus fresh-session paths remain within the limit, and places policy-lock coordination under `os.tmpdir()` because Android `/tmp` is shell-owned and inaccessible
185
188
  - keep wrapper-spawned upstream CLI calls bounded by clamping `AGENT_BROWSER_DEFAULT_TIMEOUT` to the upstream documented 25-second default while deriving a longer subprocess watchdog for explicit long `wait <ms>` / `wait --timeout <ms>`, read, and WebMCP calls from the effective direct or raw-argument-else-stdin batch steps; dialog commands, likely dialog-trigger clicks/taps/finds, and `eval --stdin` snippets that look like alert/confirm/prompt/dialog triggers use shorter wrapper subprocess budgets so blocking JavaScript prompts surface recovery actions before the full default watchdog. Timeout recovery removes standalone snapshots when the target is unknown and emits one executable session-scoped `batch --bail` (`get url`, then `snapshot -i`); blocking-dialog status/accept/dismiss remains allowed under the same unknown-target guard
186
189
 
187
190
  This is primarily about ownership clarity and avoiding surprise, not adding a heavy safety wrapper. If the extension invented the session, the extension should own its lifecycle without breaking reload, resume, or branch-tree semantics. If the caller explicitly chose the upstream session model, the extension should stay out of the way.
@@ -333,11 +333,11 @@ For desktop, contenteditable, or host-controlled rich inputs, treat a semantic `
333
333
 
334
334
  Do not assume Playwright selector dialects such as `text=Close` or `button:has-text('Close')` are supported wrapper syntax. In particular, current upstream can report successful `scrollintoview text=...` without moving the page, so the wrapper rejects that form before dispatch—directly or in an effective raw/stdin batch row—and shows executable `find text <label> hover` plus snapshot/ref recovery payloads in visible failure text and `details.nextActions`. `scrollintoview ... --help` and `-h` remain native help calls. Use `scrollintoview` with CSS, `xpath=...`, or a current `@e…` ref; use `find` for semantic text targets.
335
335
 
336
- Treat `@e…` refs as page-scoped. After a successful `snapshot`, the wrapper records the latest refs and page target for that session; getter or mutation ref commands such as `get text @e4`, `click @e4`, `select @e5 chocolate`, or batch steps with old refs fail with `failureCategory: "stale-ref"` when the page target changed or the ref is absent from the latest same-page snapshot. If a session `snapshot -i` fails with `No active page`, the wrapper invalidates prior refs for that session; later mutation-prone `@e…` calls fail before upstream until a successful fresh `snapshot -i` records refs again. Inside `batch` stdin JSON, the wrapper also walks steps in order before spawn: steps whose first token can navigate or mutate set a latch; a later step whose first token is `snapshot` clears that latch for following rows; guarded steps that still mention `@e…` after an uncleared latch fail with the same `stale-ref` bucket without launching upstream. Same-snapshot form fills and native form-control steps are allowed before a click or submit step, so `fill`, `check`/`uncheck` checkbox or radio refs, checkbox/radio `click`/`tap` refs, `select` combobox refs, then a final submit `click` can run from one snapshot. Split dynamic or autosubmit forms with a fresh snapshot if a control interaction rerenders the targets. Follow the `refresh-interactive-refs` next action (it includes `--session <name>` when needed) and prefer stable `find` or `semanticAction` locators when navigation or rerendering is likely. Contract detail: [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#details) (`refSnapshot`, `refSnapshotInvalidation`).
336
+ Treat `@eN`, `eN`, and `ref=eN` selector refs as page-scoped. Ref-looking fill/type text, select values, paths, and keyboard/mouse data remain literal. After a successful `snapshot`, the wrapper records the latest refs and page target for that session; getter or mutation ref commands such as `get text @e4`, `click @e4`, `select @e5 chocolate`, or batch steps with old refs fail with `failureCategory: "stale-ref"` when the page target changed or the ref is absent from the latest same-page snapshot. If a session `snapshot -i` fails with `No active page`, the wrapper invalidates prior refs for that session; later mutation-prone `@e…` calls fail before upstream until a successful fresh `snapshot -i` records refs again. Inside `batch` stdin JSON, the wrapper also walks steps in order before spawn: steps whose first token can navigate or mutate set a latch; a later step whose first token is `snapshot` clears that latch for following rows; guarded steps that still mention `@e…` after an uncleared latch fail with the same `stale-ref` bucket without launching upstream. Same-snapshot form fills and native form-control steps are allowed before a click or submit step, so `fill`, `check`/`uncheck` checkbox or radio refs, checkbox/radio `click`/`tap` refs, `select` combobox refs, then a final submit `click` can run from one snapshot. Split dynamic or autosubmit forms with a fresh snapshot if a control interaction rerenders the targets. Follow the `refresh-interactive-refs` next action (it includes `--session <name>` when needed) and prefer stable `find` or `semanticAction` locators when navigation or rerendering is likely. Contract detail: [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#details) (`refSnapshot`, `refSnapshotInvalidation`).
337
337
 
338
- A successful `click` result means upstream reported a target, not that the app definitely handled the event. For top-level non-Electron direct clicks on `xpath=` targets and eligible current `@e…` refs, the wrapper installs a bounded target-specific DOM-event probe when it can; when upstream reports success but no trusted event reaches the resolved target, it fails the tool and exposes `details.clickDispatch` plus a `Click dispatch diagnostic` line with explicit retry/inspect next actions (no in-page click replay). 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. Direct `@e…` click probes are role-gated to current snapshot refs whose accessible role is `button`, `checkbox`, `menuitem`, `radio`, `switch`, or `tab`; duplicate names use snapshot order. If the probe evidence shows the target is outside a nested scroll container or viewport, `details.clickDispatch.scrollContainer` and `scroll-target-into-view-after-dispatch-miss` point to `scrollintoview <target>` before retry. When the workflow depends on a mutation, use `details.pageChangeSummary`, a wait, URL/text extraction, or a fresh `snapshot -i` before trusting the state; if nothing changed, retry with a current visible ref or stable selector and report the workflow issue. For static local fixtures or debugging where the user explicitly accepts scripted activation, `eval --stdin` can call `document.querySelector(...).click()` to exercise inline handlers and app code; treat that as an untrusted programmatic event, not as evidence that CDP/user-like clicking works. Respect explicit user stop boundaries yourself: if the user says to stop before a final order, post, purchase, or submit action, gather evidence from that page and do not click the final action or use scripted activation to bypass the stop. The wrapper does not infer broad business intent from prompt text; `details.promptGuard` is reserved for concrete artifact-before-close checks. `press`, `key`, `keydown`, and `keyup` accept exactly one key token; focus or click the target first, then run `press Enter` or another single-key command.
338
+ A successful `click` result means upstream reported a target, not that the app definitely handled the event. For top-level non-Electron direct clicks on `xpath=` targets and eligible current `@e…` refs, the wrapper installs a bounded target-specific DOM-event probe when it can; when upstream reports success but no trusted event reaches the resolved target, it fails the tool and exposes `details.clickDispatch` plus a `Click dispatch diagnostic` line with explicit retry/inspect next actions (no in-page click replay). 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. Direct `@e…` click probes are role-gated to current snapshot refs whose accessible role is `button`, `checkbox`, `menuitem`, `radio`, `switch`, or `tab`, with a unique role/name in both the saved snapshot and the live candidates. Duplicate-name refs pass through without a probe because their old ordinal does not prove target identity. If the probe evidence shows the target is outside a nested scroll container or viewport, `details.clickDispatch.scrollContainer` and `scroll-target-into-view-after-dispatch-miss` point to `scrollintoview <target>` before retry. When the workflow depends on a mutation, use `details.pageChangeSummary`, a wait, URL/text extraction, or a fresh `snapshot -i` before trusting the state; if nothing changed, retry with a current visible ref or stable selector and report the workflow issue. For static local fixtures or debugging where the user explicitly accepts scripted activation, `eval --stdin` can call `document.querySelector(...).click()` to exercise inline handlers and app code; treat that as an untrusted programmatic event, not as evidence that CDP/user-like clicking works. Respect explicit user stop boundaries yourself: if the user says to stop before a final order, post, purchase, or submit action, gather evidence from that page and do not click the final action or use scripted activation to bypass the stop. The wrapper does not infer broad business intent from prompt text; `details.promptGuard` is reserved for concrete artifact-before-close checks. `press`, `key`, `keydown`, and `keyup` accept exactly one key token; focus or click the target first, then run `press Enter` or another single-key command.
339
339
 
340
- Successful `snapshot -i` results can also surface `Possible overlay blockers` when their own refs already show dialog/alertdialog context plus close/dismiss controls, so agents can detect likely obstruction before clicking. When a **top-level** `@e…`/`ref=` click succeeds (not a `click` hidden inside a `batch`/`job` tool call—the unified command must be `click`), the upstream payload includes `data.clicked`, no `details.clickDispatch` diagnostic fired for the same result, and the wrapper sees `details.navigationSummary.url` unchanged after the same normalization it uses for ref guards (**`#fragment` ignored**), it may run one extra `snapshot -i` and surface `Possible overlay blockers` plus `details.overlayBlockers` (`candidates`, `summary`, and a `snapshot` map that can refresh `refSnapshot`) when that snapshot shows strong modal context (`dialog` / `alertdialog`) **and** up to three close/dismiss-like controls; page-wide words such as privacy, sign in, or banner alone do not trigger it. The URL check compares the session’s prior pinned tab target to `details.navigationSummary.url`. CSS selector clicks do not run this overlay probe. The diagnostic is skipped if the wrapper already applied tab-focus correction or about-blank recovery on that result. Appended `inspect-overlay-state` / `try-overlay-blocker-candidate-*` entries in `details.nextActions` preserve namespace/session context (`--namespace <namespace> --session <name>` when namespaced, otherwise `--session <name>` when the session is named), same as other session-scoped follow-ups. Treat `inspect-overlay-state` as the safe first follow-up; only use a `try-overlay-blocker-candidate-*` next action when the candidate is clearly the control you intend to close.
340
+ Successful `snapshot -i` results can also surface `Possible overlay blockers` when their own refs already show dialog/alertdialog context plus close/dismiss controls, so agents can detect likely obstruction before clicking. When a **top-level** `@e…`/`ref=` click succeeds (not a `click` hidden inside a `batch`/`job` tool call—the unified command must be `click`), the upstream payload includes `data.clicked`, no `details.clickDispatch` diagnostic fired for the same result, and the wrapper sees `details.navigationSummary.url` unchanged after the same normalization it uses for ref guards (**`#fragment` ignored**), it may run one extra `snapshot -i` and surface `Possible overlay blockers` plus `details.overlayBlockers` (`candidates`, `summary`, and a `snapshot` map that can refresh `refSnapshot`) when that snapshot shows strong modal context (`dialog` / `alertdialog`) **and** up to three close/dismiss-like controls; page-wide words such as privacy, sign in, or banner alone do not trigger it. The URL check compares the session’s prior pinned tab target to `details.navigationSummary.url`. CSS selector clicks do not run this overlay probe. The diagnostic is skipped if the wrapper already applied tab-focus correction or about-blank recovery on that result. Appended `inspect-overlay-state` / `try-overlay-blocker-candidate-*` entries in `details.nextActions` preserve namespace/session context (`--namespace <namespace> --session <name>` when namespaced, otherwise `--session <name>` when the session is named), same as other session-scoped follow-ups. Treat `inspect-overlay-state` as the safe first follow-up; only use a `try-overlay-blocker-candidate-*` next action when the candidate is clearly the control you intend to close. A click that upstream rejects because another element covers the target's click point (`is covered by` … `at its click point`) remains `failureCategory: "upstream-error"`, but receives the same session-aware `inspect-overlay-state` snapshot action. This includes direct `click`, `semanticAction` click, raw `find` clicks (including `nth` and omitted default-click actions), and failed `batch`/`job` rows. That failed-click path does not retry the original click or synthesize a dismiss candidate before a fresh snapshot provides evidence; it is separate from silent input-dispatch failures.
341
341
 
342
342
  ### Extract page data
343
343
 
@@ -370,10 +370,12 @@ On tabbed or hidden-DOM pages, `get text <selector>` reads the upstream-selected
370
370
  { "args": ["batch"], "stdin": "[[\"open\",\"https://example.com\"],[\"snapshot\",\"-i\"]]" }
371
371
  ```
372
372
 
373
- Use `batch --bail` when later steps should stop after the first failed command.
373
+ Use exact `batch --bail` when later steps should stop after the first failed command; omit it to continue after errors. `--bail=true` / `--bail=false` are unsupported: upstream treats them as raw command strings and ignores stdin. The wrapper returns shape guidance without running that ignored stdin. Tab recovery does not change caller flags, literal operands, or batch control flow; a failed wrapper tab selection stops before user commands. Both pinned and unpinned mixed failures retain per-step results and failure counts.
374
374
 
375
375
  For short constrained flows, use top-level `job` instead of hand-writing `batch` stdin. Supported job steps are `open`, `click`, `fill`, `type`, `select`, `wait`, `assertText`, `assertUrl`, `waitForDownload`, `snapshot`, and `screenshot`. `open` can include `loadState: "domcontentloaded" | "load" | "networkidle"` to insert a `wait --load …` row immediately after navigation before the next click/read step. `click` and `fill` accept either a stable `selector` or the same semantic locator fields as top-level `semanticAction` (`locator`, plus `role`/`name` or `value` as appropriate) and compile locator steps to upstream `find` argv. `type` focuses an optional selector, sends text through upstream keyboard typing, can insert `wait` rows via `delayMs` for human-paced input, and can append a final `press` key such as `Enter`; delayed typing is capped at 200 characters per step, and generated per-character rows are compacted in model-visible batch text while remaining available in `details.batchSteps`. `select` requires `selector` plus `value` or `values`, and compiles to upstream `select <selector> <value...>`. By default the wrapper compiles steps to upstream `batch --bail` so a failed setup/fill/assertion step stops later mutating clicks; set `failFast: false` only when you explicitly need continue-after-error diagnostics and those later steps remain safe if an earlier navigation fails; otherwise keep fail-fast or split navigation from content. The wrapper records `details.compiledJob.steps[]` plus `details.compiledJob.failFast`. There is still no separate first-class catalog of reusable named browser recipes above `job`, the `qa` preset, and raw `batch`; see [`ARCHITECTURE.md`](ARCHITECTURE.md#no-reusable-recipe-layer-yet) for the closed `RQ-0068` decision and revisit bar.
376
376
 
377
+ `assertText` takes only `text`, not selector or locator fields. Clicks can stale subsequent `@refs`; split the job and take a fresh snapshot before using them.
378
+
377
379
  **Job navigation is explicit.** A `click` step (or other navigation-prone interaction) does not prove the next page loaded. The wrapper does not auto-insert `assertUrl` or `assertText` after clicks inside `job`; add those steps yourself with the exact URL, a `*` / `**` glob-style URL pattern, or on-page text you expect, especially after forms, checkout, tabs, or submit buttons, before screenshots or later steps. Exact and glob-style `assertUrl` values compile to `wait --url` unchanged, including query strings and literal `?`; upstream `agent-browser 0.31.1` matches `*` / `**` patterns against the full active URL. Do not put a whole dynamic checkout into one long job: split around login, sorting/cart mutations, checkout navigation, and final evidence capture so refs and app state can be rechecked between phases.
378
380
 
379
381
  ```json
@@ -459,7 +461,9 @@ Typical lifecycle:
459
461
  { "electron": { "action": "cleanup", "launchId": "electron-…" } }
460
462
  ```
461
463
 
462
- `electron.status` and `electron.cleanup` take either `launchId`, **`all: true`** (literal boolean) to walk every wrapper-tracked launch in one call, or neither when exactly one active launch exists—never both `launchId` and `all`. They can target the current branch-visible launch plus still-owned off-branch launch records by `launchId`; default no-arg calls are intentionally ambiguous when more than one active launch is owned. `/reload` preserves the current branch-visible active Electron launch and its isolated temp `userDataDir` for continuity, and cleans off-branch owned Electron launches; if cleanup is partial and skips or fails profile removal, the generic temp sweep preserves that `userDataDir` across reload, quit, later temp cleanup, process exit, and stale temp-root pruning after restart. For `electron.launch`, `timeoutMs` bounds host CDP readiness with a **15s** default and **120s** cap in `extensions/agent-browser/lib/electron/launch.ts`. Optional `timeoutMs` on **`status`** applies to managed-session `get url`, then `get title` reads (localhost CDP probes stay on a short fixed fetch budget). On **`cleanup`**, it caps upstream `close` **and** host teardown (process exit, debug-port idle check, isolated profile removal); when omitted it follows the implicit session close default (**5s** unless `PI_AGENT_BROWSER_IMPLICIT_SESSION_CLOSE_TIMEOUT_MS` overrides). A successful managed-session close step retires that wrapper-managed session even when host process/profile cleanup remains partial. On **`probe`**, it bounds each underlying upstream read subprocess—omit it to use the normal tool subprocess default, or raise it on slow desktops.
464
+ `electron.status` and `electron.cleanup` take either `launchId`, **`all: true`** (literal boolean) to walk every active wrapper-tracked launch (including dead, failed, or partial records, but excluding cleaned records), or neither when exactly one active launch exists—never both `launchId` and `all`. They can target the current branch-visible launch plus still-owned off-branch launch records by `launchId`; default no-arg calls are intentionally ambiguous when more than one active launch is owned. `/reload` preserves the current branch-visible active Electron launch and its isolated temp `userDataDir` for continuity, and cleans off-branch owned Electron launches; if cleanup is partial and skips or fails profile removal, the generic temp sweep preserves that `userDataDir` across reload, quit, later temp cleanup, process exit, and stale temp-root pruning after restart. `electron.list` has no configurable timeout and rejects both top-level and nested `timeoutMs`. For `electron.launch`, nested `timeoutMs` sets host CDP readiness polling to a **15s** default and **120s** cap after target discovery; upstream attach and handoff use separate subprocess budgets. Optional `timeoutMs` on **`status`** applies to managed-session `get url`, then `get title` reads (localhost CDP probes stay on a short fixed fetch budget). On **`cleanup`**, it is applied separately to upstream `close` and the initial host process-exit wait, not to the entire teardown; debug-port checks have fixed fetch budgets and profile removal has no configurable deadline; when omitted it follows the implicit session close default (**5s** unless `PI_AGENT_BROWSER_IMPLICIT_SESSION_CLOSE_TIMEOUT_MS` overrides). A successful managed-session close step retires that wrapper-managed session even when host process/profile cleanup remains partial. On **`probe`**, it bounds each underlying upstream read subprocess—omit it to use the normal tool subprocess default, or raise it on slow desktops.
465
+
466
+ Explicit-ID `electron.status` labels a cleaned record as historical while measuring PID/port liveness independently. `details.electron.statuses[].userDataDirState` freshly reports the tracked profile path as `present`, `absent` (only ENOENT), or `unknown` (other native `lstat` errors); dangling symlinks are present. This is not an audit of all app residue and does not change stored launch records or cleanup ownership.
463
467
 
464
468
  `launch.handoff` defaults to `"snapshot"`, which attaches through upstream `connect`, lists targets, and captures a current `snapshot -i` in one call. Snapshot handoff retries briefly when the first Electron snapshot has no refs; if it still reports no refs, run `snapshot -i` once more before assuming the app is blank. Use `handoff: "tabs"` as the safer diagnostic starting point when you only need target discovery and do not want to snapshot app content yet, or `handoff: "connect"` when you want to attach first and run your own follow-up commands. `targetType` defaults to `"page"`; use `"webview"` or `"any"` for apps that expose useful webviews. When a matching CDP target exposes a WebSocket URL, launch connects to that target; otherwise it falls back to the browser port.
465
469
 
@@ -533,15 +537,15 @@ Prefer `download <selector> <path>` when the target element itself is the downlo
533
537
 
534
538
  For evidence-only screenshots, QA captures, or audit artifacts, save to an explicit path and branch on `details.artifactVerification` plus `details.artifacts` before reporting PASS/FAIL. Inline image attachments are optional convenience when size limits allow; do not require vision review unless the user asked for visual inspection.
535
539
 
536
- Wrapper result rendering is metadata-first for saved files. An artifact-producing command fails as `artifact-missing` with artifact `status: "stale"` when the reported path's `mtimeMs` falls outside the command's bounded start/end window (with two seconds of filesystem precision tolerance), including a previous recording that `record restart` claims to finalize; clearly old or future-dated evidence is never accepted as a fresh capture. A batch, whether supplied through stdin arrays or argument command strings, must use distinct explicit artifact destinations; preflight canonicalizes existing path ancestry, compares existing file identities to catch hardlinks, and applies full Unicode plus platform case folding on macOS/Windows so aliases cannot satisfy another step's verification. The same preflight prevents `outputPath` from aliasing a same-call browser artifact, follows upstream's forward option consumption and final effective `-o` / `--output` for `diff screenshot`, and treats the optional path on `network har stop` as an artifact destination; upstream ignores positional paths on `network har start`. Artifact and lifecycle parsing first removes upstream global flags wherever they occur, so accepted forms such as `record --json start <path>` and `pdf --quiet <path>` cannot shift or bypass destination tracking. Screenshot destination parsing mirrors upstream's exact flag matching and `[selector] [path]` positional order: `--` is positional, `true` / `false` after screenshot-only `--full` / `-f` remain positional, extra positionals are ignored after the path slot, selector-prefixed (`.`, `#`, `@`) or uppercase-extension single arguments remain selectors, and lowercase image extensions or slash-bearing arguments are paths. The wrapper deliberately keeps its existing slash-bearing hidden-workspace path normalization (for example `.dogfood/run/foo.png`) before launch. `wait --download` is observational and may verify a download that completed just before the wait began, so it is exempt from the command-window mtime gate; an explicit wait destination, in long `--download <path>` or short `-d <path>` form (including after `--timeout`), still participates in active-recording reservation preflight; unsupported `--download=<path>` fails with split-argument guidance:
540
+ Wrapper result rendering is metadata-first for saved files. Image MIME types come from a bounded header read for PNG, JPEG, GIF and WebP, never from a filename suffix; missing, unreadable, unknown or truncated headers omit `mediaType`. This identifies a format, not full image validity. Inline screenshots use the same byte check and existing size limit, so a PNG saved as `.webm` still attaches as `image/png`; other artifact kinds are not auto-inlined. An artifact-producing command fails as `artifact-missing` with artifact `status: "stale"` when the reported path's `mtimeMs` falls outside the command's bounded start/end window (with two seconds of filesystem precision tolerance), including a previous recording that `record restart` claims to finalize; clearly old or future-dated evidence is never accepted as a fresh capture. A batch, whether supplied through stdin arrays or argument command strings, must use distinct explicit artifact destinations; preflight canonicalizes existing path ancestry, compares existing file identities to catch hardlinks, and applies full Unicode plus platform case folding on macOS/Windows so aliases cannot satisfy another step's verification. The same preflight prevents `outputPath` from aliasing a same-call browser artifact, follows upstream's forward option consumption and final effective `-o` / `--output` for `diff screenshot`, and treats the optional path on `network har stop` as an artifact destination; upstream ignores positional paths on `network har start`. Artifact and lifecycle parsing first removes upstream global flags wherever they occur, so accepted forms such as `record --json start <path>` and `pdf --quiet <path>` cannot shift or bypass destination tracking. Screenshot destination parsing mirrors upstream's exact flag matching and `[selector] [path]` positional order: `--` is positional, `true` / `false` after screenshot-only `--full` / `-f` remain positional, extra positionals are ignored after the path slot, selector-prefixed (`.`, `#`, `@`) or uppercase-extension single arguments remain selectors, and lowercase image extensions or slash-bearing arguments are paths. The wrapper deliberately keeps its existing slash-bearing hidden-workspace path normalization (for example `.dogfood/run/foo.png`) before launch. `wait --download` is observational and may verify a download that completed just before the wait began, so it is exempt from the command-window mtime gate; an explicit wait destination, in long `--download <path>` or short `-d <path>` form (including after `--timeout`), still participates in active-recording reservation preflight; unsupported `--download=<path>` fails with split-argument guidance:
537
541
  - screenshots return a saved-path summary, visible artifact metadata, structured `details.artifacts` metadata, and an inline image attachment when safe; the visible block includes artifact type, requested path, absolute path, existence, size, cwd, session, and repair/copy status when applicable
538
542
  - downloads, PDFs, `wait --download` files, `state save` state files, diff screenshot output images, traces, CPU profiles, completed WebM recordings from `record stop`, and path-bearing HAR captures return concise saved-path summaries plus structured `details.artifacts` metadata without inlining large files
539
- - `record start <path>` and `record restart <path>` report `successCategory: "artifact-pending"` and that output will be written on `record stop`; `record start` also states that upstream switches to a fresh active page for video capture, prior in-page DOM and JavaScript state does not carry over, and the next interaction should follow a fresh snapshot — the wrapper invalidates the session’s prior ref snapshot (direct calls and batch steps alike, and even when the start fails with `Recording already active`, because upstream swaps the page before that check), so old `@e…` refs fail as `stale-ref` until a fresh `snapshot -i` succeeds; `record restart <path> <url>` navigates the current page and invalidates refs the same way, while a plain `record restart <path>` keeps the current page and refs; `details.artifacts` / `details.artifactVerification` mark that future file as `pending` with `recordingState: "openRecording"` and `willExistOnStop: true`, and `details.nextActions` includes exact `stop-pending-recording` args. When `record restart` finalizes a previous wrapper-known recording, that file must exist and fall within the command mtime window before the result includes `Previous recording saved: …`; a missing or stale prior file fails as `artifact-missing` while the new recording remains visible as pending and the prior manifest row is retired. Within one Pi extension process, an unbounded transcript-backed index reserves active recording destinations independently of the bounded artifact manifest. Artifact lifecycle calls and result `outputPath` writes serialize around that global check; reservations use canonical namespace/session identity, survive manifest eviction and branch replay, and retire after direct, ordered nested-batch, fresh-replacement, script, Electron, or shutdown close; the newest pending row per identity is authoritative. Legacy batch replay retires a pending manifest only when the ordered close lifecycle leaves recording closed; a later successful browser reactivation plus `record start` keeps the new pending reservation. Lexical, hardlink, existing/dangling symlink, full Unicode-fold, and macOS/Windows case aliases are rejected, so `record restart` must use a distinct new path. Do not place `record start` or `record restart` after `close` / `quit` / `exit` in one batch: wrapper preflight rejects it because upstream can report success without starting a recording; split the close and recording into separate calls. A definitive `No recording in progress` stop failure, whether direct or inside a batch, retires stale reservation state at that ordered step; a later successful batch recording row opens its new pending path normally. Any success or failure result that still contains pending recording output includes `stop-pending-recording`. The target may not exist until recording stops, and upstream needs `ffmpeg` on `PATH` at stop time to encode the WebM. If `ffmpeg` is missing after a successful `record start` / `record restart`, the wrapper appends `Recording dependency warning: ffmpeg not found on PATH` and sets `details.recordingDependencyWarning` without blocking the upstream command.
543
+ - `record start <path>` and `record restart <path>` report `successCategory: "artifact-pending"` and that output will be written on `record stop`; dispatched `record start` and URL-bearing `record restart` attempts append one `Page state:` warning on success or failure, advising a fresh snapshot because in-page DOM and JavaScript state may not carry over; explicit `--json` puts that warning in `warnings`. Only reached batch rows qualify, not preflight failures, missing binaries, help calls or unconfirmed planned rows — the wrapper invalidates the session’s prior ref snapshot (direct calls and batch steps alike, and even when the start fails with `Recording already active`, because upstream swaps the page before that check), so old `@e…` refs fail as `stale-ref` until a fresh `snapshot -i` succeeds; `record restart <path> <url>` navigates the current page and invalidates refs the same way, while a plain `record restart <path>` keeps the current page and refs; `details.artifacts` / `details.artifactVerification` mark that future file as `pending` with `recordingState: "openRecording"` and `willExistOnStop: true`, and `details.nextActions` includes exact `stop-pending-recording` args. When `record restart` finalizes a previous wrapper-known recording, that file must exist and fall within the command mtime window before the result includes `Previous recording saved: …`; a missing or stale prior file fails as `artifact-missing` while the new recording remains visible as pending and the prior manifest row is retired. Within one Pi extension process, an unbounded transcript-backed index reserves active recording destinations independently of the bounded artifact manifest. Artifact lifecycle calls and result `outputPath` writes serialize around that global check; reservations use canonical namespace/session identity, survive manifest eviction and branch replay, and retire after direct, ordered nested-batch, fresh-replacement, script, Electron, or shutdown close; the newest pending row per identity is authoritative. Legacy batch replay retires a pending manifest only when the ordered close lifecycle leaves recording closed; a later successful browser reactivation plus `record start` keeps the new pending reservation. Lexical, hardlink, existing/dangling symlink, full Unicode-fold, and macOS/Windows case aliases are rejected, so `record restart` must use a distinct new path. Do not place `record start` or `record restart` after `close` / `quit` / `exit` in one batch: wrapper preflight rejects it because upstream can report success without starting a recording; split the close and recording into separate calls. A definitive `No recording in progress` stop failure, whether direct or inside a batch, retires stale reservation state at that ordered step; a later successful batch recording row opens its new pending path normally. Any success or failure result that still contains pending recording output includes `stop-pending-recording`. The target may not exist until recording stops, and upstream needs `ffmpeg` on `PATH` at stop time to encode the WebM. If `ffmpeg` is missing after a successful `record start` / `record restart`, the wrapper appends `Recording dependency warning: ffmpeg not found on PATH` and sets `details.recordingDependencyWarning` without blocking the upstream command.
540
544
  - `batch` keeps each step's artifacts in `details.batchSteps[].artifacts`; top-level `details.artifacts` and `details.artifactManifest` coalesce an earlier pending recording into the later saved, missing, or stale terminal result for the same namespace/session identity; a successful later close marks an unfinalized pending recording `missing` / `close-abandoned`, removes its stop action, and resets earlier ref/page/network-route batch state; a later successful `record stop` replaces that intermediate abandoned row with its verified saved artifact, and later rows—including failed rows—whose lifecycle reports a browser launch may rebuild state without triggering stale pre-close `about:blank` recovery; failed-step `batchSteps[]` retains only the bounded `lifecycle.effectiveLaunch.browserLaunched` boolean for replay, explicitly non-launching diagnostics leave the close terminal, missing lifecycle evidence remains conservatively active even on the first managed call, every successful close clears wrapper trace/profiler ownership before ordered later successful rows can rebuild it, namespace-scoped `close --all` clears all matching managed/attached/page/ref/route/trace/recording ownership, and any later same-session failure before recording stops keeps exact `stop-pending-recording` args alongside its normal recovery
541
545
 
542
546
  `diff screenshot` follows the file-artifact path above for the **diff** image: model-visible text and `details.artifacts` focus on that output, while baseline paths stay out of the artifact summary block, and Pi does **not** auto-inline the diff the way it inlines trusted `screenshot` captures. `state load` may print the loaded path in prose but does not add a saved-file artifact entry the way `state save` does.
543
547
 
544
- For screenshot paths under dot-directories such as `.dogfood/run/foo.png`, the wrapper normalizes the requested path to an absolute path before invoking upstream `agent-browser`, verifies the requested file exists, and repairs from an upstream temp screenshot when possible. For direct artifact commands and batch artifact steps (`download`, `pdf`, `screenshot`, `state save`, and `wait --download`), the wrapper creates missing parent directories before launch. The requested path remains visible as `Requested path`, while `Absolute path` shows the actual on-disk location.
548
+ For screenshot paths under dot-directories such as `.dogfood/run/foo.png`, the wrapper normalizes the requested path to an absolute path before invoking upstream `agent-browser`, verifies the requested file exists, and repairs from an upstream temp screenshot when possible. For direct artifact commands and batch artifact steps (`download`, `pdf`, `screenshot`, `state save`, and `wait --download`), the wrapper creates missing parent directories before launch. A parent-directory failure returns `validation-error` with the attempted directory and `verify-artifact-path` guidance before the browser command runs. Use **absolute paths in raw batch artifact rows**: raw strings stay unchanged, and the daemon's working directory may differ from Pi's. Screenshot path normalization still applies only to direct calls and stdin rows. Known caller paths appear as `Requested path`; `Absolute path` is the resolved location checked on disk, and `Reported path` exposes a differing screenshot report (including a canonical `/private/tmp` alias) through the existing `tempPath` metadata. No extra canonicalization rewrites upstream arguments.
545
549
 
546
550
  For annotated screenshots in `batch`, put `--annotate` in top-level args instead of inside the screenshot step:
547
551
 
@@ -664,7 +668,7 @@ Skill-source debugging note: upstream honors `AGENT_BROWSER_SKILLS_DIR` as an ov
664
668
  | `click <sel>` | Click an element or `@ref`. |
665
669
  | `click <sel> --new-tab` | Click a link/control while requesting a new tab. |
666
670
  | `dblclick <sel>` | Double-click an element. |
667
- | `type <sel> <text>` | Type into an element. |
671
+ | `type <sel> <text>` | Type into an element; both selector and text are required. For the focused element without a selector, use `keyboard type <text>`. |
668
672
  | `fill <sel> <text>` | Clear and fill an element. |
669
673
  | `press <key>` | Press a key such as `Enter`, `Tab`, or `Control+a`. `key <key>` is the upstream alias. |
670
674
  | `key <key>` | Alias for `press <key>`. |
@@ -697,7 +701,7 @@ Skill-source debugging note: upstream honors `AGENT_BROWSER_SKILLS_DIR` as an ov
697
701
 
698
702
  On dashboards and other apps with nested scroll containers, `scroll <dir> [px]` can miss because a page-level wheel does not move the document or the intended pane. Without startup-scoped launch flags, the wrapper first applies ordinary `scroll <up|down|left|right> [px|percent]` directly to `document.scrollingElement` with smooth scrolling temporarily disabled; successful movement reports `details.scrollPage`. If the document cannot move, it falls back to upstream wheel behavior. For large fallback calls on an existing or fresh managed session, the wrapper samples viewport and prominent scroll-container positions before and after the command; when nothing changes it reclassifies the nominal upstream success as `failureCategory: "upstream-error"`, prepends `Scroll completed with no observed movement`, appends `Scroll diagnostic: no observed scroll movement`, exposes `details.scrollNoop`, marks `details.data.scrolled: false`, and adds exact `details.nextActions` for a fresh `snapshot -i` and screenshot. Explicit CSS-container calls `scroll <selector> <up|down|left|right> [px|percent]` remain wrapper-handled and report `details.scrollContainer`; `scroll to end` / `scroll to top` report `details.scrollPage`. Calls with startup-scoped flags skip all helper shims so the requested launch configuration runs first. Use these paths before repeating page scrolls; when you need a specific element, prefer `scrollintoview <@ref>` or target the actual scrollable region. Do not pass `text=...` to `scrollintoview`: the wrapper rejects that upstream false-success path and returns `scroll-semantic-text-target` (`find text ... hover`) plus `refresh-refs-for-scroll-target` (`snapshot -i`) actions.
699
703
 
700
- Comboboxes vary by app. For native `<select>` controls, prefer raw `select <selector> <value...>`, direct `semanticAction: { action: "select", selector, value|values }`, active-session semantic role/name or label select, or a `job` `select` step instead of clicking option refs; native option refs can be non-boxed in CDP and fail before a real selection. A `click` or `semanticAction` role/name click may focus a searchable custom combobox without opening its option list. For explicit combobox-targeted actions such as `semanticAction` role `combobox`, the wrapper checks whether a combobox-like element is focused, has explicit `aria-expanded` state, and has no visible listbox/options open; this still applies when the semantic action first resolves to a current visible `@ref` before execution. When that happens it appends `Combobox diagnostic: focused combobox did not expose visible options`, exposes `details.comboboxFocus`, and adds exact `details.nextActions` for a fresh `snapshot -i`, `press ArrowDown`, and `press Enter`. Use those instead of assuming click alone expanded the control; reserve visible option refs for custom comboboxes after a fresh snapshot shows the intended option.
704
+ Comboboxes vary by app. For native `<select>` controls, prefer raw `select <selector> <value...>`, direct `semanticAction: { action: "select", selector, value|values }`, active-session semantic role/name or label select, or a `job` `select` step instead of clicking option refs; native option refs can be non-boxed in CDP and fail before a real selection. A `click` or `semanticAction` role/name click may focus a searchable custom combobox without opening its option list. For explicit combobox-targeted actions such as `semanticAction` role `combobox`, the wrapper checks whether a combobox-like element is focused, has explicit `aria-expanded` state, and has no visible listbox/options open; this still applies when the semantic action first resolves to a current visible `@ref` before execution. When that happens it appends `Combobox diagnostic: focused combobox did not expose visible options`, exposes `details.comboboxFocus`, and adds exact `details.nextActions` for a fresh `snapshot -i`, `press ArrowDown`, and `press Enter`. Use those instead of assuming click alone expanded the control. To search the focused input, use `keyboard type <text>`; raw `type` requires both a selector and text. Reserve visible option refs for custom comboboxes after a fresh snapshot shows the intended option.
701
705
 
702
706
  ### Navigation
703
707
 
@@ -726,10 +730,12 @@ Comboboxes vary by app. For native `<select>` controls, prefer raw `select <sele
726
730
  | `dialog accept [text]` | Accept an alert, confirm, or prompt dialog, optionally supplying prompt text. |
727
731
  | `dialog dismiss` | Dismiss or cancel the current dialog. |
728
732
  | `dialog status` | Check whether a dialog is pending. |
729
- | `window new` | Open a new browser window. |
733
+ | `window new` | Open and activate a new blank window. The wrapper keeps that intentional `about:blank` target rather than selecting the old tab. Old refs are invalid; take a fresh snapshot before further interaction. |
730
734
  | `close` | Close the current browser session. |
731
735
  | `close --all` | Close every session. |
732
736
 
737
+ A canceled cold-session reopen returns an aborted result with its exact session identity and the consumed reopen marker once the CLI starts. Reload does not repeat that navigation. Cancellation before the attempt leaves the remembered URL pending for the next current-page operation.
738
+
733
739
  <!-- agent-browser-playbook:start inspection -->
734
740
  <!-- Generated from extensions/agent-browser/lib/playbook.ts. Run `npm run docs -- playbook write` to update. -->
735
741
  Native inspection calls use the `agent_browser` tool shape, not shell-like direct-binary commands:
@@ -813,7 +819,7 @@ For dense pages, the wrapper also accepts `snapshot -i --search <text>` and `sna
813
819
  | Mode | Purpose |
814
820
  | --- | --- |
815
821
  | `wait <selector>` | Wait for an element to appear. |
816
- | `wait <ms>` | Wait for a fixed number of milliseconds. The native Pi wrapper now forwards long waits and derives a subprocess watchdog from the explicit wait duration when the caller does not provide top-level `timeoutMs`. |
822
+ | `wait <ms>` | Wait for a fixed number of milliseconds; the duration is positional, not `--time <ms>`. The native Pi wrapper now forwards long waits and derives a subprocess watchdog from the explicit wait duration when the caller does not provide top-level `timeoutMs`. |
817
823
  | `wait --url <pattern>` | Wait for the URL to match a pattern. On timeout the wrapper appends a `fresh-session-after-url-wait-timeout` next action (`sessionMode: "fresh"` + `open about:blank`, after the inspect action): if a preceding click or submit reported success but the page never navigated, upstream click dispatch may have silently missed, so replace about:blank with the target URL and replay the flow as one batch in a fresh session instead of retrying the wait. |
818
824
  | `wait --load <state>` | Wait for load state: `load`, `domcontentloaded`, or `networkidle`. |
819
825
  | `wait --fn <expression>` | Wait for a JavaScript expression to become truthy. |
@@ -829,7 +835,7 @@ Current upstream still does not parse `wait <selector> --state hidden` / `wait <
829
835
  | --- | --- |
830
836
  | `diff snapshot` | Compare current versus last snapshot. Use `diff snapshot --baseline <file> --selector <sel> --compact --depth <n>` when you need a saved baseline, scoped subtree, compact output, or depth bound. |
831
837
  | `diff screenshot --baseline` | Compare current screenshot versus a baseline image. Use `diff screenshot --baseline <file> --output <file> --threshold <0-1> --selector <sel> --full` when you need a saved diff image, threshold tuning, element scope, or full-page capture. |
832
- | `diff url <u1> <u2>` | Compare two pages. Use `diff url <u1> <u2> --screenshot --wait-until <strategy> --selector <sel> --compact --depth <n>` when you need screenshot comparison, navigation wait control, or scoped/compact snapshot comparison. |
838
+ | `diff url <u1> <u2>` | Navigate to both pages and compare them, leaving the second destination active. The wrapper observes the final URL, including redirects to `about:blank`, and invalidates old refs without recovering the old tab; direct and reached batch rows use the same rule. If the URL cannot be observed, run `get url` before taking a fresh snapshot. Use `diff url <u1> <u2> --screenshot --wait-until <strategy> --selector <sel> --compact --depth <n>` when you need screenshot comparison, navigation wait control, or scoped/compact snapshot comparison. |
833
839
  | `trace start`, `trace stop [path]` | Record a Chrome DevTools trace. |
834
840
  | `profiler start|stop [path]` | Record a Chrome DevTools profile. |
835
841
  | `record start <path> [url]` | Start WebM video recording; output is written on `record stop`. Requires `ffmpeg` on `PATH` for the final encode. |
@@ -853,6 +859,8 @@ Current upstream still does not parse `wait <selector> --state hidden` / `wait <
853
859
  | `pushstate <url>` | Perform SPA client-side navigation; detects Next.js router pushes and falls back to history navigation events. |
854
860
  | `removeinitscript <id>` | Remove an init script registered through upstream init-script mechanisms. |
855
861
 
862
+ Recording destinations are reserved within one Pi process, not across processes. Use unique paths for concurrent Pi processes: different explicit sessions can overwrite one file even when both `record stop` results are verified. Upstream’s same-session `record start` guard does not reserve the filename across other sessions.
863
+
856
864
  When these diagnostic commands are invoked through the native `agent_browser` tool, structured console, page-error, React, Web Vitals, and SPA outputs render as compact summaries when possible, with large outputs previewed and spilled instead of dumped into context. Large outputs are previewed with a `Full output path:` spill file instead of dumping the entire payload into context. Artifact-producing commands such as `network har stop`, `diff screenshot`, `trace stop`, `profiler stop`, and `record stop` report `details.artifacts[]` plus `details.artifactVerification`; `record start` / `record restart` are reported as pending until `record stop` completes. For video workflows, keep `ffmpeg` on `PATH` first; on macOS with Homebrew, `brew install ffmpeg` or `brew install ffmpeg-full` is sufficient. Successful `record start` / `record restart` results warn early with `details.recordingDependencyWarning` when the wrapper cannot find `ffmpeg`, so fix PATH before `record stop` instead of discovering the missing encoder after the capture. The README install section keeps the concise external-dependency list for maximal extension use.
857
865
 
858
866
  Long-running or lifecycle commands should be explicitly paired with cleanup calls: `stream enable` → `stream disable`, `dashboard start` → `dashboard stop`, `trace start` → `trace stop`, `profiler start` → `profiler stop`, and `record start` → `record stop`. The wrapper keeps each subprocess bounded by its normal timeout; it does not keep an interactive `chat` REPL open, so prefer `chat <message>` with `--model` or `AI_GATEWAY_MODEL` for single-shot AI use.
@@ -882,7 +890,7 @@ Long-running or lifecycle commands should be explicitly paired with cleanup call
882
890
  | `device list` | List available iOS simulators. Use with `-p ios` when exercising iOS provider flows. |
883
891
  | `install` | Install browser binaries. |
884
892
  | `install --with-deps` | Install browser binaries plus Linux system dependencies; exits nonzero when required libraries cannot be installed. |
885
- | `upgrade` | Upgrade `agent-browser` to the latest version. |
893
+ | `upgrade` | Upgrade `agent-browser` using its detected package manager. Native output is text, even with `--json`; the wrapper displays it with surrounding whitespace trimmed and normal redaction, and exposes it in `details.data`. Caller-requested `--json` stays a parseable result with the text in `data`. Nonzero exits, spawn failures, timeout and cancellation remain failures; failed upgrade stdout and stderr remain available as diagnostics. |
886
894
  | `doctor [--fix]` | Diagnose install issues and optionally auto-clean stale files. Use `doctor --offline --quick` for a fast local-only check and `doctor --json` for structured output. |
887
895
  | `plugin add <ref>` | Add a plugin from npm or GitHub (`<owner>/<repo>` or `@scope/<name>`); writes `agent-browser.json`. Flags such as `--name`, `--capability`, `--global`, and `--no-manifest` shape discovery. |
888
896
  | `plugin [list]` | List configured plugins (default subcommand); `{ "plugins": [...] }` is a successful sessionless result. |
@@ -1004,7 +1012,7 @@ Browser default config is conservative: it adds agent guidance for signed-in/acc
1004
1012
 
1005
1013
  - `--executable-path <path>`: custom Chromium-compatible browser executable, such as Brave/Edge/Arc/Vivaldi when upstream can launch that binary. Environment: `AGENT_BROWSER_EXECUTABLE_PATH`.
1006
1014
  - `--extension <path>`: load browser extensions; repeatable. Environment: `AGENT_BROWSER_EXTENSIONS`.
1007
- - `--args <args>`: browser launch args, comma or newline separated. Environment: `AGENT_BROWSER_ARGS`.
1015
+ - `--args <args>`: browser launch args, comma or newline separated. Chromium switches belong in this value, for example `{ "args": ["--args", "--no-sandbox", "open", "https://example.com"], "sessionMode": "fresh" }`. A bare `--no-sandbox` is diagnosed when it occupies the command slot (unknown command) or an `open` / `goto` / `navigate` option position (ignored by upstream); literal operands in other commands are left alone. For batches, put `--args` before `batch` in top-level `args`, not inside a row. Environment: `AGENT_BROWSER_ARGS`.
1008
1016
  - `--user-agent <ua>`: custom user agent. Environment: `AGENT_BROWSER_USER_AGENT`.
1009
1017
  - `--proxy <server>`: proxy server URL. Environments: `AGENT_BROWSER_PROXY`, `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`.
1010
1018
  - `--proxy-bypass <hosts>`: proxy bypass hosts. Environments: `AGENT_BROWSER_PROXY_BYPASS`, `NO_PROXY`.
@@ -1070,7 +1078,8 @@ Other useful environment variables include `AGENT_BROWSER_DEFAULT_TIMEOUT`, `AGE
1070
1078
  <!-- agent-browser-playbook:start wrapper-tab-recovery -->
1071
1079
  <!-- Generated from extensions/agent-browser/lib/playbook.ts. Run `npm run docs -- playbook write` to update. -->
1072
1080
  - After open/goto/navigate calls with --profile, --restore, --session-name, or --state, agent_browser best-effort re-selects the tab whose URL matches the returned page when restored tabs steal focus during launch or reconnect.
1073
- - After the wrapper observes tab-drift risk for a session (for example open correction, overlapping stale opens, or resumed session state), later active-tab commands best-effort pin that tab inside the same upstream invocation. Routine same-session commands are not preflighted with tab list just because a target tab or ref snapshot is known.
1081
+ - After confirmed shutdown of an automatically restored managed session, the wrapper retains its complete recorded URL, including the fragment, until the first current-page operation (including get url and reload). Non-page calls such as tab list or read <url> may start a daemon without fulfilling that reopen. The wrapper uses native open once, verifies the observed tab, and discards old refs/frame scope; it does not restore unsaved forms, JavaScript memory, or history. Explicit navigation, caller-owned/attached sessions, and restore-disabled sessions are not auto-reopened.
1082
+ - For a still-live browser after tab drift or resume, the wrapper verifies/selects the intended tab before ref/semantic helpers and page commands; failed selection stops the call without navigating. Local commands, read <url>, URL a11y/vitals, diff url, window new, and explicit tab/navigation/connection/state recovery do not require the prior tab. Batch checks follow effective rows past non-page prefixes and stop at explicit context changes, preserving caller argv/stdin and continue-on-error behavior. Same-tab reselection is avoided because it clears refs. Use exact batch --bail for fail-fast, not --bail=<value>. Routine same-session calls skip tab-list preflights.
1074
1083
  - 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.
1075
1084
  - 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.
1076
1085
  - If upstream reports tab_gone, the pinned bound tab is gone; use details.nextActions (tab list / tab new) instead of assuming another tab is yours.