pi-agent-browser-native 0.6.10 → 0.6.12

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 (48) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +50 -7
  3. package/dist/extensions/agent-browser/index.js +416 -430
  4. package/dist/extensions/agent-browser/lib/argv-grammar.js +1 -1
  5. package/dist/extensions/agent-browser/lib/command-policy.js +41 -2
  6. package/dist/extensions/agent-browser/lib/input-modes/job.js +7 -7
  7. package/dist/extensions/agent-browser/lib/input-modes/params.js +1 -1
  8. package/dist/extensions/agent-browser/lib/input-modes/script.js +3 -2
  9. package/dist/extensions/agent-browser/lib/managed-session-restore.js +40 -10
  10. package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +3 -0
  11. package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +8 -2
  12. package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +1 -0
  13. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/wait-timeouts.js +3 -2
  14. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +25 -14
  15. package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +56 -10
  16. package/dist/extensions/agent-browser/lib/orchestration/browser-run/recording-recovery.js +161 -0
  17. package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +3 -3
  18. package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +2 -4
  19. package/dist/extensions/agent-browser/lib/orchestration/native-session-defaults.js +68 -0
  20. package/dist/extensions/agent-browser/lib/orchestration/output-file.js +41 -6
  21. package/dist/extensions/agent-browser/lib/page-target-validation.js +9 -5
  22. package/dist/extensions/agent-browser/lib/playbook.js +10 -9
  23. package/dist/extensions/agent-browser/lib/process-environment.js +26 -8
  24. package/dist/extensions/agent-browser/lib/process.js +8 -5
  25. package/dist/extensions/agent-browser/lib/read-confirmation.js +59 -0
  26. package/dist/extensions/agent-browser/lib/recording-reservations.js +11 -1
  27. package/dist/extensions/agent-browser/lib/results/action-recommendations.js +8 -0
  28. package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +6 -5
  29. package/dist/extensions/agent-browser/lib/results/categories.js +4 -2
  30. package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +60 -29
  31. package/dist/extensions/agent-browser/lib/results/presentation/batch.js +19 -8
  32. package/dist/extensions/agent-browser/lib/results/presentation/common.js +0 -20
  33. package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +40 -38
  34. package/dist/extensions/agent-browser/lib/results/presentation/errors.js +1 -0
  35. package/dist/extensions/agent-browser/lib/results/presentation/navigation.js +3 -3
  36. package/dist/extensions/agent-browser/lib/results/presentation.js +38 -9
  37. package/dist/extensions/agent-browser/lib/results/recording.js +50 -0
  38. package/dist/extensions/agent-browser/lib/runtime.js +72 -20
  39. package/dist/extensions/agent-browser/lib/session-page-state.js +23 -7
  40. package/dist/extensions/agent-browser/lib/temp.js +4 -0
  41. package/docs/ARCHITECTURE.md +27 -11
  42. package/docs/COMMAND_REFERENCE.md +37 -17
  43. package/docs/RELEASE.md +3 -1
  44. package/docs/SUPPORT_MATRIX.md +10 -4
  45. package/docs/TOOL_CONTRACT.md +87 -25
  46. package/docs/platform-smoke.md +9 -1
  47. package/package.json +1 -1
  48. package/scripts/prepare.mjs +2 -4
@@ -1,3 +1,4 @@
1
+ import { stat } from "node:fs/promises";
1
2
  import { isCloseCommand } from "../../command-taxonomy.js";
2
3
  import { isRecord } from "../../parsing.js";
3
4
  import { getAgentBrowserSessionIdentityKey } from "../../argv-grammar.js";
