pi-agent-browser-native 0.2.77 → 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 (118) hide show
  1. package/CHANGELOG.md +154 -0
  2. package/README.md +69 -35
  3. package/dist/extensions/agent-browser/index.js +911 -121
  4. package/dist/extensions/agent-browser/lib/argv-descriptor.js +35 -8
  5. package/dist/extensions/agent-browser/lib/argv-grammar.js +48 -7
  6. package/dist/extensions/agent-browser/lib/batch-lifecycle.js +71 -0
  7. package/dist/extensions/agent-browser/lib/command-policy.js +1 -6
  8. package/dist/extensions/agent-browser/lib/command-taxonomy.js +35 -8
  9. package/dist/extensions/agent-browser/lib/config-policy.js +0 -5
  10. package/dist/extensions/agent-browser/lib/config.js +0 -6
  11. package/dist/extensions/agent-browser/lib/electron/cdp.js +6 -7
  12. package/dist/extensions/agent-browser/lib/electron/cleanup.js +0 -7
  13. package/dist/extensions/agent-browser/lib/electron/discovery.js +0 -7
  14. package/dist/extensions/agent-browser/lib/electron/launch.js +0 -7
  15. package/dist/extensions/agent-browser/lib/input-modes/electron.js +0 -5
  16. package/dist/extensions/agent-browser/lib/input-modes/job.js +61 -9
  17. package/dist/extensions/agent-browser/lib/input-modes/lookups.js +2 -7
  18. package/dist/extensions/agent-browser/lib/input-modes/params.js +23 -29
  19. package/dist/extensions/agent-browser/lib/input-modes/script.js +462 -0
  20. package/dist/extensions/agent-browser/lib/input-modes/semantic-action.js +51 -17
  21. package/dist/extensions/agent-browser/lib/input-modes/shared.js +0 -5
  22. package/dist/extensions/agent-browser/lib/input-modes/types.js +0 -5
  23. package/dist/extensions/agent-browser/lib/json-schema.js +0 -5
  24. package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +12 -5
  25. package/dist/extensions/agent-browser/lib/managed-session-capabilities.js +0 -2
  26. package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +3 -4
  27. package/dist/extensions/agent-browser/lib/managed-session-restore.js +55 -48
  28. package/dist/extensions/agent-browser/lib/managed-session-snapshots.js +2 -9
  29. package/dist/extensions/agent-browser/lib/managed-session-state-policy.js +48 -36
  30. package/dist/extensions/agent-browser/lib/managed-session-storage.js +50 -29
  31. package/dist/extensions/agent-browser/lib/navigation-policy.js +0 -5
  32. package/dist/extensions/agent-browser/lib/orchestration/batch-stdin.js +26 -5
  33. package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.js +110 -30
  34. package/dist/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.js +2 -1
  35. package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +27 -26
  36. package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +58 -33
  37. package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +2 -1
  38. package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +22 -14
  39. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/direct-anchor-download.js +1 -1
  40. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/network-page-filter.js +1 -1
  41. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/scroll-shims.js +1 -1
  42. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/snapshot-filter.js +1 -1
  43. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +148 -34
  44. package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +225 -61
  45. package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +74 -30
  46. package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +47 -26
  47. package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +40 -19
  48. package/dist/extensions/agent-browser/lib/orchestration/output-file.js +42 -1
  49. package/dist/extensions/agent-browser/lib/orchestration/script-mode.js +299 -0
  50. package/dist/extensions/agent-browser/lib/parsing.js +0 -7
  51. package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +36 -11
  52. package/dist/extensions/agent-browser/lib/playbook.js +20 -24
  53. package/dist/extensions/agent-browser/lib/process-environment.js +14 -0
  54. package/dist/extensions/agent-browser/lib/process-identity.js +4 -9
  55. package/dist/extensions/agent-browser/lib/process.js +131 -48
  56. package/dist/extensions/agent-browser/lib/prompt-policy.js +171 -27
  57. package/dist/extensions/agent-browser/lib/recording-reservations.js +183 -0
  58. package/dist/extensions/agent-browser/lib/results/action-recommendations.js +63 -13
  59. package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +68 -11
  60. package/dist/extensions/agent-browser/lib/results/categories.js +7 -9
  61. package/dist/extensions/agent-browser/lib/results/confirmation.js +0 -7
  62. package/dist/extensions/agent-browser/lib/results/contracts.js +0 -7
  63. package/dist/extensions/agent-browser/lib/results/editable-ref-evidence.js +0 -7
  64. package/dist/extensions/agent-browser/lib/results/envelope.js +0 -7
  65. package/dist/extensions/agent-browser/lib/results/network.js +0 -7
  66. package/dist/extensions/agent-browser/lib/results/next-actions.js +19 -35
  67. package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +85 -44
  68. package/dist/extensions/agent-browser/lib/results/presentation/batch.js +66 -11
  69. package/dist/extensions/agent-browser/lib/results/presentation/browser-profile-recovery.js +0 -5
  70. package/dist/extensions/agent-browser/lib/results/presentation/common.js +18 -5
  71. package/dist/extensions/agent-browser/lib/results/presentation/content.js +0 -5
  72. package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +7 -6
  73. package/dist/extensions/agent-browser/lib/results/presentation/errors.js +2 -1
  74. package/dist/extensions/agent-browser/lib/results/presentation/large-output.js +0 -5
  75. package/dist/extensions/agent-browser/lib/results/presentation/managed-list-filter.js +0 -5
  76. package/dist/extensions/agent-browser/lib/results/presentation/navigation.js +26 -16
  77. package/dist/extensions/agent-browser/lib/results/presentation/registry.js +58 -13
  78. package/dist/extensions/agent-browser/lib/results/presentation/semantic-action.js +1 -15
  79. package/dist/extensions/agent-browser/lib/results/presentation/skills.js +0 -5
  80. package/dist/extensions/agent-browser/lib/results/presentation.js +6 -8
  81. package/dist/extensions/agent-browser/lib/results/recovery-actions.js +2 -7
  82. package/dist/extensions/agent-browser/lib/results/recovery-next-actions.js +0 -7
  83. package/dist/extensions/agent-browser/lib/results/selector-recovery.js +51 -15
  84. package/dist/extensions/agent-browser/lib/results/snapshot-high-value-controls.js +13 -14
  85. package/dist/extensions/agent-browser/lib/results/snapshot-refs.js +0 -7
  86. package/dist/extensions/agent-browser/lib/results/snapshot-segments.js +0 -7
  87. package/dist/extensions/agent-browser/lib/results/snapshot-spill.js +0 -7
  88. package/dist/extensions/agent-browser/lib/results/snapshot.js +0 -7
  89. package/dist/extensions/agent-browser/lib/results/text.js +0 -7
  90. package/dist/extensions/agent-browser/lib/runtime.js +119 -52
  91. package/dist/extensions/agent-browser/lib/session-page-state.js +62 -17
  92. package/dist/extensions/agent-browser/lib/string-enum-schema.js +0 -5
  93. package/dist/extensions/agent-browser/lib/temp.js +0 -7
  94. package/dist/extensions/agent-browser/lib/upstream-version.js +14 -0
  95. package/dist/extensions/agent-browser/lib/web-search.js +0 -5
  96. package/dist/extensions/agent-browser/script-worker.js +169 -0
  97. package/dist/scripts/agent-browser-target.mjs +3 -0
  98. package/docs/ARCHITECTURE.md +50 -31
  99. package/docs/COMMAND_REFERENCE.md +97 -41
  100. package/docs/ELECTRON.md +3 -19
  101. package/docs/RELEASE.md +8 -20
  102. package/docs/REQUIREMENTS.md +8 -6
  103. package/docs/SUPPORT_MATRIX.md +32 -25
  104. package/docs/TOOL_CONTRACT.md +107 -64
  105. package/docs/platform-smoke.md +1 -1
  106. package/package.json +7 -6
  107. package/platform-smoke.config.mjs +2 -2
  108. package/scripts/agent-browser-capability-baseline.mjs +24 -6
  109. package/scripts/agent-browser-target.mjs +3 -0
  110. package/scripts/build.mjs +41 -0
  111. package/scripts/doctor.mjs +8 -7
  112. package/scripts/platform-smoke/browser-dogfood-windows.ps1 +9 -3
  113. package/scripts/platform-smoke/targets.mjs +12 -6
  114. package/dist/extensions/agent-browser/lib/electron/text.js +0 -13
  115. package/dist/extensions/agent-browser/lib/input-modes.js +0 -10
  116. package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-artifacts.js +0 -5
  117. package/dist/extensions/agent-browser/lib/results/artifact-state.js +0 -13
  118. package/dist/extensions/agent-browser/lib/results.js +0 -14
