pi-agent-browser-native 0.3.0 → 0.5.0

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 (73) hide show
  1. package/CHANGELOG.md +127 -0
  2. package/README.md +63 -20
  3. package/dist/extensions/agent-browser/index.js +787 -105
  4. package/dist/extensions/agent-browser/lib/argv-descriptor.js +35 -3
  5. package/dist/extensions/agent-browser/lib/argv-grammar.js +44 -2
  6. package/dist/extensions/agent-browser/lib/batch-lifecycle.js +71 -0
  7. package/dist/extensions/agent-browser/lib/command-policy.js +1 -1
  8. package/dist/extensions/agent-browser/lib/command-taxonomy.js +35 -2
  9. package/dist/extensions/agent-browser/lib/input-modes/job.js +61 -4
  10. package/dist/extensions/agent-browser/lib/input-modes/lookups.js +2 -2
  11. package/dist/extensions/agent-browser/lib/input-modes/params.js +22 -23
  12. package/dist/extensions/agent-browser/lib/input-modes/script.js +462 -0
  13. package/dist/extensions/agent-browser/lib/input-modes/semantic-action.js +51 -12
  14. package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +8 -0
  15. package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +3 -1
  16. package/dist/extensions/agent-browser/lib/managed-session-restore.js +26 -36
  17. package/dist/extensions/agent-browser/lib/managed-session-snapshots.js +2 -4
  18. package/dist/extensions/agent-browser/lib/managed-session-state-policy.js +47 -29
  19. package/dist/extensions/agent-browser/lib/managed-session-storage.js +50 -24
  20. package/dist/extensions/agent-browser/lib/orchestration/batch-stdin.js +26 -5
  21. package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.js +110 -30
  22. package/dist/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.js +2 -1
  23. package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +26 -25
  24. package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +17 -3
  25. package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +2 -1
  26. package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +6 -4
  27. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +129 -32
  28. package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +191 -55
  29. package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +57 -29
  30. package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +15 -11
  31. package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +36 -18
  32. package/dist/extensions/agent-browser/lib/orchestration/script-mode.js +299 -0
  33. package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +32 -10
  34. package/dist/extensions/agent-browser/lib/playbook.js +18 -15
  35. package/dist/extensions/agent-browser/lib/process-environment.js +14 -0
  36. package/dist/extensions/agent-browser/lib/process-identity.js +4 -4
  37. package/dist/extensions/agent-browser/lib/process.js +131 -41
  38. package/dist/extensions/agent-browser/lib/recording-reservations.js +183 -0
  39. package/dist/extensions/agent-browser/lib/results/action-recommendations.js +62 -5
  40. package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +62 -4
  41. package/dist/extensions/agent-browser/lib/results/categories.js +6 -1
  42. package/dist/extensions/agent-browser/lib/results/next-actions.js +19 -5
  43. package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +85 -38
  44. package/dist/extensions/agent-browser/lib/results/presentation/batch.js +66 -11
  45. package/dist/extensions/agent-browser/lib/results/presentation/common.js +18 -0
  46. package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +7 -1
  47. package/dist/extensions/agent-browser/lib/results/presentation/errors.js +2 -1
  48. package/dist/extensions/agent-browser/lib/results/presentation/navigation.js +26 -11
  49. package/dist/extensions/agent-browser/lib/results/presentation/registry.js +58 -13
  50. package/dist/extensions/agent-browser/lib/results/presentation/semantic-action.js +1 -10
  51. package/dist/extensions/agent-browser/lib/results/presentation.js +6 -3
  52. package/dist/extensions/agent-browser/lib/results/recovery-actions.js +2 -0
  53. package/dist/extensions/agent-browser/lib/results/selector-recovery.js +51 -8
  54. package/dist/extensions/agent-browser/lib/results/snapshot-high-value-controls.js +13 -7
  55. package/dist/extensions/agent-browser/lib/runtime.js +116 -39
  56. package/dist/extensions/agent-browser/lib/session-page-state.js +62 -10
  57. package/dist/extensions/agent-browser/lib/upstream-version.js +14 -0
  58. package/dist/extensions/agent-browser/script-worker.js +169 -0
  59. package/dist/scripts/agent-browser-target.mjs +3 -0
  60. package/docs/ARCHITECTURE.md +40 -21
  61. package/docs/COMMAND_REFERENCE.md +90 -35
  62. package/docs/RELEASE.md +3 -3
  63. package/docs/REQUIREMENTS.md +4 -2
  64. package/docs/SUPPORT_MATRIX.md +26 -19
  65. package/docs/TOOL_CONTRACT.md +93 -52
  66. package/package.json +3 -1
  67. package/platform-smoke.config.mjs +2 -2
  68. package/scripts/agent-browser-capability-baseline.mjs +24 -6
  69. package/scripts/agent-browser-target.mjs +3 -0
  70. package/scripts/build.mjs +41 -0
  71. package/scripts/doctor.mjs +7 -6
  72. package/scripts/platform-smoke/browser-dogfood-windows.ps1 +9 -3
  73. package/scripts/platform-smoke/targets.mjs +12 -6