@@ -194,7 +195,7 @@ async function buildBatchStepPresentation(options) {
194
195
  const redactedCommand = command ? redactInvocationArgs(command) : undefined;
195
196
  const commandText = formatBatchStepCommand(hasModelFacingArgRedaction(redactedCommand) ? redactedCommand : command, index);
196
197
  const lifecycle = extractAgentBrowserLifecycle(item.result);
197
- if (item.success === false) {
198
+ if (item.success === false && command?.[0] !== "record") {
198
199
  const redactedErrorData = redactBatchStepErrorData(command, item.error);
199
200
  const errorText = formatBatchStepError(redactedErrorData);
200
201
  const failureCategory = classifyAgentBrowserFailureCategory({
@@ -252,7 +253,9 @@ async function buildBatchStepPresentation(options) {
252
253
  commandInfo: commandInfoWithTokens,
253
254
  cwd,
254
255
  args: command,
255
- envelope: { data: item.result, success: true },
256
+ envelope: { data: item.result, success: item.success !== false, error: item.error },
257
+ errorText: item.success === false ? formatBatchStepError(redactBatchStepErrorData(command, item.error)) : undefined,
258
+ piCleanupOwnership: options.piCleanupOwnership,
256
259
  networkRouteDiagnostics,
257
260
  namespace,
258
261
  persistentArtifactStore,
@@ -320,11 +323,18 @@ async function buildBatchStepPresentation(options) {
320
323
  presentation,
321
324
  };
322
325
  }
323
- function abandonedRecordingArtifact(artifact) {
326
+ async function abandonedRecordingArtifact(artifact) {
324
327
  const { recordingState: _recordingState, willExistOnStop: _willExistOnStop, ...terminal } = artifact;
325
- return { ...terminal, exists: false, status: "missing", subcommand: "close-abandoned" };
328
+ try {
329
+ const file = await stat(artifact.absolutePath);
330
+ return { ...terminal, exists: file.isFile(), sizeBytes: file.size, status: file.isFile() ? "unverified" : "missing", subcommand: "close-abandoned" };
331
+ }
332
+ catch (error) {
333
+ const missing = error.code === "ENOENT";
334
+ return { ...terminal, exists: missing ? false : undefined, status: missing ? "missing" : "unverified", subcommand: "close-abandoned" };
335
+ }
326
336
  }
327
- function coalesceTerminalBatchRecordingArtifacts(steps, sessionName, namespace) {
337
+ async function coalesceTerminalBatchRecordingArtifacts(steps, sessionName, namespace) {
328
338
  const artifacts = [];
329
339
  const pendingIndexesBySession = new Map();
330
340
  const removedPendingIndexes = new Set();
@@ -351,7 +361,7 @@ function coalesceTerminalBatchRecordingArtifacts(steps, sessionName, namespace)
351
361
  for (const pendingIndex of pendingIndexesBySession.get(session) ?? []) {
352
362
  const pending = artifacts[pendingIndex];
353
363
  if (pending && !removedPendingIndexes.has(pendingIndex))
354
- artifacts[pendingIndex] = abandonedRecordingArtifact(pending);
364
+ artifacts[pendingIndex] = await abandonedRecordingArtifact(pending);
355
365
  }
356
366
  }
357
367
  return artifacts.filter((_, index) => !removedPendingIndexes.has(index));
@@ -372,6 +382,7 @@ export async function buildBatchPresentation(options) {
372
382
  cwd,
373
383
  index,
374
384
  item,
385
+ piCleanupOwnership: options.piCleanupOwnership,
375
386
  namespace,
376
387
  networkRoutes: currentNetworkRoutes,
377
388
  persistentArtifactStore: persistentArtifactStore ? { ...persistentArtifactStore, protectedPaths: protectedPersistentPaths } : undefined,
@@ -387,7 +398,7 @@ export async function buildBatchPresentation(options) {
387
398
  }
388
399
  const batchFailure = getBatchFailureDetails(steps);
389
400
  const images = steps.flatMap((step) => getPresentationImages(step.presentation));
390
- const artifacts = coalesceTerminalBatchRecordingArtifacts(steps, sessionName, namespace);
401
+ const artifacts = await coalesceTerminalBatchRecordingArtifacts(steps, sessionName, namespace);
391
402
  const artifactVerification = buildArtifactVerificationSummary(artifacts);
392
403
  const fullOutputPaths = steps.flatMap((step) => getPresentationPaths({
393
404
  primaryPath: step.presentation.fullOutputPath,
@@ -399,7 +410,7 @@ export async function buildBatchPresentation(options) {
399
410
  }));
400
411
  const redactedBatchData = steps.map(({ details }) => (details.success
401
412
  ? { command: details.command, result: details.data, success: true }
402
- : { command: details.command, error: details.text, success: false }));
413
+ : { command: details.command, error: details.text, ...(details.command?.[0] === "record" ? { result: details.data } : {}), success: false }));
403
414
  const unverifiedMutationCount = steps.filter((step) => step.details.pageChangeSummary?.changeType === "mutation" && step.details.pageChangeSummary.observed === false).length;
404
415
  const mutationEvidenceText = unverifiedMutationCount > 0
405
416
  ? `Mutation evidence: ${unverifiedMutationCount} action result${unverifiedMutationCount === 1 ? " proves" : "s prove"} dispatch only, not application state change. Use explicit later assertions or external receipts as postconditions; fixed waits are not postconditions.`
@@ -5,29 +5,9 @@ const UNTITLED_PAGE_SUMMARY = "(untitled page)";
5
5
  export function stringifyModelFacing(value) {
6
6
  return stringifyUnknown(redactSensitiveValue(value));
7
7
  }
8
- export function parseJsonPreviewString(value) {
9
- const trimmed = value.trim();
10
- if (!trimmed.startsWith("{") && !trimmed.startsWith("["))
11
- return value;
12
- try {
13
- return JSON.parse(trimmed);
14
- }
15
- catch {
16
- return value;
17
- }
18
- }
19
8
  export function redactModelFacingText(text) {
20
- const parsed = parseJsonPreviewString(text);
21
- if (parsed !== text) {
22
- return stringifyModelFacing(parsed);
23
- }
24
9
  return redactSensitiveText(text);
25
10
  }
26
- export function redactModelFacingTextIfSensitive(text) {
27
- return /(?:@|\b(?:access[_-]?key|api[_-]?key|auth|authorization|basic|bearer|connection[_-]?string|cookie|database[_-]?url|db[_-]?url|mongo(?:db)?[_-]?uri|pass(?:word)?|private[_-]?key|redis[_-]?url|secret|session[_-]?id|token)\b)/i.test(text)
28
- ? redactModelFacingText(text)
29
- : text;
30
- }
31
11
  export function getArrayField(data, key) {
32
12
  return Array.isArray(data[key]) ? data[key] : undefined;
33
13
  }
@@ -1,9 +1,9 @@
1
1
  import { isRecord } from "../../parsing.js";
2
- import { isSensitiveFieldName, redactSensitiveText, redactSensitiveValue } from "../../runtime.js";
2
+ import { redactSensitiveText, redactSensitiveValue } from "../../runtime.js";
3
3
  import { classifyNetworkRequestFailure, isApiLikeNetworkRequest, isNetworkArtifactNoiseRequest, summarizeNetworkFailures } from "../network.js";
4
4
  import { withOptionalSessionArgs } from "../next-actions.js";
5
5
  import { stringifyUnknown, truncateText } from "../text.js";
6
- import { firstLine, formatCount, getArrayField, getStringField, parseJsonPreviewString, redactModelFacingText, redactModelFacingTextIfSensitive, stringifyModelFacing, } from "./common.js";
6
+ import { firstLine, formatCount, getArrayField, getStringField, redactModelFacingText, stringifyModelFacing, } from "./common.js";
7
7
  const DIAGNOSTIC_REQUEST_PREVIEW_LIMIT = 40;
8
8
  const DIAGNOSTIC_LOG_PREVIEW_LIMIT = 80;
9
9
  const NETWORK_BODY_PREVIEW_MAX_CHARS = 280;
@@ -185,11 +185,11 @@ export function formatDiagnosticSummary(commandInfo, data) {
185
185
  }
186
186
  if (commandInfo.subcommand === "route") {
187
187
  const routed = getStringField(data, "routed") ?? getStringField(data, "url") ?? getStringField(data, "pattern");
188
- return routed ? `Network route: ${redactModelFacingTextIfSensitive(routed)}` : "Network route configured";
188
+ return routed ? `Network route: ${redactSensitiveText(routed)}` : "Network route configured";
189
189
  }
190
190
  if (commandInfo.subcommand === "unroute") {
191
191
  const unrouted = getStringField(data, "unrouted") ?? getStringField(data, "url") ?? getStringField(data, "pattern");
192
- return unrouted ? `Network unroute: ${redactModelFacingTextIfSensitive(unrouted)}` : "Network route removed";
192
+ return unrouted ? `Network unroute: ${redactSensitiveText(unrouted)}` : "Network route removed";
193
193
  }
194
194
  if (commandInfo.subcommand === "har") {
195
195
  const state = getStringField(data, "state") ?? getStringField(data, "status") ?? commandInfo.subcommand;
@@ -273,14 +273,33 @@ function formatSessionText(data) {
273
273
  const metadata = [
274
274
  `active=${active ? "true" : "false"}`,
275
275
  label ? `label=${redactModelFacingText(label)}` : undefined,
276
- title ? `title=${redactModelFacingTextIfSensitive(title)}` : undefined,
277
- url ? `url=${redactModelFacingTextIfSensitive(url)}` : undefined,
276
+ title ? `title=${redactSensitiveText(title)}` : undefined,
277
+ url ? `url=${redactSensitiveText(url)}` : undefined,
278
278
  tabCount,
279
279
  ].filter(Boolean).join("; ");
280
280
  return `${index + 1}. name=${name}${active ? " *active*" : ""}; ${metadata}`;
281
281
  })
282
282
  .join("\n");
283
283
  }
284
+ if (typeof data.active === "boolean") {
285
+ const runtime = data.runtime;
286
+ const browser = isRecord(runtime) && isRecord(runtime.browser) ? runtime.browser : undefined;
287
+ const identity = [
288
+ `Daemon: ${data.active ? "active" : "inactive"}; PID: ${data.pid ?? "unknown"}`,
289
+ `Browser: ${browser?.status ?? "unknown"}; alive: ${browser?.alive ?? "unknown"}; Chrome PID: ${browser?.pid ?? "unknown"}`,
290
+ `Exact profile: ${browser?.userDataDir ?? "unknown"}`,
291
+ `Native browser ownership: ${browser?.ownership ?? "unknown"}; Pi cleanup ownership: ${data.piCleanupOwnership ?? "unknown"}`,
292
+ ].join("\n");
293
+ return `${redactModelFacingText(identity)}\n\n${stringifyModelFacing({
294
+ ...Object.fromEntries(["session", "namespace", "socketDir", "active", "pid", "version", "runtimeError"].map((key) => [key, data[key]])),
295
+ // Native runtime also includes restore check URLs, text and code; show status/identity only.
296
+ runtime: isRecord(runtime) ? Object.fromEntries([
297
+ "session", "namespace", "socketDir", "backgroundPid", "browserLaunched", "browser", "recording", "capabilities", "pageCount", "engine", "launchHash",
298
+ "compatibilityStatus", "restoreKey", "restoreStatus", "restoreLoadedPath", "restoreValidationPending",
299
+ "restoreSave", "saveStatus", "restoreSavedPath",
300
+ ].map((key) => [key, runtime[key]])) : runtime,
301
+ })}`;
302
+ }
284
303
  const session = getStringField(data, "session");
285
304
  return session ? `Current session: ${redactModelFacingText(session)}` : undefined;
286
305
  }
@@ -315,8 +334,7 @@ function getPreviewCandidate(item, keys) {
315
334
  function formatNetworkPreviewValue(value, maxChars) {
316
335
  if (value === undefined || value === null)
317
336
  return undefined;
318
- const previewValue = typeof value === "string" ? parseJsonPreviewString(value) : value;
319
- const redacted = redactSensitiveValue(previewValue);
337
+ const redacted = redactSensitiveValue(value);
320
338
  const raw = typeof redacted === "string" ? redacted : stringifyUnknown(redacted);
321
339
  const normalized = raw.replace(/\s+/g, " ").trim();
322
340
  if (normalized.length === 0)
@@ -797,7 +815,7 @@ function valueContainsStorageSecret(value) {
797
815
  return true;
798
816
  }
799
817
  catch { }
800
- if (redactSensitiveText(trimmed) !== trimmed || redactModelFacingTextIfSensitive(trimmed) !== trimmed)
818
+ if (redactSensitiveText(trimmed) !== trimmed)
801
819
  return true;
802
820
  try {
803
821
  return valueContainsStorageSecret(JSON.parse(trimmed));
@@ -830,13 +848,13 @@ function formatStorageValue(key, value) {
830
848
  }
831
849
  function redactStorageEntryValue(item) {
832
850
  if (!Object.hasOwn(item, "value"))
833
- return redactStructuredPresentationValue(item);
851
+ return redactSensitiveValue(item);
834
852
  const key = getStringField(item, "key") ?? getStringField(item, "name");
835
853
  const value = item.value;
836
854
  if (shouldRevealStorageValue(key, value))
837
- return redactStructuredPresentationValue(item);
855
+ return redactSensitiveValue(item);
838
856
  return {
839
- ...redactStructuredPresentationValue({ ...item, value: undefined }),
857
+ ...redactSensitiveValue({ ...item, value: undefined }),
840
858
  value: "[REDACTED]",
841
859
  valueRedacted: true,
842
860
  valueRedactionReason: key && STORAGE_SECRET_KEY_PATTERN.test(key) ? "sensitive-key" : "sensitive-value",
@@ -846,15 +864,15 @@ function redactStorageData(value) {
846
864
  if (Array.isArray(value))
847
865
  return value.map((item) => redactStorageData(item));
848
866
  if (!isRecord(value))
849
- return redactStructuredPresentationValue(value);
867
+ return redactSensitiveValue(value);
850
868
  const entries = Object.fromEntries(Object.entries(value).map(([key, entryValue]) => {
851
869
  if ((key === "entries" || key === "items") && Array.isArray(entryValue))
852
- return [key, entryValue.map((item) => isRecord(item) ? redactStorageEntryValue(item) : redactStructuredPresentationValue(item))];
870
+ return [key, entryValue.map((item) => isRecord(item) ? redactStorageEntryValue(item) : redactSensitiveValue(item))];
853
871
  if (key === "value") {
854
872
  const itemKey = getStringField(value, "key") ?? getStringField(value, "name");
855
- return [key, shouldRevealStorageValue(itemKey, entryValue) ? redactStructuredPresentationValue(entryValue) : "[REDACTED]"];
873
+ return [key, shouldRevealStorageValue(itemKey, entryValue) ? redactSensitiveValue(entryValue) : "[REDACTED]"];
856
874
  }
857
- return [key, redactStructuredPresentationValue(entryValue)];
875
+ return [key, redactSensitiveValue(entryValue)];
858
876
  }));
859
877
  if (Object.hasOwn(value, "value")) {
860
878
  const key = getStringField(value, "key") ?? getStringField(value, "name");
@@ -899,7 +917,7 @@ function formatDialogText(data) {
899
917
  lines.push(`Type: ${redactModelFacingText(type)}`);
900
918
  const message = getStringField(data, "message");
901
919
  if (message)
902
- lines.push(`Message: ${/(?:auth|authorization|bearer|cookie|pass(?:word)?|secret|session|token)/i.test(message) ? "[REDACTED]" : redactModelFacingText(message)}`);
920
+ lines.push(`Message: ${redactModelFacingText(message)}`);
903
921
  if (data.accepted === true)
904
922
  lines.push("Accepted.");
905
923
  if (data.dismissed === true)
@@ -910,7 +928,7 @@ function formatFrameText(data) {
910
928
  const frame = getStringField(data, "frame") ?? getStringField(data, "name") ?? getStringField(data, "selector");
911
929
  const url = getStringField(data, "url");
912
930
  const title = getStringField(data, "title");
913
- const lines = [frame ? `Frame: ${redactModelFacingText(frame)}` : undefined, title ? `Title: ${redactModelFacingText(title)}` : undefined, url ? `URL: ${redactModelFacingTextIfSensitive(url)}` : undefined].filter(Boolean);
931
+ const lines = [frame ? `Frame: ${redactModelFacingText(frame)}` : undefined, title ? `Title: ${redactModelFacingText(title)}` : undefined, url ? `URL: ${redactSensitiveText(url)}` : undefined].filter(Boolean);
914
932
  return lines.length > 0 ? lines.join("\n") : undefined;
915
933
  }
916
934
  function formatStateText(data, subcommand) {
@@ -933,10 +951,10 @@ function formatStateText(data, subcommand) {
933
951
  return states
934
952
  .map((item, index) => {
935
953
  if (!isRecord(item))
936
- return `${index + 1}. ${redactModelFacingTextIfSensitive(stringifyModelFacing(item))}`;
954
+ return `${index + 1}. ${stringifyModelFacing(item)}`;
937
955
  const name = getStringField(item, "name") ?? getStringField(item, "file") ?? getStringField(item, "path") ?? `(state ${index + 1})`;
938
956
  const url = getStringField(item, "url");
939
- return url ? `${index + 1}. ${redactModelFacingText(name)} — ${redactModelFacingTextIfSensitive(url)}` : `${index + 1}. ${redactModelFacingText(name)}`;
957
+ return url ? `${index + 1}. ${redactModelFacingText(name)} — ${redactSensitiveText(url)}` : `${index + 1}. ${redactModelFacingText(name)}`;
940
958
  })
941
959
  .join("\n");
942
960
  }
@@ -946,23 +964,11 @@ function formatStateText(data, subcommand) {
946
964
  return "State cleared.";
947
965
  return undefined;
948
966
  }
949
- function redactStructuredPresentationValue(value) {
950
- if (typeof value === "string")
951
- return redactModelFacingTextIfSensitive(value);
952
- if (Array.isArray(value))
953
- return value.map((item) => redactStructuredPresentationValue(item));
954
- if (!isRecord(value))
955
- return value;
956
- return Object.fromEntries(Object.entries(value).map(([key, entryValue]) => [
957
- key,
958
- isSensitiveFieldName(key) ? "[REDACTED]" : redactStructuredPresentationValue(entryValue),
959
- ]));
960
- }
961
967
  function redactStatefulValues(value, sensitiveKeys) {
962
968
  if (Array.isArray(value))
963
969
  return value.map((item) => redactStatefulValues(item, sensitiveKeys));
964
970
  if (!isRecord(value))
965
- return redactStructuredPresentationValue(value);
971
+ return redactSensitiveValue(value);
966
972
  return Object.fromEntries(Object.entries(value).map(([key, entryValue]) => [
967
973
  key,
968
974
  sensitiveKeys.has(key.toLowerCase()) ? "[REDACTED]" : redactStatefulValues(entryValue, sensitiveKeys),
@@ -973,13 +979,9 @@ export function redactPresentationData(commandInfo, data) {
973
979
  return redactStatefulValues(data, new Set(["value"]));
974
980
  if (commandInfo.command === "storage")
975
981
  return redactStorageData(data);
976
- if (commandInfo.command === "session" && commandInfo.subcommand === "list")
977
- return redactStructuredPresentationValue(data);
978
- if (commandInfo.command === "state" && commandInfo.subcommand === "list")
979
- return redactStructuredPresentationValue(data);
980
982
  if (commandInfo.command === "state" && commandInfo.subcommand === "show")
981
983
  return redactStatefulValues(data, new Set(["value"]));
982
- return redactStructuredPresentationValue(data);
984
+ return redactSensitiveValue(data);
983
985
  }
984
986
  export function formatDiagnosticText(commandInfo, data) {
985
987
  if (commandInfo.command === "session")
@@ -203,6 +203,7 @@ export function buildErrorPresentation(options) {
203
203
  overlayBlockedClick: isOverlayBlockedClickError(presentationCommand ?? commandInfo.command, safeErrorText, args ?? commandInfo.commandTokens),
204
204
  resultCategory: "failure",
205
205
  sessionName,
206
+ subcommand: commandInfo.subcommand,
206
207
  }) ?? []),
207
208
  ];
208
209
  return {
@@ -1,7 +1,7 @@
1
1
  import { isNavigationObservableCommandName, isOpenNavigationCommand, isPageChangeSummaryCommand } from "../../command-taxonomy.js";
2
2
  import { isRecord } from "../../parsing.js";
3
3
  import { detectConfirmationRequired } from "../confirmation.js";
4
- import { omitUpstreamLifecycle, redactModelFacingText, stringifyModelFacing } from "./common.js";
4
+ import { firstLine, omitUpstreamLifecycle, redactModelFacingText, stringifyModelFacing } from "./common.js";
5
5
  const NAVIGATION_SUMMARY_FIELD = "navigationSummary";
6
6
  const GET_RESULT_FIELDS = {
7
7
  attr: "value",
@@ -25,7 +25,7 @@ function getScalarExtractionResult(commandInfo, data) {
25
25
  if (result === null || result === undefined)
26
26
  return "null";
27
27
  if (typeof result === "object")
28
- return JSON.stringify(result);
28
+ return JSON.stringify(result, null, 2);
29
29
  return undefined;
30
30
  }
31
31
  function getExtractionOrigin(data) {
@@ -52,7 +52,7 @@ export function formatExtractionSummary(commandInfo, data) {
52
52
  return undefined;
53
53
  }
54
54
  const safeScalarResult = redactModelFacingText(scalarResult);
55
- const firstResultLine = safeScalarResult.split("\n", 1)[0] ?? safeScalarResult;
55
+ const firstResultLine = firstLine(safeScalarResult);
56
56
  if (commandInfo.command === "get") {
57
57
  return `${formatGetSummaryLabel(commandInfo.subcommand)}: ${firstResultLine}`;
58
58
  }
@@ -1,10 +1,11 @@
1
1
  import { isRecord } from "../parsing.js";
2
+ import { buildReadConfirmationNextActions, nextReadConfirmation } from "../read-confirmation.js";
2
3
  import { extractUpstreamCommandTokens, parseCommandInfo, redactInvocationArgs } from "../runtime.js";
3
4
  import { buildAgentBrowserNextActions } from "./action-recommendations.js";
4
5
  import { buildAgentBrowserResultCategoryDetails } from "./categories.js";
5
6
  import { detectConfirmationRequired } from "./confirmation.js";
6
7
  import { buildSnapshotPresentation } from "./snapshot.js";
7
- import { parseJsonPreviewString, redactModelFacingText, stringifyModelFacing } from "./presentation/common.js";
8
+ import { redactModelFacingText } from "./presentation/common.js";
8
9
  import { applyArtifactManifest, attachInlineImage, buildArtifactVerificationSummary, buildManifestEntriesForFileArtifacts, classifyPresentationSuccessCategory, extractFileArtifacts, extractImagePath, formatArtifactMetadataLines, formatArtifactSummary, formatMissingArtifactFailureText, getSavedFileDetails, hasMissingFileArtifact, isManifestFileArtifact, } from "./presentation/artifacts.js";
9
10
  import { buildBatchPresentation, isAgentBrowserBatchResultArray, redactBatchStepErrorData } from "./presentation/batch.js";
10
11
  import { getPresentationPaths, isStringArray } from "./presentation/content.js";
@@ -18,8 +19,7 @@ function sanitizeModelFacingPresentation(presentation) {
18
19
  presentation.content = presentation.content.map((item) => {
19
20
  if (item.type !== "text")
20
21
  return item;
21
- const parsed = parseJsonPreviewString(item.text);
22
- return parsed === item.text ? item : { ...item, text: stringifyModelFacing(parsed) };
22
+ return { ...item, text: redactModelFacingText(item.text) };
23
23
  });
24
24
  presentation.summary = redactModelFacingText(presentation.summary);
25
25
  return presentation;
@@ -52,7 +52,10 @@ export async function buildToolPresentation(options) {
52
52
  const { args, artifactManifest, artifactRequest, commandInfo, compiledSemanticAction, cwd, envelope, errorText, namespace, networkRouteDiagnostics, networkRoutes, persistentArtifactStore, sessionName, } = options;
53
53
  const commandInfoWithTokens = commandInfo.commandTokens || !args ? commandInfo : { ...commandInfo, commandTokens: extractUpstreamCommandTokens(args) };
54
54
  const presentationCommandInfo = resolvePresentationCommandInfo(commandInfoWithTokens, compiledSemanticAction);
55
- if (errorText) {
55
+ const recordingCommand = commandInfo.command === "record";
56
+ const recordingBatch = commandInfo.command === "batch" && isAgentBrowserBatchResultArray(envelope?.data)
57
+ && envelope.data.some((row) => row.command?.[0] === "record");
58
+ if (errorText && !recordingCommand && !recordingBatch) {
56
59
  return buildErrorPresentation({
57
60
  args,
58
61
  commandInfo,
@@ -61,11 +64,15 @@ export async function buildToolPresentation(options) {
61
64
  sessionName,
62
65
  });
63
66
  }
64
- const data = enrichStreamStatusData(commandInfoWithTokens, envelope?.data);
67
+ let data = enrichStreamStatusData(commandInfoWithTokens, envelope?.data);
68
+ if (commandInfo.command === "session" && commandInfo.subcommand === "info" && isRecord(data)) {
69
+ data = { ...data, piCleanupOwnership: options.piCleanupOwnership ?? "unknown" };
70
+ }
71
+ const readConfirmation = nextReadConfirmation({ commandTokens: commandInfoWithTokens.commandTokens ?? [], data, namespace, sessionName: sessionName ?? "default", succeeded: envelope?.success !== false });
65
72
  const presentationData = commandInfo.command === "batch" && isAgentBrowserBatchResultArray(data)
66
73
  ? redactBatchSpillData(data)
67
74
  : redactPresentationData(commandInfoWithTokens, data);
68
- const artifacts = await extractFileArtifacts({ artifactManifest, artifactMaxUpdatedAtMs: options.artifactMaxUpdatedAtMs, artifactMinUpdatedAtMs: options.artifactMinUpdatedAtMs, artifactRequest, commandInfo: presentationCommandInfo, cwd, data, namespace, sessionName });
75
+ const artifacts = await extractFileArtifacts({ artifactManifest, artifactMaxUpdatedAtMs: options.artifactMaxUpdatedAtMs, artifactMinUpdatedAtMs: options.artifactMinUpdatedAtMs, artifactRequest, commandInfo: presentationCommandInfo, cwd, data, namespace, recordingOutcome: recordingCommand ? envelope?.success : undefined, recordingPending: options.recordingPending, sessionName });
69
76
  const artifactVerification = buildArtifactVerificationSummary(artifacts);
70
77
  const artifactSummary = formatArtifactSummary(artifacts);
71
78
  const summary = artifactSummary ?? formatPresentationSummary(commandInfoWithTokens, data, compiledSemanticAction);
@@ -83,6 +90,7 @@ export async function buildToolPresentation(options) {
83
90
  namespace,
84
91
  networkRoutes,
85
92
  persistentArtifactStore,
93
+ piCleanupOwnership: options.piCleanupOwnership,
86
94
  sessionName,
87
95
  summary,
88
96
  });
@@ -99,6 +107,12 @@ export async function buildToolPresentation(options) {
99
107
  summary,
100
108
  };
101
109
  }
110
+ if (errorText && (recordingCommand || recordingBatch)) {
111
+ const errorPresentation = buildErrorPresentation({ args, commandInfo, errorText, presentationCommand: presentationCommandInfo.command, sessionName });
112
+ presentation = { ...presentation, resultCategory: "failure", failureCategory: errorPresentation.failureCategory, summary: errorPresentation.summary,
113
+ content: [{ type: "text", text: `${errorPresentation.content[0]?.type === "text" ? errorPresentation.content[0].text : errorText}\n\n${presentation.content[0]?.type === "text" ? presentation.content[0].text : ""}` }],
114
+ };
115
+ }
102
116
  if (networkRouteDiagnostics && networkRouteDiagnostics.length > 0 && presentation.content[0]?.type === "text") {
103
117
  const diagnosticText = formatNetworkRouteDiagnosticsText(networkRouteDiagnostics);
104
118
  if (diagnosticText)
@@ -141,7 +155,7 @@ export async function buildToolPresentation(options) {
141
155
  presentationWithManifest.artifactVerification = buildArtifactVerificationSummary(artifacts, presentationWithManifest.artifactManifest, currentSpillPaths) ?? presentationWithManifest.artifactVerification;
142
156
  const confirmationRequired = detectConfirmationRequired(data);
143
157
  const missingArtifactFailureText = formatMissingArtifactFailureText(presentationWithManifest.artifacts);
144
- if (missingArtifactFailureText && hasMissingFileArtifact(presentationWithManifest.artifacts)) {
158
+ if (!errorText && missingArtifactFailureText && hasMissingFileArtifact(presentationWithManifest.artifacts)) {
145
159
  presentationWithManifest.resultCategory = "failure";
146
160
  presentationWithManifest.failureCategory = "artifact-missing";
147
161
  presentationWithManifest.successCategory = undefined;
@@ -153,6 +167,21 @@ export async function buildToolPresentation(options) {
153
167
  presentationWithManifest.content.unshift({ type: "text", text: missingArtifactFailureText });
154
168
  }
155
169
  }
170
+ const failedRecording = presentationWithManifest.artifacts?.find((artifact) => artifact.recording?.success === false || artifact.recording?.output.encoderSucceeded === false);
171
+ if (failedRecording && !errorText) {
172
+ const failure = `Recording failed: ${failedRecording.recording?.error ?? "native capture/encoder failure"}`;
173
+ presentationWithManifest.resultCategory = "failure";
174
+ presentationWithManifest.failureCategory = "upstream-error";
175
+ presentationWithManifest.successCategory = undefined;
176
+ presentationWithManifest.summary = failure;
177
+ presentationWithManifest.content.unshift({ type: "text", text: failure });
178
+ }
179
+ if (readConfirmation?.state === "pending") {
180
+ presentationWithManifest.readConfirmation = readConfirmation;
181
+ presentationWithManifest.resultCategory = "failure";
182
+ presentationWithManifest.failureCategory = "confirmation-required";
183
+ presentationWithManifest.successCategory = undefined;
184
+ }
156
185
  if (!presentationWithManifest.resultCategory) {
157
186
  const categoryDetails = buildAgentBrowserResultCategoryDetails({
158
187
  artifacts: presentationWithManifest.artifacts,
@@ -160,7 +189,7 @@ export async function buildToolPresentation(options) {
160
189
  confirmationRequired: confirmationRequired !== undefined,
161
190
  errorText: envelope?.success === false ? presentationWithManifest.summary : undefined,
162
191
  savedFile: presentationWithManifest.savedFile,
163
- succeeded: envelope?.success !== false,
192
+ succeeded: envelope?.success !== false && confirmationRequired === undefined,
164
193
  });
165
194
  presentationWithManifest.resultCategory = categoryDetails.resultCategory;
166
195
  presentationWithManifest.successCategory = categoryDetails.resultCategory === "success"
@@ -196,7 +225,7 @@ export async function buildToolPresentation(options) {
196
225
  ? buildNetworkRequestsNextActions(data, sessionName, presentationWithManifest.networkRouteDiagnostics)
197
226
  : undefined;
198
227
  const streamNextActions = presentationWithManifest.resultCategory === "success" ? buildStreamNextActions(commandInfoWithTokens, data, sessionName) : undefined;
199
- presentationWithManifest.nextActions = mergeNextActions(presentationWithManifest.nextActions, genericNextActions, networkNextActions, streamNextActions);
228
+ presentationWithManifest.nextActions = readConfirmation ? buildReadConfirmationNextActions(readConfirmation, true) : mergeNextActions(presentationWithManifest.nextActions, genericNextActions, networkNextActions, streamNextActions);
200
229
  presentationWithManifest.pageChangeSummary = presentationWithManifest.pageChangeSummary ?? buildPageChangeSummary({
201
230
  artifacts: presentationWithManifest.artifacts,
202
231
  commandInfo: presentationCommandInfo,
@@ -0,0 +1,50 @@
1
+ import { isRecord } from "../parsing.js";
2
+ const RECORDING_QUALITY_WARNING = "Capture quality warning: repaint-driven capture, held/repeated or static frames, and late/final-state-only frames cannot establish UI smoothness. Output FPS is not captured-frame rate; inspect the recording and capture window before judging motion.";
3
+ function number(value) {
4
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
5
+ }
6
+ function text(value) {
7
+ return typeof value === "string" && value.length > 0 ? value : null;
8
+ }
9
+ export function getRecordingReceipt(value, outcome) {
10
+ if (!isRecord(value) || typeof value.path !== "string" || !value.path)
11
+ return undefined;
12
+ const capture = isRecord(value.capture) ? value.capture : {};
13
+ const output = isRecord(value.output) ? value.output : {};
14
+ const file = isRecord(value.file) ? value.file : {};
15
+ return {
16
+ warning: RECORDING_QUALITY_WARNING,
17
+ recordingId: text(value.recordingId), path: value.path,
18
+ success: typeof value.success === "boolean" ? value.success : value.success === null ? null : outcome ?? null,
19
+ error: text(value.error), frames: number(value.frames), capturedFrames: number(value.capturedFrames), fps: number(value.fps),
20
+ capture: {
21
+ startedAt: text(capture.startedAt), endedAt: text(capture.endedAt), durationMs: number(capture.durationMs),
22
+ firstFrameAt: text(capture.firstFrameAt), lastFrameAt: text(capture.lastFrameAt), firstFrameAfterMs: number(capture.firstFrameAfterMs), lastFrameAfterMs: number(capture.lastFrameAfterMs),
23
+ averageFps: number(capture.averageFps), maxFrameGapMs: number(capture.maxFrameGapMs), timestampSource: text(capture.timestampSource),
24
+ },
25
+ output: {
26
+ frames: number(output.frames ?? value.frames), fps: number(output.fps ?? value.fps), encodedFrames: number(output.encodedFrames),
27
+ durationMs: number(output.durationMs), durationSource: text(output.durationSource), heldFrames: number(output.heldFrames), droppedFrames: number(output.droppedFrames), skippedFrames: number(output.skippedFrames),
28
+ encoderSucceeded: typeof output.encoderSucceeded === "boolean" ? output.encoderSucceeded : null,
29
+ },
30
+ file: { exists: typeof file.exists === "boolean" ? file.exists : null, sizeBytes: number(file.sizeBytes) },
31
+ };
32
+ }
33
+ export function formatRecordingReceipt(receipt) {
34
+ const metric = (value, unit = "") => value === null ? "unknown" : `${value}${unit}`;
35
+ return [
36
+ `Recording ID: ${metric(receipt.recordingId)}`,
37
+ `Native recording outcome: ${receipt.success === true ? "succeeded" : receipt.success === false ? "failed" : "pending/unknown"}${receipt.error ? ` — ${receipt.error}` : ""}`,
38
+ `Capture started: ${metric(receipt.capture.startedAt)}; ended: ${metric(receipt.capture.endedAt)}`,
39
+ `Wall-clock capture duration: ${metric(receipt.capture.durationMs, " ms")}`,
40
+ `Captured frames (received, not pixel-unique): ${metric(receipt.capturedFrames)}`,
41
+ `Captured-frame rate: ${metric(receipt.capture.averageFps, " fps")}`,
42
+ `First frame: ${metric(receipt.capture.firstFrameAt)}; after start: ${metric(receipt.capture.firstFrameAfterMs, " ms")}`,
43
+ `Last frame: ${metric(receipt.capture.lastFrameAt)}; after start: ${metric(receipt.capture.lastFrameAfterMs, " ms")}`,
44
+ `Maximum frame gap: ${metric(receipt.capture.maxFrameGapMs, " ms")}; timestamp source: ${metric(receipt.capture.timestampSource)}`,
45
+ `Written frames: ${metric(receipt.output.frames)}; Encoded frames: ${metric(receipt.output.encodedFrames)}`,
46
+ `Held frames: ${metric(receipt.output.heldFrames)}; dropped frames: ${metric(receipt.output.droppedFrames)}; skipped frames: ${metric(receipt.output.skippedFrames)}`,
47
+ `Nominal/output FPS: ${metric(receipt.output.fps)}; output duration (encoded frames/fps): ${metric(receipt.output.durationMs, " ms")}`,
48
+ receipt.warning,
49
+ ].join("\n");
50
+ }