@@ -1,13 +1,8 @@
1
- /**
2
- * Purpose: Own file artifact detection, verification, manifest merging, and inline image attachment for tool presentation.
3
- * Responsibilities: Build artifact metadata, verification summaries, saved-file details, artifact retention notices, and safe image content.
4
- * Scope: Artifact and image presentation only.
5
- */
6
1
  import { readFile, stat } from "node:fs/promises";
7
2
  import { extname, resolve } from "node:path";
3
+ import { getAgentBrowserSessionIdentityKey } from "../../argv-grammar.js";
8
4
  import { isRecord, parsePositiveInteger } from "../../parsing.js";
9
- import { formatSessionArtifactRetentionSummary, mergeSessionArtifactManifest, } from "../artifact-manifest.js";
10
- import { isPendingRecordingArtifact, isPendingRecordingCommand } from "../artifact-state.js";
5
+ import { formatSessionArtifactRetentionSummary, getSessionArtifactManifestEntryKey, isPendingRecordingArtifact, isPendingRecordingCommand, mergeSessionArtifactManifest, } from "../artifact-manifest.js";
11
6
  import { classifyAgentBrowserSuccessCategory } from "../categories.js";
12
7
  const IMAGE_EXTENSION_TO_MIME_TYPE = {
13
8
  ".gif": "image/gif",
@@ -18,6 +13,7 @@ const IMAGE_EXTENSION_TO_MIME_TYPE = {
18
13
  };
19
14
  const INLINE_IMAGE_MAX_BYTES_ENV = "PI_AGENT_BROWSER_INLINE_IMAGE_MAX_BYTES";
20
15
  const DEFAULT_INLINE_IMAGE_MAX_BYTES = 5 * 1_024 * 1_024;
16
+ const ARTIFACT_MTIME_TOLERANCE_MS = 2_000;
21
17
  function getImageMimeType(filePath) {
22
18
  const extension = extname(filePath).toLowerCase();
23
19
  return IMAGE_EXTENSION_TO_MIME_TYPE[extension];
@@ -42,14 +38,11 @@ function appendPresentationNotice(presentation, message) {
42
38
  function shouldAppendArtifactRetentionNotice(entries) {
43
39
  return entries.some((entry) => entry.retentionState === "evicted" || entry.storageScope !== "explicit-path");
44
40
  }
45
- function getManifestEntryKey(entry) {
46
- return entry.storageScope === "explicit-path" && entry.absolutePath ? `${entry.storageScope}:${entry.absolutePath}` : `${entry.storageScope}:${entry.path}`;
47
- }
48
41
  export function manifestHasNewNoticeWorthyEntries(base, current) {
49
42
  if (!current)
50
43
  return false;
51
- const baseKeys = new Set((base?.entries ?? []).map(getManifestEntryKey));
52
- return current.entries.some((entry) => !baseKeys.has(getManifestEntryKey(entry)) && (entry.retentionState === "evicted" || entry.storageScope !== "explicit-path"));
44
+ const baseKeys = new Set((base?.entries ?? []).map(getSessionArtifactManifestEntryKey));
45
+ return current.entries.some((entry) => !baseKeys.has(getSessionArtifactManifestEntryKey(entry)) && (entry.retentionState === "evicted" || entry.storageScope !== "explicit-path"));
53
46
  }
54
47
  export function applyArtifactManifest(presentation, baseManifest, entries) {
55
48
  if (entries.length === 0)
@@ -92,6 +85,9 @@ const ARTIFACT_EXTENSION_TO_MEDIA_TYPE = {
92
85
  ".zip": "application/zip",
93
86
  ...IMAGE_EXTENSION_TO_MIME_TYPE,
94
87
  };
88
+ function isDownloadWaitSubcommand(subcommand) {
89
+ return subcommand === "--download" || subcommand === "-d";
90
+ }
95
91
  function getArtifactKind(commandInfo) {
96
92
  if (commandInfo.command === "screenshot")
97
93
  return "image";
@@ -101,7 +97,7 @@ function getArtifactKind(commandInfo) {
101
97
  return "pdf";
102
98
  if (commandInfo.command === "download")
103
99
  return "download";
104
- if (commandInfo.command === "wait" && commandInfo.subcommand === "--download")
100
+ if (commandInfo.command === "wait" && isDownloadWaitSubcommand(commandInfo.subcommand))
105
101
  return "download";
106
102
  if (commandInfo.command === "state" && commandInfo.subcommand === "save")
107
103
  return "file";
@@ -141,6 +137,11 @@ function extractPathStrings(data) {
141
137
  }
142
138
  return [...new Set(paths)];
143
139
  }
140
+ function artifactMtimeIsOutsideCommandWindow(updatedAtMs, minUpdatedAtMs, maxUpdatedAtMs) {
141
+ return minUpdatedAtMs !== undefined
142
+ && (updatedAtMs < minUpdatedAtMs - ARTIFACT_MTIME_TOLERANCE_MS
143
+ || (maxUpdatedAtMs !== undefined && updatedAtMs > maxUpdatedAtMs + ARTIFACT_MTIME_TOLERANCE_MS));
144
+ }
144
145
  async function buildFileArtifactMetadata(options) {
145
146
  const kind = getArtifactKind(options.commandInfo);
146
147
  if (!kind) {
@@ -152,11 +153,16 @@ async function buildFileArtifactMetadata(options) {
152
153
  const pendingRecording = isPendingRecordingCommand(options.commandInfo.command, options.commandInfo.subcommand, kind);
153
154
  let exists;
154
155
  let sizeBytes;
156
+ let stale = false;
157
+ let updatedAtMs;
155
158
  if (!pendingRecording) {
156
159
  try {
157
160
  const fileStats = await stat(absolutePath);
158
161
  exists = true;
159
162
  sizeBytes = fileStats.size;
163
+ updatedAtMs = fileStats.mtimeMs;
164
+ const commandCreatesArtifact = !(options.commandInfo.command === "wait" && isDownloadWaitSubcommand(options.commandInfo.subcommand));
165
+ stale = commandCreatesArtifact && artifactMtimeIsOutsideCommandWindow(updatedAtMs, options.artifactMinUpdatedAtMs, options.artifactMaxUpdatedAtMs);
160
166
  }
161
167
  catch {
162
168
  exists = false;
@@ -171,31 +177,33 @@ async function buildFileArtifactMetadata(options) {
171
177
  extension,
172
178
  kind,
173
179
  mediaType: extension ? ARTIFACT_EXTENSION_TO_MEDIA_TYPE[extension] : undefined,
180
+ namespace: options.namespace,
174
181
  path: displayPath,
175
182
  recordingState: pendingRecording ? "openRecording" : undefined,
176
183
  requestedPath: options.artifactRequest?.path,
177
184
  session: options.sessionName,
178
185
  sizeBytes,
179
- status: options.artifactRequest?.status ?? (pendingRecording ? "pending" : exists === false ? "missing" : "saved"),
186
+ status: pendingRecording ? "pending" : exists === false ? "missing" : stale ? "stale" : options.artifactRequest?.status ?? "saved",
180
187
  subcommand: options.commandInfo.subcommand,
181
188
  tempPath: options.artifactRequest?.tempPath,
189
+ updatedAtMs,
182
190
  willExistOnStop: pendingRecording ? true : undefined,
183
191
  };
184
192
  }
185
193
  async function buildPreviousRestartRecordingArtifact(options) {
186
194
  if (options.commandInfo.command !== "record" || options.commandInfo.subcommand !== "restart")
187
195
  return undefined;
196
+ const sessionKey = options.sessionName ? getAgentBrowserSessionIdentityKey(options.sessionName, options.namespace) : undefined;
188
197
  const previousRecording = options.artifactManifest?.entries.find((entry) => (entry.command === "record" &&
189
198
  (entry.subcommand === "start" || entry.subcommand === "restart") &&
190
199
  entry.kind === "video" &&
191
- (!options.sessionName || !entry.session || entry.session === options.sessionName) &&
192
- !options.currentPaths.has(entry.path) &&
193
- (!entry.absolutePath || !options.currentPaths.has(entry.absolutePath))));
200
+ (!sessionKey || (entry.session && getAgentBrowserSessionIdentityKey(entry.session, entry.namespace) === sessionKey))));
194
201
  if (!previousRecording)
195
202
  return undefined;
196
203
  const absolutePath = previousRecording.absolutePath ?? resolve(options.cwd, previousRecording.path);
197
204
  try {
198
205
  const fileStats = await stat(absolutePath);
206
+ const stale = artifactMtimeIsOutsideCommandWindow(fileStats.mtimeMs, options.artifactMinUpdatedAtMs, options.artifactMaxUpdatedAtMs);
199
207
  return {
200
208
  absolutePath,
201
209
  artifactType: "video",
@@ -205,23 +213,39 @@ async function buildPreviousRestartRecordingArtifact(options) {
205
213
  extension: previousRecording.extension ?? (extname(absolutePath).toLowerCase() || undefined),
206
214
  kind: "video",
207
215
  mediaType: previousRecording.mediaType,
216
+ namespace: previousRecording.namespace ?? options.namespace,
208
217
  path: previousRecording.path,
209
218
  requestedPath: previousRecording.requestedPath,
210
219
  session: previousRecording.session ?? options.sessionName,
211
220
  sizeBytes: fileStats.size,
212
- status: "saved",
221
+ status: stale ? "stale" : "saved",
213
222
  subcommand: "restart-previous",
223
+ updatedAtMs: fileStats.mtimeMs,
214
224
  };
215
225
  }
216
226
  catch {
217
- return undefined;
227
+ return {
228
+ absolutePath,
229
+ artifactType: "video",
230
+ command: "record",
231
+ cwd: previousRecording.cwd ?? options.cwd,
232
+ exists: false,
233
+ extension: previousRecording.extension ?? (extname(absolutePath).toLowerCase() || undefined),
234
+ kind: "video",
235
+ mediaType: previousRecording.mediaType,
236
+ namespace: previousRecording.namespace ?? options.namespace,
237
+ path: previousRecording.path,
238
+ requestedPath: previousRecording.requestedPath,
239
+ session: previousRecording.session ?? options.sessionName,
240
+ status: "missing",
241
+ subcommand: "restart-previous",
242
+ };
218
243
  }
219
244
  }
220
245
  export async function extractFileArtifacts(options) {
221
246
  const candidates = extractPathStrings(options.data);
222
247
  const currentArtifacts = (await Promise.all(candidates.map((path) => buildFileArtifactMetadata({ ...options, path })))).filter((artifact) => artifact !== undefined);
223
- const currentPaths = new Set(currentArtifacts.flatMap((artifact) => [artifact.path, artifact.absolutePath]));
224
- const previousRestartRecordingArtifact = await buildPreviousRestartRecordingArtifact({ artifactManifest: options.artifactManifest, commandInfo: options.commandInfo, currentPaths, cwd: options.cwd, sessionName: options.sessionName });
248
+ const previousRestartRecordingArtifact = await buildPreviousRestartRecordingArtifact({ artifactManifest: options.artifactManifest, artifactMaxUpdatedAtMs: options.artifactMaxUpdatedAtMs, artifactMinUpdatedAtMs: options.artifactMinUpdatedAtMs, commandInfo: options.commandInfo, cwd: options.cwd, namespace: options.namespace, sessionName: options.sessionName });
225
249
  return previousRestartRecordingArtifact ? [previousRestartRecordingArtifact, ...currentArtifacts] : currentArtifacts;
226
250
  }
227
251
  export function buildManifestEntriesForFileArtifacts(artifacts, nowMs = Date.now()) {
@@ -234,9 +258,10 @@ export function buildManifestEntriesForFileArtifacts(artifacts, nowMs = Date.now
234
258
  extension: artifact.extension,
235
259
  kind: artifact.kind,
236
260
  mediaType: artifact.mediaType,
261
+ namespace: artifact.namespace,
237
262
  path: artifact.path,
238
263
  requestedPath: artifact.requestedPath,
239
- retentionState: artifact.exists === false ? "missing" : "live",
264
+ retentionState: artifact.exists === false || artifact.status === "stale" ? "missing" : "live",
240
265
  session: artifact.session,
241
266
  sizeBytes: artifact.sizeBytes,
242
267
  storageScope: "explicit-path",
@@ -244,6 +269,9 @@ export function buildManifestEntriesForFileArtifacts(artifacts, nowMs = Date.now
244
269
  }));
245
270
  }
246
271
  export function isManifestFileArtifact(artifact) {
272
+ if (artifact.status === "stale") {
273
+ return artifact.kind === "video" && artifact.command === "record" && artifact.subcommand === "restart-previous";
274
+ }
247
275
  return artifact.kind === "video" && artifact.command === "record" ? true : !isPendingRecordingArtifact(artifact);
248
276
  }
249
277
  function getArtifactVerificationEntry(artifact) {
@@ -265,20 +293,24 @@ function getArtifactVerificationEntry(artifact) {
265
293
  willExistOnStop: artifact.willExistOnStop ?? true,
266
294
  };
267
295
  }
268
- const state = artifact.exists === true
269
- ? "verified"
270
- : artifact.exists === false
271
- ? "missing"
272
- : "unverified";
296
+ const state = artifact.status === "stale"
297
+ ? "unverified"
298
+ : artifact.exists === true
299
+ ? "verified"
300
+ : artifact.exists === false
301
+ ? "missing"
302
+ : "unverified";
273
303
  return {
274
304
  absolutePath: artifact.absolutePath,
275
305
  exists: artifact.exists,
276
306
  kind: artifact.kind,
277
- limitation: state === "missing"
278
- ? "The wrapper did not find the reported artifact at absolutePath. Treat the path as unverified until recovered or regenerated."
279
- : state === "unverified"
280
- ? "The wrapper could not prove local filesystem existence for this artifact."
281
- : undefined,
307
+ limitation: artifact.status === "stale"
308
+ ? "The reported path's modification time fell outside this command's bounded artifact window. Treat the artifact as stale until regenerated."
309
+ : state === "missing"
310
+ ? "The wrapper did not find the reported artifact at absolutePath. Treat the path as unverified until recovered or regenerated."
311
+ : state === "unverified"
312
+ ? "The wrapper could not prove local filesystem existence for this artifact."
313
+ : undefined,
282
314
  mediaType: artifact.mediaType,
283
315
  path: artifact.path,
284
316
  requestedPath: artifact.requestedPath,
@@ -287,6 +319,7 @@ function getArtifactVerificationEntry(artifact) {
287
319
  state,
288
320
  status: artifact.status,
289
321
  storageScope: "explicit-path",
322
+ updatedAtMs: artifact.updatedAtMs,
290
323
  };
291
324
  }
292
325
  function getManifestVerificationEntry(entry) {
@@ -341,17 +374,19 @@ export function buildArtifactVerificationSummary(artifacts, manifest, manifestPa
341
374
  };
342
375
  }
343
376
  export function hasMissingFileArtifact(artifacts) {
344
- return (artifacts ?? []).some((artifact) => !isPendingRecordingArtifact(artifact) && artifact.exists === false);
377
+ return (artifacts ?? []).some((artifact) => !isPendingRecordingArtifact(artifact) && (artifact.exists === false || artifact.status === "stale"));
345
378
  }
346
379
  export function formatMissingArtifactFailureText(artifacts) {
347
- const missingArtifacts = (artifacts ?? []).filter((artifact) => !isPendingRecordingArtifact(artifact) && artifact.exists === false);
348
- if (missingArtifacts.length === 0)
380
+ const failedArtifacts = (artifacts ?? []).filter((artifact) => !isPendingRecordingArtifact(artifact) && (artifact.exists === false || artifact.status === "stale"));
381
+ if (failedArtifacts.length === 0)
349
382
  return undefined;
350
- if (missingArtifacts.length === 1) {
351
- const artifact = missingArtifacts[0];
352
- return `Artifact verification failed: requested ${artifact.kind} was not found at ${artifact.absolutePath}.`;
383
+ if (failedArtifacts.length === 1) {
384
+ const artifact = failedArtifacts[0];
385
+ return artifact.status === "stale"
386
+ ? `Artifact verification failed: requested ${artifact.kind} path ${artifact.absolutePath} has a modification time outside the current command window.`
387
+ : `Artifact verification failed: requested ${artifact.kind} was not found at ${artifact.absolutePath}.`;
353
388
  }
354
- return `Artifact verification failed: ${missingArtifacts.length} requested artifacts were not found on disk.`;
389
+ return `Artifact verification failed: ${failedArtifacts.length} requested artifacts were missing or stale.`;
355
390
  }
356
391
  export function classifyPresentationSuccessCategory(options) {
357
392
  if ((options.artifactVerification?.missingCount ?? 0) > 0 || (options.artifactVerification?.unverifiedCount ?? 0) > 0) {
@@ -363,9 +398,9 @@ function formatArtifactLabel(artifact) {
363
398
  switch (artifact.kind) {
364
399
  case "download":
365
400
  if (artifact.exists !== true) {
366
- return artifact.command === "wait" && artifact.subcommand === "--download" ? "Download event reported; file not verified" : "Download reported; file not verified";
401
+ return artifact.command === "wait" && isDownloadWaitSubcommand(artifact.subcommand) ? "Download event reported; file not verified" : "Download reported; file not verified";
367
402
  }
368
- return artifact.command === "wait" && artifact.subcommand === "--download" ? "Download saved and verified" : "Downloaded file verified";
403
+ return artifact.command === "wait" && isDownloadWaitSubcommand(artifact.subcommand) ? "Download saved and verified" : "Downloaded file verified";
369
404
  case "file":
370
405
  return artifact.command === "state" ? "State file" : "Saved file";
371
406
  case "har":
@@ -381,11 +416,16 @@ function formatArtifactLabel(artifact) {
381
416
  case "trace":
382
417
  return "Saved trace";
383
418
  case "video":
384
- if (artifact.command === "record" && artifact.subcommand === "restart-previous")
419
+ if (artifact.command === "record" && artifact.subcommand === "restart-previous") {
420
+ if (artifact.status === "stale")
421
+ return "Previous recording stale";
422
+ if (artifact.exists === false)
423
+ return "Previous recording missing";
385
424
  return "Previous recording saved";
425
+ }
386
426
  if (!isPendingRecordingArtifact(artifact))
387
427
  return "Saved recording";
388
- return artifact.subcommand === "restart" ? "Recording restarted; output will be written on stop" : "Recording started; output will be written on stop";
428
+ return artifact.subcommand === "restart" ? "Recording restarted; output will be written on stop" : "Recording started in a fresh active page; output will be written on stop";
389
429
  }
390
430
  }
391
431
  export function formatArtifactSummary(artifacts) {
@@ -415,6 +455,7 @@ export function formatArtifactMetadataLines(artifacts) {
415
455
  `Status: ${artifact.status ?? "pending"}`,
416
456
  `Recording state: ${artifact.recordingState ?? "openRecording"}`,
417
457
  `Will exist on stop: ${artifact.willExistOnStop !== false}`,
458
+ artifact.subcommand === "start" ? "Page state: record start uses a fresh active page for video capture; prior in-page DOM and JavaScript state does not carry over. Take a fresh snapshot before continuing." : undefined,
418
459
  artifact.session ? `Session: ${artifact.session}` : undefined,
419
460
  artifact.cwd ? `CWD: ${artifact.cwd}` : undefined,
420
461
  `Machine data: details.artifacts[${index}]`,
@@ -439,7 +480,7 @@ export function formatArtifactMetadataLines(artifacts) {
439
480
  });
440
481
  }
441
482
  function isDownloadWaitCommand(commandInfo) {
442
- return commandInfo.command === "wait" && commandInfo.subcommand === "--download";
483
+ return commandInfo.command === "wait" && isDownloadWaitSubcommand(commandInfo.subcommand);
443
484
  }
444
485
  function extractSavedFilePath(data) {
445
486
  return typeof data.path === "string" && data.path.trim().length > 0 ? data.path : undefined;
@@ -1,11 +1,13 @@
1
+ import { isCloseCommand } from "../../command-taxonomy.js";
1
2
  import { isRecord } from "../../parsing.js";
2
- import { extractCommandTokens, parseCommandInfo, redactInvocationArgs, redactSensitiveText, redactSensitiveValue } from "../../runtime.js";
3
+ import { getAgentBrowserSessionIdentityKey } from "../../argv-grammar.js";
4
+ import { extractUpstreamCommandTokens, parseCommandInfo, redactInvocationArgs, redactSensitiveText, redactSensitiveValue } from "../../runtime.js";
3
5
  import { buildAgentBrowserNextActions } from "../action-recommendations.js";
4
- import { formatSessionArtifactRetentionSummary } from "../artifact-manifest.js";
6
+ import { formatSessionArtifactRetentionSummary, isPendingRecordingArtifact } from "../artifact-manifest.js";
5
7
  import { classifyAgentBrowserFailureCategory } from "../categories.js";
6
8
  import { detectConfirmationRequired } from "../confirmation.js";
7
9
  import { applyNetworkRouteRecords, buildNetworkRouteDiagnostics } from "../network-routes.js";
8
- import { applyNamespaceToNextActions, withOptionalSessionArgs } from "../next-actions.js";
10
+ import { appendUniqueAgentBrowserNextActions, applyNamespaceToNextActions, withOptionalSessionArgs } from "../next-actions.js";
9
11
  import { stringifyModelFacing } from "./common.js";
10
12
  import { buildArtifactVerificationSummary, classifyPresentationSuccessCategory, manifestHasNewNoticeWorthyEntries } from "./artifacts.js";
11
13
  import { formatBatchStepCommand, getPresentationImages, getPresentationPaths, getPresentationText, isStringArray } from "./content.js";
@@ -62,7 +64,7 @@ function hasModelFacingArgRedaction(args) {
62
64
  function getStatefulCommandSensitiveValues(command) {
63
65
  if (!command)
64
66
  return [];
65
- const tokens = extractCommandTokens(command);
67
+ const tokens = extractUpstreamCommandTokens(command);
66
68
  const values = [];
67
69
  if (tokens[0] === "cookies" && tokens[1] === "set" && tokens[3])
68
70
  values.push(tokens[3]);
@@ -181,10 +183,11 @@ function formatBatchStepsText(steps) {
181
183
  return lines.join("\n\n");
182
184
  }
183
185
  async function buildBatchStepPresentation(options) {
184
- const { artifactManifest, artifactRequest, buildNestedToolPresentation, cwd, index, item, namespace, networkRoutes, persistentArtifactStore, sessionName } = options;
186
+ const { artifactManifest, artifactMaxUpdatedAtMs, artifactMinUpdatedAtMs, artifactRequest, buildNestedToolPresentation, cwd, index, item, namespace, networkRoutes, persistentArtifactStore, sessionName } = options;
185
187
  const command = isStringArray(item.command) ? item.command : undefined;
186
188
  const redactedCommand = command ? redactInvocationArgs(command) : undefined;
187
189
  const commandText = formatBatchStepCommand(hasModelFacingArgRedaction(redactedCommand) ? redactedCommand : command, index);
190
+ const lifecycle = extractBatchStepLifecycle(item.result);
188
191
  if (item.success === false) {
189
192
  const redactedErrorData = command?.[0] === "clipboard"
190
193
  ? redactSensitiveValue(redactClipboardPermissionErrorValue({ command: "clipboard", subcommand: command[1] }, item.error, getClipboardWritePayloadCandidates(command)))
@@ -196,13 +199,14 @@ async function buildBatchStepPresentation(options) {
196
199
  errorText,
197
200
  });
198
201
  const confirmationRequired = detectConfirmationRequired(item.error);
199
- const nextActions = applyNamespaceToNextActions(mergePresentationNextActions(buildAgentBrowserNextActions({
202
+ const nextActions = applyNamespaceToNextActions(mergePresentationNextActions(isWaitTextAssertionCommand(command) ? [buildWaitTextAssertionFailureNextAction(sessionName)] : undefined, buildAgentBrowserNextActions({
200
203
  args: command,
201
204
  command: command?.[0],
202
205
  confirmationId: confirmationRequired?.id,
203
206
  failureCategory,
204
207
  resultCategory: "failure",
205
- }), isWaitTextAssertionCommand(command) ? [buildWaitTextAssertionFailureNextAction(sessionName)] : undefined), namespace);
208
+ sessionName,
209
+ })), namespace);
206
210
  const presentation = {
207
211
  content: [{ type: "text", text: errorText }],
208
212
  failureCategory,
@@ -219,6 +223,7 @@ async function buildBatchStepPresentation(options) {
219
223
  data: redactedErrorData,
220
224
  failureCategory,
221
225
  index,
226
+ lifecycle,
222
227
  nextActions,
223
228
  resultCategory: "failure",
224
229
  success: false,
@@ -235,12 +240,15 @@ async function buildBatchStepPresentation(options) {
235
240
  : undefined;
236
241
  const presentation = await buildNestedToolPresentation({
237
242
  artifactManifest,
243
+ artifactMaxUpdatedAtMs,
244
+ artifactMinUpdatedAtMs,
238
245
  artifactRequest,
239
246
  commandInfo: commandInfoWithTokens,
240
247
  cwd,
241
248
  args: command,
242
249
  envelope: { data: item.result, success: true },
243
250
  networkRouteDiagnostics,
251
+ namespace,
244
252
  persistentArtifactStore,
245
253
  sessionName,
246
254
  });
@@ -261,6 +269,7 @@ async function buildBatchStepPresentation(options) {
261
269
  failureCategory: presentation.failureCategory,
262
270
  resultCategory: stepSucceeded ? "success" : "failure",
263
271
  savedFilePath: presentation.savedFilePath,
272
+ sessionName,
264
273
  successCategory: presentation.successCategory,
265
274
  }), namespace);
266
275
  const pageChangeSummary = buildPageChangeSummary({
@@ -284,6 +293,7 @@ async function buildBatchStepPresentation(options) {
284
293
  imagePath: imagePaths[0],
285
294
  imagePaths: imagePaths.length > 0 ? imagePaths : undefined,
286
295
  index,
296
+ lifecycle,
287
297
  networkRouteDiagnostics: presentation.networkRouteDiagnostics,
288
298
  nextActions,
289
299
  pageChangeSummary,
@@ -298,6 +308,48 @@ async function buildBatchStepPresentation(options) {
298
308
  presentation,
299
309
  };
300
310
  }
311
+ function extractBatchStepLifecycle(result) {
312
+ if (!isRecord(result) || !isRecord(result.lifecycle) || !isRecord(result.lifecycle.effectiveLaunch))
313
+ return undefined;
314
+ const browserLaunched = result.lifecycle.effectiveLaunch.browserLaunched;
315
+ return typeof browserLaunched === "boolean" ? { effectiveLaunch: { browserLaunched } } : undefined;
316
+ }
317
+ function abandonedRecordingArtifact(artifact) {
318
+ const { recordingState: _recordingState, willExistOnStop: _willExistOnStop, ...terminal } = artifact;
319
+ return { ...terminal, exists: false, status: "missing", subcommand: "close-abandoned" };
320
+ }
321
+ function coalesceTerminalBatchRecordingArtifacts(steps, sessionName, namespace) {
322
+ const artifacts = [];
323
+ const pendingIndexesBySession = new Map();
324
+ const removedPendingIndexes = new Set();
325
+ for (const step of steps) {
326
+ for (const artifact of step.presentation.artifacts ?? []) {
327
+ const index = artifacts.push(artifact) - 1;
328
+ if (artifact.command !== "record" || artifact.kind !== "video")
329
+ continue;
330
+ const session = artifact.session ? getAgentBrowserSessionIdentityKey(artifact.session, artifact.namespace) : "";
331
+ if (isPendingRecordingArtifact(artifact)) {
332
+ const pendingIndexes = pendingIndexesBySession.get(session) ?? [];
333
+ pendingIndexes.push(index);
334
+ pendingIndexesBySession.set(session, pendingIndexes);
335
+ continue;
336
+ }
337
+ const pendingIndex = pendingIndexesBySession.get(session)?.pop();
338
+ if (pendingIndex !== undefined)
339
+ removedPendingIndexes.add(pendingIndex);
340
+ }
341
+ const command = step.details.command;
342
+ if (step.details.success !== true || !command || !isCloseCommand(extractUpstreamCommandTokens(command)[0]))
343
+ continue;
344
+ const session = sessionName ? getAgentBrowserSessionIdentityKey(sessionName, namespace) : "";
345
+ for (const pendingIndex of pendingIndexesBySession.get(session) ?? []) {
346
+ const pending = artifacts[pendingIndex];
347
+ if (pending && !removedPendingIndexes.has(pendingIndex))
348
+ artifacts[pendingIndex] = abandonedRecordingArtifact(pending);
349
+ }
350
+ }
351
+ return artifacts.filter((_, index) => !removedPendingIndexes.has(index));
352
+ }
301
353
  export async function buildBatchPresentation(options) {
302
354
  const { artifactRequests, buildNestedToolPresentation, cwd, data, namespace, networkRoutes, persistentArtifactStore, sessionName, summary } = options;
303
355
  const steps = [];
@@ -307,6 +359,8 @@ export async function buildBatchPresentation(options) {
307
359
  for (const [index, item] of data.entries()) {
308
360
  const step = await buildBatchStepPresentation({
309
361
  artifactManifest: currentArtifactManifest,
362
+ artifactMaxUpdatedAtMs: options.artifactMaxUpdatedAtMs,
363
+ artifactMinUpdatedAtMs: options.artifactMinUpdatedAtMs,
310
364
  artifactRequest: artifactRequests?.[index],
311
365
  buildNestedToolPresentation,
312
366
  cwd,
@@ -319,7 +373,7 @@ export async function buildBatchPresentation(options) {
319
373
  });
320
374
  steps.push(step);
321
375
  currentArtifactManifest = step.presentation.artifactManifest ?? currentArtifactManifest;
322
- currentNetworkRoutes = applyNetworkRouteRecords(currentNetworkRoutes, isStringArray(item.command) ? extractCommandTokens(item.command) : undefined, item.success !== false && step.details.success);
376
+ currentNetworkRoutes = applyNetworkRouteRecords(currentNetworkRoutes, isStringArray(item.command) ? extractUpstreamCommandTokens(item.command) : undefined, item.success !== false && step.details.success);
323
377
  protectedPersistentPaths.push(...getPresentationPaths({
324
378
  primaryPath: step.presentation.fullOutputPath,
325
379
  secondaryPaths: step.presentation.fullOutputPaths,
@@ -327,7 +381,7 @@ export async function buildBatchPresentation(options) {
327
381
  }
328
382
  const batchFailure = getBatchFailureDetails(steps);
329
383
  const images = steps.flatMap((step) => getPresentationImages(step.presentation));
330
- const artifacts = steps.flatMap((step) => step.presentation.artifacts ?? []);
384
+ const artifacts = coalesceTerminalBatchRecordingArtifacts(steps, sessionName, namespace);
331
385
  const artifactVerification = buildArtifactVerificationSummary(artifacts);
332
386
  const fullOutputPaths = steps.flatMap((step) => getPresentationPaths({
333
387
  primaryPath: step.presentation.fullOutputPath,
@@ -356,9 +410,10 @@ export async function buildBatchPresentation(options) {
356
410
  const contentText = artifactRetentionSummary && manifestHasNewNoticeWorthyEntries(options.artifactManifest, currentArtifactManifest)
357
411
  ? `${text}\n\n${artifactRetentionSummary}`
358
412
  : text;
413
+ const artifactLifecycleActions = applyNamespaceToNextActions(buildAgentBrowserNextActions({ artifacts, command: "batch", resultCategory: batchFailure ? "failure" : "success", sessionName }), namespace);
359
414
  const nextActions = batchFailure
360
- ? batchFailure.failedStep.nextActions
361
- : buildAgentBrowserNextActions({ artifacts, command: "batch", resultCategory: "success" });
415
+ ? appendUniqueAgentBrowserNextActions([...(batchFailure.failedStep.nextActions ?? [])], artifactLifecycleActions)
416
+ : artifactLifecycleActions;
362
417
  const changedSteps = steps.map((step) => step.details).filter((details) => details.pageChangeSummary !== undefined);
363
418
  const pageChangeSummary = artifacts.length > 0
364
419
  ? buildPageChangeSummary({
@@ -1,8 +1,3 @@
1
- /**
2
- * Purpose: Build browser profile/config recovery hints for launch/setup failures.
3
- * Responsibilities: Recognize local Chrome/profile setup errors, avoid self-loop diagnostics, and emit canonical next actions.
4
- * Scope: Error presentation only; upstream profile discovery and doctor execution remain normal agent_browser commands.
5
- */
6
1
  import { isOpenNavigationCommand } from "../../command-taxonomy.js";
7
2
  import { getStartupScopedFlags } from "../../runtime.js";
8
3
  import { buildNextToolAction } from "../next-actions.js";
@@ -1,11 +1,7 @@
1
- /**
2
- * Purpose: Share small presentation formatting and redaction helpers across result presentation modules.
3
- * Responsibilities: Normalize scalar fields, stringify model-facing values, and apply sensitive-text redaction.
4
- * Scope: Leaf helpers only; command-family formatting lives in sibling modules.
5
- */
6
1
  import { containsManagedSessionRestoreKey } from "../../managed-session-capabilities.js";
7
2
  import { redactSensitiveText, redactSensitiveValue } from "../../runtime.js";
8
3
  import { stringifyUnknown, truncateText } from "../text.js";
4
+ const UNTITLED_PAGE_SUMMARY = "(untitled page)";
9
5
  export function stringifyModelFacing(value) {
10
6
  return stringifyUnknown(redactSensitiveValue(value));
11
7
  }
@@ -39,6 +35,23 @@ export function getStringField(data, key) {
39
35
  const value = data[key];
40
36
  return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
41
37
  }
38
+ // `lifecycle` is upstream launch/reuse bookkeeping, never page content, so it must not be the
39
+ // answer an agent reads when a command has no dedicated presenter.
40
+ export function omitUpstreamLifecycle(data) {
41
+ const { lifecycle: _lifecycle, ...rest } = data;
42
+ return rest;
43
+ }
44
+ export function getPageSummary(data) {
45
+ const title = typeof data.title === "string" ? data.title : undefined;
46
+ const url = typeof data.url === "string" ? data.url : undefined;
47
+ if (title === undefined && url === undefined)
48
+ return undefined;
49
+ if (title && url)
50
+ return `${title}\n${url}`;
51
+ if (url)
52
+ return url;
53
+ return title || UNTITLED_PAGE_SUMMARY;
54
+ }
42
55
  export function formatCount(count, singular, plural = `${singular}s`) {
43
56
  return `${count} ${count === 1 ? singular : plural}`;
44
57
  }
@@ -1,8 +1,3 @@
1
- /**
2
- * Purpose: Share small ToolPresentation content helpers used by batch and compaction code.
3
- * Responsibilities: Extract text/image/path fields and format batch step command labels.
4
- * Scope: Pure ToolPresentation content helpers only.
5
- */
6
1
  export function isStringArray(value) {
7
2
  return Array.isArray(value) && value.every((item) => typeof item === "string");
8
3
  }
@@ -1,8 +1,3 @@
1
- /**
2
- * Purpose: Render diagnostic command families and safe redacted diagnostic data.
3
- * Responsibilities: Format sessions, profiles, auth/cookies/storage, network diagnostics, console/errors, stream/dashboard/chat, and build network follow-up actions.
4
- * Scope: Diagnostic/result-state command presentation only; core orchestration stays in presentation.ts.
5
- */
6
1
  import { isRecord } from "../../parsing.js";
7
2
  import { isSensitiveFieldName, redactSensitiveText, redactSensitiveValue } from "../../runtime.js";
8
3
  import { classifyNetworkRequestFailure, isApiLikeNetworkRequest, isNetworkArtifactNoiseRequest, summarizeNetworkFailures } from "../network.js";
@@ -66,7 +61,9 @@ export function getTabSummary(data) {
66
61
  ? String(tab.index)
67
62
  : String(index);
68
63
  const labelText = label && label !== tabSelector ? ` label=${redactModelFacingText(label)}` : "";
69
- return `${marker} [${tabSelector}]${labelText} ${title} ${url}`;
64
+ const targetId = typeof tab.targetId === "string" && tab.targetId.trim().length > 0 ? tab.targetId.trim() : undefined;
65
+ const targetText = targetId && targetId !== tabSelector ? ` target=${redactModelFacingText(targetId)}` : "";
66
+ return `${marker} [${tabSelector}]${labelText}${targetText} ${title} — ${url}`;
70
67
  });
71
68
  return lines.join("\n");
72
69
  }
@@ -353,6 +350,8 @@ function formatNetworkRequestLine(item, index) {
353
350
  }
354
351
  function formatNetworkRequestsText(data, commandInfo) {
355
352
  const requests = getArrayField(data, "requests");
353
+ if (isClearDiagnosticCommand(commandInfo) && data.cleared === true && !requests)
354
+ return "Network request buffer cleared.";
356
355
  if (!requests)
357
356
  return undefined;
358
357
  if (isClearDiagnosticCommand(commandInfo)) {
@@ -598,6 +597,8 @@ export function buildStreamNextActions(commandInfo, data, sessionName) {
598
597
  }
599
598
  function formatConsoleText(data, commandInfo) {
600
599
  const messages = getArrayField(data, "messages");
600
+ if (isClearDiagnosticCommand(commandInfo) && data.cleared === true && !messages)
601
+ return "Console buffer cleared.";
601
602
  if (!messages)
602
603
  return undefined;
603
604
  if (isClearDiagnosticCommand(commandInfo)) {
@@ -189,11 +189,12 @@ export function buildErrorPresentation(options) {
189
189
  const nextActions = [
190
190
  ...(buildUnknownCommandSuggestionActions(unknownCommandSuggestions, sessionName) ?? []),
191
191
  ...(browserProfileConfigRecovery?.actions ?? []),
192
- ...(buildAgentBrowserNextActions({
192
+ ...(browserProfileConfigRecovery ? [] : buildAgentBrowserNextActions({
193
193
  args,
194
194
  command: commandInfo.command,
195
195
  failureCategory: categoryDetails.failureCategory,
196
196
  resultCategory: "failure",
197
+ sessionName,
197
198
  }) ?? []),
198
199
  ];
199
200
  return {
@@ -1,8 +1,3 @@
1
- /**
2
- * Purpose: Compact oversized model-facing tool output into bounded previews and spill artifacts.
3
- * Responsibilities: Write full output to persistent/session temp storage, update artifact manifests, and preserve safe previews.
4
- * Scope: Large-output compaction only.
5
- */
6
1
  import { writePersistentSessionArtifactFile, writeSecureTempFile, } from "../../temp.js";
7
2
  import { buildEvictedSessionArtifactEntries } from "../artifact-manifest.js";
8
3
  import { countLines, truncateText } from "../text.js";
@@ -1,8 +1,3 @@
1
- /**
2
- * Purpose: Remove wrapper-owned managed session and state capabilities from caller-visible list results.
3
- * Responsibilities: Recognize managed list rows recursively and preserve the surrounding upstream result shape.
4
- * Scope: Presentation filtering only; command authorization lives in managed-session-state-policy.ts.
5
- */
6
1
  import { containsManagedSessionRestoreKey, isWrapperManagedSessionName } from "../../managed-session-capabilities.js";
7
2
  import { isRecord } from "../../parsing.js";
8
3
  function isWrapperManagedSessionListItem(item) {