@@ -1,3 +1,4 @@
1
+ const BATCH_STDIN_EXAMPLE = ' Example: { "args": ["batch"], "stdin": "[[\\"get\\",\\"title\\"],[\\"get\\",\\"url\\"]]" }';
1
2
  // Mirror upstream commands::shell_words_split so policy inspection sees the same argv.
2
3
  export function parseBatchCommandArgument(command) {
3
4
  const tokens = [];
@@ -36,20 +37,20 @@ export function parseBatchCommandArgument(command) {
36
37
  function validateUserBatchStep(step, index) {
37
38
  if (!Array.isArray(step)) {
38
39
  return {
39
- error: `agent_browser batch stdin step ${index} must be a non-empty array of string command tokens.`,
40
+ error: `agent_browser batch stdin step ${index} must be a non-empty array of string command tokens.${BATCH_STDIN_EXAMPLE}`,
40
41
  ok: false,
41
42
  };
42
43
  }
43
44
  if (step.length === 0) {
44
45
  return {
45
- error: `agent_browser batch stdin step ${index} must not be empty.`,
46
+ error: `agent_browser batch stdin step ${index} must not be empty.${BATCH_STDIN_EXAMPLE}`,
46
47
  ok: false,
47
48
  };
48
49
  }
49
50
  const invalidTokenIndex = step.findIndex((token) => typeof token !== "string");
50
51
  if (invalidTokenIndex !== -1) {
51
52
  return {
52
- error: `agent_browser batch stdin step ${index} token ${invalidTokenIndex} must be a string.`,
53
+ error: `agent_browser batch stdin step ${index} token ${invalidTokenIndex} must be a string.${BATCH_STDIN_EXAMPLE}`,
53
54
  ok: false,
54
55
  };
55
56
  }
@@ -62,13 +63,13 @@ export function parseBatchStdinJsonArray(stdin) {
62
63
  try {
63
64
  const parsed = JSON.parse(stdin);
64
65
  if (!Array.isArray(parsed)) {
65
- return { error: "agent_browser batch stdin must be a JSON array of command steps." };
66
+ return { error: `agent_browser batch stdin must be a JSON array of command steps.${BATCH_STDIN_EXAMPLE}` };
66
67
  }
67
68
  return { steps: parsed };
68
69
  }
69
70
  catch (error) {
70
71
  const message = error instanceof Error ? error.message : String(error);
71
- return { error: `agent_browser batch stdin could not be parsed as JSON: ${message}` };
72
+ return { error: `agent_browser batch stdin could not be parsed as JSON: ${message}.${BATCH_STDIN_EXAMPLE}` };
72
73
  }
73
74
  }
74
75
  export function parseUserBatchStdin(stdin) {
@@ -86,6 +87,26 @@ export function parseUserBatchStdin(stdin) {
86
87
  }
87
88
  return { steps };
88
89
  }
90
+ /**
91
+ * The batch steps upstream will actually execute: run_batch uses raw batch
92
+ * arguments exclusively when any exist and reads stdin only otherwise.
93
+ * Upstream filters only the exact `--bail` token, so an equals form such as
94
+ * `--bail=true` stays a raw command (an unknown-command row) and keeps stdin
95
+ * ignored.
96
+ */
97
+ export function getUpstreamEffectiveBatchSteps(commandTokens, stdin) {
98
+ if (commandTokens[0] !== "batch")
99
+ return [];
100
+ const argumentSteps = commandTokens.slice(1).flatMap((command) => {
101
+ if (command === "--bail")
102
+ return [];
103
+ const step = parseBatchCommandArgument(command).step;
104
+ return step ? [step] : [];
105
+ });
106
+ if (argumentSteps.length > 0)
107
+ return argumentSteps;
108
+ return parseUserBatchStdin(stdin).steps ?? [];
109
+ }
89
110
  export function parseValidBatchStepEntries(stdin) {
90
111
  const parsed = parseBatchStdinJsonArray(stdin);
91
112
  if (parsed.error || parsed.steps === undefined)
@@ -1,46 +1,126 @@
1
- import { extname, isAbsolute } from "node:path";
2
- import { GLOBAL_BOOLEAN_FLAGS_WITH_OPTIONAL_VALUES, VALUE_FLAGS } from "../../argv-grammar.js";
1
+ import { lstatSync, readlinkSync, realpathSync, statSync } from "node:fs";
2
+ import { basename, dirname, join, resolve } from "node:path";
3
+ import { foldAgentBrowserFilesystemIdentity } from "../../argv-grammar.js";
4
+ import { parseWaitCommandTokens } from "../../argv-descriptor.js";
3
5
  const SCREENSHOT_BOOLEAN_FLAGS = new Set(["--annotate", "--full", "-f"]);
4
6
  const SCREENSHOT_VALUE_FLAGS = new Set(["--screenshot-dir", "--screenshot-format", "--screenshot-quality"]);
5
- const SCREENSHOT_IMAGE_EXTENSIONS = new Set([".jpeg", ".jpg", ".png", ".webp"]);
6
- function isImagePathToken(token) {
7
- const extension = extname(token).toLowerCase();
8
- return SCREENSHOT_IMAGE_EXTENSIONS.has(extension);
7
+ const SCREENSHOT_IMAGE_EXTENSIONS = [".jpeg", ".jpg", ".png", ".webp"];
8
+ function isSingleScreenshotPathToken(token) {
9
+ const explicitlyRelative = token.startsWith("./") || token.startsWith("../");
10
+ if (token.startsWith("#") || token.startsWith("@") || (token.startsWith(".") && !explicitlyRelative && !token.includes("/")))
11
+ return false;
12
+ return explicitlyRelative || token.includes("/") || SCREENSHOT_IMAGE_EXTENSIONS.some((extension) => token.endsWith(extension));
13
+ }
14
+ function getScreenshotPositionalIndices(commandTokens) {
15
+ if (commandTokens[0] !== "screenshot")
16
+ return [];
17
+ const positionalIndices = [];
18
+ for (let index = 1; index < commandTokens.length; index += 1) {
19
+ const token = commandTokens[index];
20
+ if (SCREENSHOT_VALUE_FLAGS.has(token)) {
21
+ index += 1;
22
+ continue;
23
+ }
24
+ if (SCREENSHOT_BOOLEAN_FLAGS.has(token))
25
+ continue;
26
+ positionalIndices.push(index);
27
+ }
28
+ return positionalIndices;
9
29
  }
10
30
  export function getScreenshotPathTokenIndex(commandTokens) {
11
- if (commandTokens[0] !== "screenshot") {
31
+ const positionalIndices = getScreenshotPositionalIndices(commandTokens);
32
+ if (positionalIndices.length === 0)
12
33
  return undefined;
34
+ const candidateIndex = positionalIndices.length >= 2 ? positionalIndices[1] : positionalIndices[0];
35
+ const candidate = commandTokens[candidateIndex];
36
+ if (positionalIndices.length >= 2 || isSingleScreenshotPathToken(candidate)) {
37
+ return candidateIndex;
13
38
  }
14
- const positionalIndices = [];
15
- for (let index = 1; index < commandTokens.length; index += 1) {
39
+ return undefined;
40
+ }
41
+ const DIFF_SCREENSHOT_VALUE_FLAGS = new Set(["-b", "--baseline", "-o", "--output", "-s", "--selector", "-t", "--threshold"]);
42
+ function getDiffScreenshotOutputPath(commandTokens) {
43
+ let outputPath;
44
+ for (let index = 2; index < commandTokens.length; index += 1) {
16
45
  const token = commandTokens[index];
17
- if (token === "--") {
18
- for (let positionalIndex = index + 1; positionalIndex < commandTokens.length; positionalIndex += 1) {
19
- positionalIndices.push(positionalIndex);
46
+ if (!DIFF_SCREENSHOT_VALUE_FLAGS.has(token))
47
+ continue;
48
+ const value = commandTokens[index + 1];
49
+ if (value === undefined)
50
+ return undefined;
51
+ if (token === "-o" || token === "--output")
52
+ outputPath = value;
53
+ index += 1;
54
+ }
55
+ return outputPath;
56
+ }
57
+ function foldArtifactPath(path, platform) {
58
+ return foldAgentBrowserFilesystemIdentity(path, platform);
59
+ }
60
+ function canonicalizeArtifactPath(absolutePath, platform, seenSymlinks) {
61
+ let cursor = absolutePath;
62
+ const suffix = [];
63
+ while (true) {
64
+ try {
65
+ const canonicalPath = join(realpathSync.native(cursor), ...suffix);
66
+ try {
67
+ const stats = statSync(canonicalPath, { bigint: true });
68
+ if (stats.ino > 0n)
69
+ return `inode:${stats.dev}:${stats.ino}`;
70
+ }
71
+ catch {
72
+ // The destination does not exist yet; canonical ancestry still catches aliases.
20
73
  }
21
- break;
74
+ return foldArtifactPath(canonicalPath, platform);
22
75
  }
23
- if (token.startsWith("-")) {
24
- const normalizedToken = token.split("=", 1)[0] ?? token;
25
- if ((SCREENSHOT_VALUE_FLAGS.has(normalizedToken) || VALUE_FLAGS.has(normalizedToken)) && !token.includes("=")) {
26
- index += 1;
27
- continue;
76
+ catch {
77
+ let symlinkTarget;
78
+ try {
79
+ if (lstatSync(cursor).isSymbolicLink())
80
+ symlinkTarget = resolve(dirname(cursor), readlinkSync(cursor));
28
81
  }
29
- if (SCREENSHOT_BOOLEAN_FLAGS.has(normalizedToken) || GLOBAL_BOOLEAN_FLAGS_WITH_OPTIONAL_VALUES.has(normalizedToken)) {
30
- if (["true", "false"].includes(commandTokens[index + 1] ?? ""))
31
- index += 1;
32
- continue;
82
+ catch { }
83
+ if (symlinkTarget) {
84
+ if (seenSymlinks.has(cursor))
85
+ throw new Error(`Artifact destination contains a symlink loop: ${absolutePath}`);
86
+ if (seenSymlinks.size >= 32)
87
+ throw new Error(`Artifact destination has too many symlink hops: ${absolutePath}`);
88
+ seenSymlinks.add(cursor);
89
+ return canonicalizeArtifactPath(join(symlinkTarget, ...suffix), platform, seenSymlinks);
33
90
  }
91
+ const parent = dirname(cursor);
92
+ if (parent === cursor)
93
+ return foldArtifactPath(absolutePath, platform);
94
+ suffix.unshift(basename(cursor));
95
+ cursor = parent;
34
96
  }
35
- positionalIndices.push(index);
36
97
  }
37
- if (positionalIndices.length === 0) {
38
- return undefined;
39
- }
40
- const candidateIndex = positionalIndices[positionalIndices.length - 1];
41
- const candidate = commandTokens[candidateIndex];
42
- if (positionalIndices.length >= 2 || isImagePathToken(candidate) || isAbsolute(candidate) || candidate.startsWith("./") || candidate.startsWith("../")) {
43
- return candidateIndex;
98
+ }
99
+ export function canonicalizeExplicitArtifactDestination(cwd, destination, platform = process.platform) {
100
+ return canonicalizeArtifactPath(resolve(cwd, destination), platform, new Set());
101
+ }
102
+ export function getExplicitArtifactDestination(commandTokens) {
103
+ const command = commandTokens[0];
104
+ const subcommand = commandTokens[1];
105
+ if (command === "screenshot") {
106
+ const index = getScreenshotPathTokenIndex(commandTokens);
107
+ return index === undefined ? undefined : commandTokens[index];
44
108
  }
109
+ if (command === "download")
110
+ return commandTokens[2];
111
+ if (command === "pdf")
112
+ return commandTokens[1];
113
+ if (command === "wait")
114
+ return parseWaitCommandTokens(commandTokens).downloadPath;
115
+ if (command === "state" && subcommand === "save")
116
+ return commandTokens[2];
117
+ if (command === "diff" && subcommand === "screenshot")
118
+ return getDiffScreenshotOutputPath(commandTokens);
119
+ if (command === "network" && subcommand === "har" && commandTokens[2] === "stop")
120
+ return commandTokens[3];
121
+ if ((command === "trace" || command === "profiler") && subcommand === "stop")
122
+ return commandTokens[2];
123
+ if (command === "record" && (subcommand === "start" || subcommand === "restart"))
124
+ return commandTokens[2];
45
125
  return undefined;
46
126
  }
@@ -245,6 +245,7 @@ export async function collectClickDispatchDiagnostic(options) {
245
245
  const result = getEvalResultRecord(data);
246
246
  if (!result)
247
247
  return undefined;
248
+ options.probe.cleaned = true;
248
249
  const status = typeof result.status === "string" ? result.status : undefined;
249
250
  if (status !== "no-native-event-observed")
250
251
  return undefined;
@@ -264,7 +265,7 @@ export async function collectClickDispatchDiagnostic(options) {
264
265
  };
265
266
  }
266
267
  export async function cleanupClickDispatchProbe(options) {
267
- if (!options.probe || !options.sessionName)
268
+ if (!options.probe || options.probe.cleaned || !options.sessionName)
268
269
  return;
269
270
  await runSessionCommandData({
270
271
  args: ["eval", "--stdin"],
@@ -7,8 +7,8 @@ import { isHttpOrHttpsUrl } from "../../input-modes/job.js";
7
7
  import { formatSessionArtifactRetentionSummary } from "../../results/artifact-manifest.js";
8
8
  import { buildNextToolAction, withOptionalSessionArgs } from "../../results/next-actions.js";
9
9
  import { buildVisibleRefFallbackDiagnosticFromSnapshot, getVisibleRefFallbackTarget } from "../../results/selector-recovery.js";
10
- import { extractRefSnapshotFromData, normalizeComparableUrl } from "../../session-page-state.js";
11
- import { redactInvocationArgs, redactSensitiveText } from "../../runtime.js";
10
+ import { extractRefSnapshotFromData, isAboutBlankUrl, normalizeComparableUrl } from "../../session-page-state.js";
11
+ import { extractUpstreamCommandTokens, parseWaitCommandTokens, redactInvocationArgs, redactSensitiveText } from "../../runtime.js";
12
12
  import { isRecord } from "../../parsing.js";
13
13
  import { getManagedSessionStateAccessValidationError, isFileUrl } from "../../managed-session-state-policy.js";
14
14
  import { extractBatchResultCommand, extractNavigationSummaryFromData, extractStringResultField, findElectronLaunchRecordForSession, runSessionCommandData, } from "./session-state.js";
@@ -22,8 +22,14 @@ export async function collectNavigationSummary(options) {
22
22
  const url = extractStringResultField(await runSessionCommandData({ args: ["get", "url"], cwd: options.cwd, namespace: options.namespace, sessionName: options.sessionName, signal: options.signal }), "url");
23
23
  if (!url || !/^[a-z][a-z0-9+.-]*:/i.test(url))
24
24
  return undefined;
25
- if (isFileUrl(url))
25
+ if (isFileUrl(url) || isAboutBlankUrl(url))
26
26
  return { url };
27
+ // Reuse the title already observed for this exact URL instead of spending a second probe. Titles can
28
+ // change without a URL change on SPAs, but this summary is only a "last observed" page label; the URL
29
+ // stays live-probed on every call.
30
+ if (options.priorTarget?.title && normalizeComparableUrl(options.priorTarget.url) === normalizeComparableUrl(url)) {
31
+ return { title: options.priorTarget.title, url };
32
+ }
27
33
  const title = extractStringResultField(await runSessionCommandData({ args: ["get", "title"], cwd: options.cwd, namespace: options.namespace, sessionName: options.sessionName, signal: options.signal }), "title");
28
34
  return { title, url };
29
35
  }
@@ -390,14 +396,14 @@ function isBroadGetTextSelector(selector) {
390
396
  return normalized === "body" || normalized === "html" || normalized === ":root" || normalized === "*" || normalized === "main" || normalized === "div" || normalized === "section" || normalized === "article" || /^\[role=(?:"application"|'application'|application)\]$/i.test(normalized);
391
397
  }
392
398
  function getElectronTextScopeContext(options) {
393
- const record = findElectronLaunchRecordForSession(options.sessionName, options.electronLaunchRecords);
399
+ const record = findElectronLaunchRecordForSession(options.sessionName, options.electronLaunchRecords, options.namespace);
394
400
  if (!record)
395
401
  return undefined;
396
402
  const url = options.currentTarget?.url ?? options.priorTarget?.url;
397
403
  return { launchId: record.launchId, sessionName: record.sessionName ?? options.sessionName, url };
398
404
  }
399
405
  export function getSourceLookupElectronContext(options) {
400
- const record = findElectronLaunchRecordForSession(options.sessionName, options.electronLaunchRecords);
406
+ const record = findElectronLaunchRecordForSession(options.sessionName, options.electronLaunchRecords, options.namespace);
401
407
  if (!record)
402
408
  return undefined;
403
409
  const url = options.currentTarget?.url ?? options.priorTarget?.url;
@@ -469,9 +475,11 @@ export function formatEvalResultWarningText(warning) {
469
475
  return warning ? `Eval result warning: ${warning.reason} ${warning.suggestion}` : undefined;
470
476
  }
471
477
  export async function getArtifactCleanupGuidance(options) {
472
- if (!options.succeeded || !isCloseCommand(options.command) || !options.manifest || options.manifest.entries.length === 0)
478
+ if (!options.succeeded || !isCloseCommand(options.command) || !options.manifest)
473
479
  return undefined;
474
480
  const explicitEntries = options.manifest.entries.filter((entry) => entry.storageScope === "explicit-path");
481
+ if (explicitEntries.length === 0)
482
+ return undefined;
475
483
  const explicitArtifactPaths = [];
476
484
  const seenPaths = new Set();
477
485
  for (const entry of explicitEntries) {
@@ -490,16 +498,15 @@ export async function getArtifactCleanupGuidance(options) {
490
498
  seenPaths.add(displayPath);
491
499
  explicitArtifactPaths.push(displayPath);
492
500
  }
501
+ if (explicitArtifactPaths.length === 0)
502
+ return undefined;
493
503
  return { explicitArtifactPaths, note: "Closing the browser session does not delete explicit screenshots, downloads, PDFs, traces, HAR files, or recordings; clean existing paths with host file tools when no longer needed.", owner: "host-file-tools", summary: formatSessionArtifactRetentionSummary(options.manifest) };
494
504
  }
495
505
  export function formatArtifactCleanupGuidanceText(guidance) {
496
- if (!guidance)
506
+ if (!guidance || guidance.explicitArtifactPaths.length === 0)
497
507
  return undefined;
498
508
  const explicitCount = guidance.explicitArtifactPaths.length;
499
- const explicitSummary = explicitCount === 0
500
- ? "No existing explicit artifact paths were found in the recent manifest."
501
- : `${explicitCount} explicit artifact${explicitCount === 1 ? "" : "s"} remain${explicitCount === 1 ? "s" : ""}; expand or inspect details.artifactCleanup.explicitArtifactPaths for paths.`;
502
- return `Artifact lifecycle: ${explicitSummary} Browser close does not delete explicit screenshots, downloads, PDFs, traces, HAR files, or recordings; use host file tools for cleanup.`;
509
+ return `Artifact lifecycle: ${explicitCount} explicit artifact${explicitCount === 1 ? "" : "s"} remain${explicitCount === 1 ? "s" : ""}; expand or inspect details.artifactCleanup.explicitArtifactPaths for paths. Browser close does not delete explicit screenshots, downloads, PDFs, traces, HAR files, or recordings; use host file tools for cleanup.`;
503
510
  }
504
511
  async function collectManagedSessionCommandData(options) {
505
512
  try {
@@ -718,24 +725,18 @@ function getLastPositionalToken(args, startIndex = 1) {
718
725
  return undefined;
719
726
  }
720
727
  function getTimeoutStepArtifactPath(args) {
721
- const [command] = args;
728
+ const commandArgs = extractUpstreamCommandTokens(args);
729
+ const [command] = commandArgs;
722
730
  if (command === "screenshot") {
723
- const index = getScreenshotPathTokenIndex(args);
724
- return index === undefined ? undefined : args[index];
731
+ const index = getScreenshotPathTokenIndex(commandArgs);
732
+ return index === undefined ? undefined : commandArgs[index];
725
733
  }
726
734
  if (command === "pdf")
727
- return getLastPositionalToken(args);
735
+ return getLastPositionalToken(commandArgs);
728
736
  if (command === "download")
729
- return getLastPositionalToken(args, 2);
730
- if (command === "wait") {
731
- const inlineDownload = args.find((token) => token.startsWith("--download="));
732
- if (inlineDownload)
733
- return inlineDownload.slice("--download=".length) || undefined;
734
- const downloadIndex = args.indexOf("--download");
735
- const downloadPath = downloadIndex >= 0 ? args[downloadIndex + 1] : undefined;
736
- if (downloadPath && !downloadPath.startsWith("-"))
737
- return downloadPath;
738
- }
737
+ return getLastPositionalToken(commandArgs, 2);
738
+ if (command === "wait")
739
+ return parseWaitCommandTokens(commandArgs).downloadPath;
739
740
  return undefined;
740
741
  }
741
742
  async function statTimeoutArtifactPath(absolutePath) {
@@ -195,7 +195,7 @@ export async function prepareFinalResultRecoveryState(options) {
195
195
  }
196
196
  }
197
197
  const richInputRecoveryDiagnostic = buildRichInputRecoveryDiagnostic(visibleRefFallbackDiagnostic);
198
- const noActivePageSnapshotFailure = categoryDetails.resultCategory === "failure" && (isNoActivePageSnapshotFailure(options.executionPlan.commandInfo.command, options.errorText ?? options.presentation.summary) || options.batchRefSnapshotState?.invalidation !== undefined);
198
+ const noActivePageSnapshotFailure = categoryDetails.resultCategory === "failure" && (isNoActivePageSnapshotFailure(options.executionPlan.commandInfo.command, options.errorText ?? options.presentation.summary) || options.batchRefSnapshotState?.invalidation?.reason === "no-active-page");
199
199
  const executionSessionKey = getSessionContextKey(options.executionPlan.sessionName, options.executionPlan.namespace);
200
200
  if (noActivePageSnapshotFailure && executionSessionKey) {
201
201
  const refUpdate = options.sessionPageState.applyRefSnapshotInvalidation({ invalidation: buildNoActivePageRefSnapshotInvalidation(), sessionName: executionSessionKey, update: options.sessionPageStateUpdate });
@@ -329,6 +329,18 @@ function buildResultNextActions(options) {
329
329
  append(buildAgentBrowserNextActions({ electron: { launchId: options.electronLaunchRecord.launchId, sessionName: options.electronLaunchRecord.sessionName, status: options.electronLaunchRecord.cleanupState }, failureCategory: options.categoryDetails.failureCategory, resultCategory: options.categoryDetails.resultCategory, successCategory: options.categoryDetails.successCategory }));
330
330
  return nextActions.length > 0 ? nextActions : undefined;
331
331
  }
332
+ function formatFailureNextActionsText(options, nextActions) {
333
+ if (options.categoryDetails.resultCategory !== "failure" || !nextActions || nextActions.length === 0)
334
+ return undefined;
335
+ const lines = nextActions.slice(0, 6).map((action) => {
336
+ const params = action.params
337
+ ? { ...action.params, ...(action.params.stdin === undefined ? {} : { stdin: "[omitted; use details.nextActions]" }) }
338
+ : undefined;
339
+ const payload = action.artifactPath ? { artifactPath: action.artifactPath } : params;
340
+ return `- ${action.id}${payload ? ` ${JSON.stringify(payload)}` : ""}: ${action.reason}`;
341
+ });
342
+ return ["Next actions:", ...lines, "Use the exact redacted payloads in details.nextActions when available."].join("\n");
343
+ }
332
344
  function buildAgentBrowserResultDetails(options, nextActions) {
333
345
  const publicVisibleRefFallbackDiagnostic = options.visibleRefFallbackDiagnostic ? sanitizeVisibleRefFallbackDiagnostic(options.visibleRefFallbackDiagnostic) : undefined;
334
346
  const rawPageChangeSummary = (options.scrollNoopDiagnostic || options.comboboxFocusDiagnostic) && options.presentation.pageChangeSummary ? { ...options.presentation.pageChangeSummary, nextActionIds: nextActions?.map((action) => action.id) } : options.presentation.pageChangeSummary;
@@ -357,6 +369,7 @@ function buildAgentBrowserResultDetails(options, nextActions) {
357
369
  navigationSummary: options.navigationSummary,
358
370
  electron: options.electronLaunchRecord ? { action: "launch", cleanup: options.electronFailedConnectCleanup, handoff: options.electronHandoff, identifiers: buildElectronIdentifiers(options.electronLaunchRecord), launch: options.electronLaunchRecord, profileIsolation: options.electronProfileIsolationDetails, status: options.succeeded ? "succeeded" : "failed", targets: options.electronLaunch?.targets, version: options.electronLaunch?.version } : undefined,
359
371
  ...options.categoryDetails,
372
+ agentBrowserStarted: options.processResult.agentBrowserStarted,
360
373
  aboutBlankSessionMismatch: options.aboutBlankSessionMismatch,
361
374
  electronPostCommandHealth: options.electronPostCommandHealth,
362
375
  electronRefFreshness: options.electronRefFreshnessDiagnostic,
@@ -434,7 +447,8 @@ export function buildFinalAgentBrowserToolResult(options) {
434
447
  const artifactCleanupText = formatArtifactCleanupGuidanceText(options.artifactCleanup);
435
448
  const timeoutPartialProgressText = options.timeoutPartialProgress ? formatTimeoutPartialProgressText(options.timeoutPartialProgress) : undefined;
436
449
  const managedSessionOutcomeText = formatManagedSessionOutcomeText(options.managedSessionOutcome);
437
- const rawAppendedDiagnosticText = [visibleRefFallbackText, richInputRecoveryText, semanticActionCandidateText, clickDispatchText, overlayBlockerText, fillVerificationText, electronRefFreshnessText, selectorTextVisibilityText, electronBroadGetTextScopeText, scrollNoopDiagnosticText, comboboxFocusDiagnosticText, recordingDependencyWarningText, evalStdinHintText, evalResultWarningText, artifactCleanupText, timeoutPartialProgressText, managedSessionOutcomeText].filter((item) => item !== undefined).join("\n\n");
450
+ const failureNextActionsText = formatFailureNextActionsText(options, nextActions);
451
+ const rawAppendedDiagnosticText = [visibleRefFallbackText, richInputRecoveryText, semanticActionCandidateText, clickDispatchText, overlayBlockerText, fillVerificationText, electronRefFreshnessText, selectorTextVisibilityText, electronBroadGetTextScopeText, scrollNoopDiagnosticText, comboboxFocusDiagnosticText, recordingDependencyWarningText, evalStdinHintText, evalResultWarningText, artifactCleanupText, timeoutPartialProgressText, managedSessionOutcomeText, failureNextActionsText].filter((item) => item !== undefined).join("\n\n");
438
452
  const appendedDiagnosticText = redactSensitiveText(redactExactSensitiveText(rawAppendedDiagnosticText, options.exactSensitiveValues));
439
453
  const shouldAppendDiagnosticText = appendedDiagnosticText.length > 0 && (!options.userRequestedJson || options.plainTextInspection);
440
454
  let content = shouldAppendDiagnosticText && options.redactedContent[0]?.type === "text" ? [{ ...options.redactedContent[0], text: `${options.redactedContent[0].text}\n\n${appendedDiagnosticText}` }, ...options.redactedContent.slice(1)] : options.redactedContent;
@@ -462,5 +476,5 @@ export async function buildMissingBinaryFailureResult(options) {
462
476
  missingBinaryElectronRecord = missingBinaryElectronCleanup.record;
463
477
  }
464
478
  const textParts = [errorText, managedSessionOutcomeText, missingBinaryElectronCleanup ? `Electron cleanup after failed attach: ${missingBinaryElectronCleanup.summary}` : undefined].filter((part) => part !== undefined && part.length > 0);
465
- return { content: [{ type: "text", text: textParts.join("\n\n") }], details: { args: options.redactedArgs, compatibilityWorkaround: options.compatibilityWorkaround, effectiveArgs: options.redactedProcessArgs, electron: missingBinaryElectronRecord ? { action: "launch", cleanup: missingBinaryElectronCleanup, launch: missingBinaryElectronRecord, status: "failed", targets: options.electronLaunch?.targets, version: options.electronLaunch?.version } : undefined, managedSessionOutcome, namespace: options.executionPlan.namespace, nextActions: managedSessionRecoveryNextActions.length > 0 ? managedSessionRecoveryNextActions : undefined, sessionMode: options.sessionMode, sessionTabCorrection: options.sessionTabCorrection, ...buildAgentBrowserResultCategoryDetails({ args: options.redactedProcessArgs, command: options.executionPlan.commandInfo.command, errorText, failureCategory: "missing-binary", spawnError, succeeded: false }), spawnError }, isError: true };
479
+ return { content: [{ type: "text", text: textParts.join("\n\n") }], details: { agentBrowserStarted: options.processResult.agentBrowserStarted, args: options.redactedArgs, compatibilityWorkaround: options.compatibilityWorkaround, effectiveArgs: options.redactedProcessArgs, electron: missingBinaryElectronRecord ? { action: "launch", cleanup: missingBinaryElectronCleanup, launch: missingBinaryElectronRecord, status: "failed", targets: options.electronLaunch?.targets, version: options.electronLaunch?.version } : undefined, managedSessionOutcome, namespace: options.executionPlan.namespace, nextActions: managedSessionRecoveryNextActions.length > 0 ? managedSessionRecoveryNextActions : undefined, sessionMode: options.sessionMode, sessionTabCorrection: options.sessionTabCorrection, ...buildAgentBrowserResultCategoryDetails({ args: options.redactedProcessArgs, command: options.executionPlan.commandInfo.command, errorText, failureCategory: "missing-binary", spawnError, succeeded: false }), spawnError }, isError: true };
466
480
  }
@@ -20,6 +20,7 @@ async function runAgentBrowserToolInContext(options) {
20
20
  const ownedManagedSession = prepared.ownedManagedSessionContext;
21
21
  return await withOwnedManagedSessionContext(ownedManagedSession, async () => {
22
22
  try {
23
+ const artifactRunStartedAtMs = Date.now();
23
24
  const processResult = await runAgentBrowserProcess({
24
25
  args: prepared.processArgs,
25
26
  cwd: options.cwd,
@@ -51,7 +52,7 @@ async function runAgentBrowserToolInContext(options) {
51
52
  });
52
53
  if (missingBinaryResult)
53
54
  return missingBinaryResult;
54
- const output = await processBrowserOutput({ ...options, prepared, processResult });
55
+ const output = await processBrowserOutput({ ...options, artifactRunStartedAtMs, prepared, processResult });
55
56
  applyBrowserRunStatePatch(options.state, output.statePatch);
56
57
  return output.result;
57
58
  }
@@ -3,13 +3,14 @@ import { acquireManagedSessionPolicyLock } from "../../managed-session-policy-lo
3
3
  import { pruneOwnedManagedSessionRestoreSnapshots, resolveExplicitAutosaveInterval, } from "../../managed-session-restore.js";
4
4
  import { isManagedSessionRestoreKey } from "../../managed-session-storage.js";
5
5
  import { isRecord } from "../../parsing.js";
6
+ import { getAgentBrowserProcessEnvironment } from "../../process-environment.js";
6
7
  import { runAgentBrowserProcess } from "../../process.js";
7
8
  import { getAgentBrowserErrorText, parseAgentBrowserEnvelope } from "../../results/envelope.js";
8
9
  import { redactInvocationArgs } from "../../runtime.js";
9
10
  const MANAGED_SESSION_DAEMON_INSPECTION_TIMEOUT_MS = 35_000;
10
11
  const RUNNING_HEADED_AUTOSAVE_POLICY_CHANGE_ERROR = "AGENT_BROWSER_AUTOSAVE_INTERVAL_MS cannot change a running wrapper-owned headed session's launch-time periodic autosave interval. Close that session first, then retry with sessionMode: \"fresh\" so the new daemon starts with the requested interval.";
11
12
  export function getRunningHeadedAutosavePolicyChangeError(recordedInterval, closeCommand = false) {
12
- const explicitInterval = resolveExplicitAutosaveInterval(process.env.AGENT_BROWSER_AUTOSAVE_INTERVAL_MS);
13
+ const explicitInterval = resolveExplicitAutosaveInterval(getAgentBrowserProcessEnvironment().AGENT_BROWSER_AUTOSAVE_INTERVAL_MS);
13
14
  return recordedInterval !== undefined && !closeCommand && explicitInterval !== undefined && explicitInterval !== recordedInterval
14
15
  ? RUNNING_HEADED_AUTOSAVE_POLICY_CHANGE_ERROR
15
16
  : undefined;
@@ -79,7 +80,7 @@ export async function acquireOwnedManagedSessionDaemonPolicy(options) {
79
80
  if (options.mode === "close") {
80
81
  if (daemon.status === "active")
81
82
  context.restoreState.recordDaemonRestoreKey(context.sessionName, context.namespace, daemon.restoreKey);
82
- return { lock };
83
+ return { daemonStatus: daemon.status, lock };
83
84
  }
84
85
  const stickyDisabled = context.restoreState.isDisabled(context.sessionName, context.namespace);
85
86
  const hasKnownDaemonRestoreKey = context.restoreState.hasDaemonRestoreKey(context.sessionName, context.namespace);
@@ -101,13 +102,14 @@ export async function acquireOwnedManagedSessionDaemonPolicy(options) {
101
102
  context.restoreState.recordDaemonRestoreKey(context.sessionName, context.namespace, daemon.restoreKey);
102
103
  return !["inactive", "missing-binary"].includes(daemon.status) && !activePolicyMatches
103
104
  ? {
105
+ daemonStatus: daemon.status,
104
106
  error: [
105
107
  "This wrapper-owned session's live daemon does not match the requested managed-restore policy.",
106
108
  "Close that session first, retry with sessionMode: \"fresh\", or use a distinct explicit --session.",
107
109
  ].join(" "),
108
110
  lock,
109
111
  }
110
- : { lock };
112
+ : { daemonStatus: daemon.status, lock };
111
113
  }
112
114
  catch (error) {
113
115
  await lock.release();
@@ -118,7 +120,7 @@ export async function closeManagedSession(options) {
118
120
  const controller = new AbortController();
119
121
  const timer = setTimeout(() => controller.abort(), options.timeoutMs);
120
122
  let stdoutSpillPath;
121
- const closeArgs = [...(options.namespace ? ["--namespace", options.namespace] : []), "--session", options.sessionName, "close"];
123
+ const closeArgs = [...(options.namespace !== undefined ? ["--namespace", options.namespace] : []), "--session", options.sessionName, "close"];
122
124
  const policyLock = options.policyLock ?? await acquireManagedSessionPolicyLock({
123
125
  namespace: options.namespace,
124
126
  sessionName: options.sessionName,