pi-agent-browser-native 0.5.0 → 0.6.6

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 (64) hide show
  1. package/CHANGELOG.md +144 -0
  2. package/README.md +75 -42
  3. package/dist/extensions/agent-browser/index.js +13 -83
  4. package/dist/extensions/agent-browser/lib/argv-grammar.js +8 -2
  5. package/dist/extensions/agent-browser/lib/batch-lifecycle.js +1 -1
  6. package/dist/extensions/agent-browser/lib/command-policy.js +4 -7
  7. package/dist/extensions/agent-browser/lib/command-taxonomy.js +19 -11
  8. package/dist/extensions/agent-browser/lib/config-policy.js +25 -1
  9. package/dist/extensions/agent-browser/lib/config.js +1 -1
  10. package/dist/extensions/agent-browser/lib/input-modes/job.js +0 -9
  11. package/dist/extensions/agent-browser/lib/input-modes/params.js +1 -1
  12. package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +18 -4
  13. package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +3 -138
  14. package/dist/extensions/agent-browser/lib/managed-session-restore.js +1 -81
  15. package/dist/extensions/agent-browser/lib/managed-session-storage.js +4 -1
  16. package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +31 -26
  17. package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +60 -8
  18. package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +1 -4
  19. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/snapshot-filter.js +119 -2
  20. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/wait-timeouts.js +7 -6
  21. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +31 -40
  22. package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +76 -70
  23. package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +6 -8
  24. package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +5 -10
  25. package/dist/extensions/agent-browser/lib/orchestration/output-file.js +41 -21
  26. package/dist/extensions/agent-browser/lib/page-target-validation.js +270 -0
  27. package/dist/extensions/agent-browser/lib/playbook.js +13 -12
  28. package/dist/extensions/agent-browser/lib/process-identity.js +11 -10
  29. package/dist/extensions/agent-browser/lib/process.js +18 -82
  30. package/dist/extensions/agent-browser/lib/recording-reservations.js +11 -78
  31. package/dist/extensions/agent-browser/lib/results/action-recommendations.js +1 -1
  32. package/dist/extensions/agent-browser/lib/results/presentation/batch.js +27 -14
  33. package/dist/extensions/agent-browser/lib/results/presentation/common.js +20 -2
  34. package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +11 -16
  35. package/dist/extensions/agent-browser/lib/results/presentation/navigation.js +12 -9
  36. package/dist/extensions/agent-browser/lib/results/presentation/registry.js +2 -2
  37. package/dist/extensions/agent-browser/lib/results/presentation.js +31 -4
  38. package/dist/extensions/agent-browser/lib/results/recovery-actions.js +1 -1
  39. package/dist/extensions/agent-browser/lib/results/recovery-next-actions.js +9 -0
  40. package/dist/extensions/agent-browser/lib/results/selector-recovery.js +3 -3
  41. package/dist/extensions/agent-browser/lib/results/snapshot-spill.js +2 -1
  42. package/dist/extensions/agent-browser/lib/results/snapshot.js +4 -4
  43. package/dist/extensions/agent-browser/lib/runtime.js +73 -72
  44. package/dist/extensions/agent-browser/lib/session-page-state.js +12 -3
  45. package/dist/extensions/agent-browser/lib/temp.js +1 -2
  46. package/dist/extensions/agent-browser/lib/upstream-version.js +5 -5
  47. package/dist/extensions/agent-browser/lib/web-search.js +108 -24
  48. package/dist/scripts/agent-browser-target.mjs +19 -1
  49. package/docs/ARCHITECTURE.md +24 -20
  50. package/docs/COMMAND_REFERENCE.md +181 -49
  51. package/docs/ELECTRON.md +2 -2
  52. package/docs/RELEASE.md +10 -8
  53. package/docs/REQUIREMENTS.md +8 -7
  54. package/docs/SUPPORT_MATRIX.md +31 -26
  55. package/docs/TOOL_CONTRACT.md +89 -56
  56. package/package.json +1 -1
  57. package/scripts/agent-browser-capability-baseline.mjs +65 -5
  58. package/scripts/agent-browser-target.mjs +19 -1
  59. package/scripts/config.mjs +1 -0
  60. package/scripts/doctor.mjs +15 -9
  61. package/dist/extensions/agent-browser/lib/managed-session-capabilities.js +0 -20
  62. package/dist/extensions/agent-browser/lib/managed-session-state-policy.js +0 -601
  63. package/dist/extensions/agent-browser/lib/navigation-policy.js +0 -78
  64. package/dist/extensions/agent-browser/lib/results/presentation/managed-list-filter.js +0 -37
@@ -4,12 +4,11 @@ import { lstat, mkdir, readdir } from "node:fs/promises";
4
4
  import { dirname, isAbsolute, join } from "node:path";
5
5
  import { env as processEnv, platform as processPlatform } from "node:process";
6
6
  import { parseArgvDescriptor } from "./argv-descriptor.js";
7
- import { needsManagedSession } from "./command-policy.js";
8
7
  import { isKnownCommandToken } from "./command-taxonomy.js";
9
8
  import { extractExplicitSessionName, getFlagName, GLOBAL_BOOLEAN_FLAGS_WITH_OPTIONAL_VALUES, GLOBAL_VALUE_FLAGS, optionalGlobalValueFlagConsumesNext, resolveAgentBrowserNamespace, } from "./argv-grammar.js";
10
- import { canonicalizeOwnedManagedSessionCloseArgs, commitManagedSessionRestoreSuppression, getManagedSessionRestoreConfigEnv, getManagedSessionRestoreEnv, getOwnedManagedSessionRestoreKey, getManagedSessionRestoreProtectedEnv, getOwnedManagedSessionCompatibilityEnv, getOwnedManagedSessionNamespaceEnv, isOwnedManagedSessionTarget, shouldOmitOwnedManagedSessionRestoreEnv, validateManagedSessionRestoreContextForSpawn, } from "./managed-session-restore.js";
11
- import { getManagedSessionStateAccessValidationError, getManagedSessionTargetAccessValidationError, invocationMayNavigateToLocalFile, } from "./managed-session-state-policy.js";
12
- import { getImplicitSessionIdleTimeoutMs, isPlainTextInspectionArgs } from "./runtime.js";
9
+ import { commitManagedSessionRestoreSuppression, getManagedSessionRestoreEnv, getManagedSessionRestoreProtectedEnv, getOwnedManagedSessionCompatibilityEnv, getOwnedManagedSessionNamespaceEnv, isOwnedManagedSessionTarget, validateManagedSessionRestoreContextForSpawn, } from "./managed-session-restore.js";
10
+ import { getPageTargetValidationError, } from "./page-target-validation.js";
11
+ import { getImplicitSessionIdleTimeoutMs } from "./runtime.js";
13
12
  import { getAgentBrowserProcessEnvironment } from "./process-environment.js";
14
13
  import { openSecureTempFile, writeSecureTempChunk } from "./temp.js";
15
14
  const MAX_BUFFERED_STDOUT_BYTES = 512 * 1_024;
@@ -17,7 +16,6 @@ const MAX_BUFFERED_STDERR_CHARS = 32_000;
17
16
  const MAX_BUFFERED_STDOUT_TAIL_CHARS = 32_000;
18
17
  const PROCESS_STDOUT_SPILL_FILE_PREFIX = "process-stdout";
19
18
  const AGENT_BROWSER_SOCKET_DIR_ENV = "AGENT_BROWSER_SOCKET_DIR";
20
- const AGENT_BROWSER_ARGS_ENV = "AGENT_BROWSER_ARGS";
21
19
  const AGENT_BROWSER_DEFAULT_TIMEOUT_ENV = "AGENT_BROWSER_DEFAULT_TIMEOUT";
22
20
  const AGENT_BROWSER_IDLE_TIMEOUT_ENV = "AGENT_BROWSER_IDLE_TIMEOUT_MS";
23
21
  const PI_AGENT_BROWSER_PROCESS_TIMEOUT_ENV = "PI_AGENT_BROWSER_PROCESS_TIMEOUT_MS";
@@ -29,12 +27,11 @@ const DEFAULT_AGENT_BROWSER_PROCESS_TIMEOUT_MS = 35_000;
29
27
  /** Grace period after `exit` before resolving when `close` is delayed by inherited stdio handles. */
30
28
  const EXIT_STDIO_GRACE_MS = 100;
31
29
  const WINDOWS_AGENT_BROWSER_MISSING_MARKER = "PI_AGENT_BROWSER_COMMAND_NOT_FOUND:agent-browser.cmd";
32
- const UNTRUSTED_LOCAL_FILE_SESSION_MESSAGE = "Local file navigation requires a wrapper-managed local browser because caller-owned or attached sessions can retain unsafe file-access launch flags. Omit the explicit session or attachment and retry with sessionMode fresh.";
33
30
  const attachedBrowserSessionContext = new AsyncLocalStorage();
34
31
  const WINDOWS_COMMANDS_WITH_ADJACENT_SUBCOMMAND = new Set([
35
32
  "auth", "clipboard", "cookies", "dashboard", "device", "dialog", "diff", "find", "get", "is", "keyboard",
36
33
  "mouse", "network", "plugin", "profiler", "react", "record", "session", "set", "skills", "state", "storage",
37
- "stream", "tab", "trace", "window",
34
+ "stream", "tab", "trace", "webmcp", "window",
38
35
  ]);
39
36
  export function withAttachedBrowserSessionContext(preserve, run) {
40
37
  return attachedBrowserSessionContext.run(preserve || attachedBrowserSessionContext.getStore() === true, run);
@@ -111,27 +108,10 @@ export function reorderWindowsLeadingGlobalArgs(args) {
111
108
  }
112
109
  return args;
113
110
  }
114
- export function pinAgentBrowserFileAccessDisabled(args, wrapperCompatibilityUserAgent, preserveAttachedBrowserSession = false) {
115
- const filtered = [];
116
- for (let index = 0; index < args.length; index += 1) {
117
- const token = args[index];
118
- if (token.startsWith("--allow-file-access="))
119
- continue;
120
- if (token === "--allow-file-access") {
121
- if (["false", "true"].includes(args[index + 1] ?? ""))
122
- index += 1;
123
- continue;
124
- }
125
- filtered.push(token);
126
- }
127
- // These are launch-only controls. Sending them on an attached-session follow-up makes upstream replace the CDP connection with a local browser.
128
- if (preserveAttachedBrowserSession)
129
- return filtered;
130
- // Upstream's flag overrides only the active CDP target; the Chrome arg covers new tabs. Its --args parser splits commas/newlines.
131
- const browserArgs = wrapperCompatibilityUserAgent
132
- ? ["--args", `--user-agent=${wrapperCompatibilityUserAgent.replaceAll(/[\r\n,]/g, "")}`]
133
- : [];
134
- return [...browserArgs, "--allow-file-access", "false", ...filtered];
111
+ export function prepareAgentBrowserSpawnArgs(args, wrapperCompatibilityUserAgent, preserveAttachedBrowserSession = false) {
112
+ if (preserveAttachedBrowserSession || !wrapperCompatibilityUserAgent)
113
+ return args;
114
+ return ["--args", `--user-agent=${wrapperCompatibilityUserAgent.replaceAll(/[\r\n,]/g, "")}`, ...args];
135
115
  }
136
116
  export function buildAgentBrowserSpawnCommand(args, platform = processPlatform) {
137
117
  if (platform !== "win32") {
@@ -384,54 +364,27 @@ export function buildAgentBrowserProcessEnv(baseEnv = processEnv, overrides = un
384
364
  clampUpstreamDefaultTimeout(childEnv);
385
365
  return childEnv;
386
366
  }
387
- function getManagedPreSpawnPolicyError(options, effectiveEnv, allowManagedSessionTarget = false, currentPageUrl, pageUrlUnknown = false, trustedFirstBatchTabSelection = false, trustedPinnedEmptyConfig = false) {
388
- const policyEnv = effectiveEnv ?? { ...(options.parentEnv ?? processEnv), ...options.env };
389
- const managedSessionTargetError = getManagedSessionTargetAccessValidationError(options.args, allowManagedSessionTarget || options.ownedManagedSession === true || isOwnedManagedSessionTarget(options.args), policyEnv);
390
- if (managedSessionTargetError)
391
- return managedSessionTargetError;
367
+ function getManagedPreSpawnPolicyError(options, currentPageUrl, pageUrlUnknown = false, trustedFirstBatchTabSelection = false) {
392
368
  if (!validateManagedSessionRestoreContextForSpawn(options)) {
393
369
  return "Managed session restore policy, storage, or checkout identity changed after planning; refusing to start agent-browser.";
394
370
  }
395
- return getManagedSessionStateAccessValidationError({
371
+ return getPageTargetValidationError({
396
372
  args: options.args,
397
373
  currentPageUrl,
398
- cwd: options.cwd,
399
- env: effectiveEnv ?? options.env,
400
- managedSessionRestoreKey: getOwnedManagedSessionRestoreKey(),
401
374
  pageUrlUnknown,
402
- parentEnv: effectiveEnv ? {} : options.parentEnv ?? processEnv,
403
375
  stdin: options.stdin,
404
376
  trustedFirstBatchTabSelection,
405
- trustedPinnedEmptyConfig,
406
377
  });
407
378
  }
408
379
  export async function runAgentBrowserProcess(options) {
409
- const { allowManagedSessionTarget, cwd, env, managedSessionRestoreState, managedStateCurrentPageUrl, managedStatePageUrlUnknown, signal, stdin, trustedFirstBatchTabSelection } = options;
380
+ const { cwd, env, managedSessionRestoreState, managedStateCurrentPageUrl, managedStatePageUrlUnknown, signal, stdin, trustedFirstBatchTabSelection } = options;
410
381
  const preserveAttachedBrowserSession = options.preserveAttachedBrowserSession === true || attachedBrowserSessionContext.getStore() === true;
411
382
  const ownedManagedSession = options.ownedManagedSession === true || isOwnedManagedSessionTarget(options.args);
412
- const args = canonicalizeOwnedManagedSessionCloseArgs({
413
- args: options.args,
414
- cwd,
415
- env,
416
- ownedManagedSession,
417
- restoreState: managedSessionRestoreState,
418
- stdin,
419
- });
383
+ const args = options.args;
420
384
  const timeoutMs = options.timeoutMs ?? getAgentBrowserProcessTimeoutMs();
421
385
  if (signal?.aborted) {
422
386
  return { aborted: true, agentBrowserStarted: false, exitCode: 1, stderr: "", stdout: "", timedOut: false };
423
387
  }
424
- if (invocationMayNavigateToLocalFile(args, stdin) && (!ownedManagedSession || preserveAttachedBrowserSession)) {
425
- return {
426
- aborted: false,
427
- agentBrowserStarted: false,
428
- exitCode: 1,
429
- spawnError: new Error(UNTRUSTED_LOCAL_FILE_SESSION_MESSAGE),
430
- stderr: "",
431
- stdout: "",
432
- timedOut: false,
433
- };
434
- }
435
388
  const parentEnv = getAgentBrowserProcessEnvironment();
436
389
  const managedSessionRestoreOptions = {
437
390
  args,
@@ -442,7 +395,7 @@ export async function runAgentBrowserProcess(options) {
442
395
  restoreState: managedSessionRestoreState,
443
396
  stdin,
444
397
  };
445
- const planningPolicyError = getManagedPreSpawnPolicyError(managedSessionRestoreOptions, undefined, allowManagedSessionTarget, managedStateCurrentPageUrl, managedStatePageUrlUnknown, trustedFirstBatchTabSelection);
398
+ const planningPolicyError = getManagedPreSpawnPolicyError(managedSessionRestoreOptions, managedStateCurrentPageUrl, managedStatePageUrlUnknown, trustedFirstBatchTabSelection);
446
399
  if (planningPolicyError) {
447
400
  return {
448
401
  aborted: false,
@@ -455,37 +408,18 @@ export async function runAgentBrowserProcess(options) {
455
408
  };
456
409
  }
457
410
  const managedSessionRestoreEnv = getManagedSessionRestoreEnv(managedSessionRestoreOptions);
458
- const ownedManagedSessionClose = shouldOmitOwnedManagedSessionRestoreEnv(managedSessionRestoreOptions);
459
- const browserConfigPinRequired = !isPlainTextInspectionArgs(args) && needsManagedSession(parseArgvDescriptor(args));
460
- const managedSessionRestoreConfigEnv = await getManagedSessionRestoreConfigEnv(managedSessionRestoreEnv, ownedManagedSessionClose || browserConfigPinRequired);
461
- if (managedSessionRestoreConfigEnv === undefined) {
462
- return {
463
- aborted: false,
464
- agentBrowserStarted: false,
465
- exitCode: 1,
466
- spawnError: new Error("Browser-backed agent-browser commands require a protected empty config, but secure temp storage was unavailable."),
467
- stderr: "",
468
- stdout: "",
469
- timedOut: false,
470
- };
471
- }
472
411
  const ownedManagedSessionCompatibilityEnv = getOwnedManagedSessionCompatibilityEnv(managedSessionRestoreOptions);
473
412
  const processOverrides = {
474
413
  [AGENT_BROWSER_IDLE_TIMEOUT_ENV]: String(getImplicitSessionIdleTimeoutMs()),
475
414
  ...managedSessionRestoreEnv,
476
415
  ...env,
477
- ...managedSessionRestoreConfigEnv,
478
416
  ...getManagedSessionRestoreProtectedEnv(managedSessionRestoreOptions, managedSessionRestoreEnv),
479
417
  ...getOwnedManagedSessionNamespaceEnv(managedSessionRestoreOptions),
480
418
  ...getWindowsExplicitDefaultNamespaceEnv(args, parentEnv.AGENT_BROWSER_NAMESPACE),
481
419
  ...ownedManagedSessionCompatibilityEnv,
482
- AGENT_BROWSER_ALLOW_FILE_ACCESS: undefined,
483
- [AGENT_BROWSER_ARGS_ENV]: undefined,
484
420
  };
485
421
  const explicitSocketDir = processOverrides[AGENT_BROWSER_SOCKET_DIR_ENV];
486
422
  let effectiveEnv = explicitSocketDir === undefined ? { ...processOverrides, [AGENT_BROWSER_SOCKET_DIR_ENV]: undefined } : processOverrides;
487
- if (ownedManagedSessionClose)
488
- effectiveEnv = { ...effectiveEnv, AGENT_BROWSER_RESTORE: undefined };
489
423
  const requestedSocketDir = explicitSocketDir ?? parentEnv[PI_AGENT_BROWSER_SOCKET_DIR_ENV] ?? getAgentBrowserSocketDir();
490
424
  if (requestedSocketDir !== undefined) {
491
425
  const socketDirError = requestedSocketDir.length > 0
@@ -522,6 +456,7 @@ export async function runAgentBrowserProcess(options) {
522
456
  let stdoutTail = "";
523
457
  let stdoutSpillHandle;
524
458
  let stdoutSpillPath;
459
+ let stdoutSpillPending = false;
525
460
  let pendingStdoutWrite = Promise.resolve();
526
461
  let stdoutSpillError;
527
462
  let killTimer;
@@ -533,11 +468,12 @@ export async function runAgentBrowserProcess(options) {
533
468
  stdoutTail = appendTail(stdoutTail, buffer.toString("utf8"), MAX_BUFFERED_STDOUT_TAIL_CHARS);
534
469
  if (stdoutSpillError)
535
470
  return;
536
- if (!stdoutSpillPath && stdoutBufferedBytes + buffer.length <= MAX_BUFFERED_STDOUT_BYTES) {
471
+ if (!stdoutSpillPending && !stdoutSpillPath && stdoutBufferedBytes + buffer.length <= MAX_BUFFERED_STDOUT_BYTES) {
537
472
  stdoutBuffers.push(buffer);
538
473
  stdoutBufferedBytes += buffer.length;
539
474
  return;
540
475
  }
476
+ stdoutSpillPending = true;
541
477
  pendingStdoutWrite = pendingStdoutWrite
542
478
  .then(async () => {
543
479
  if (stdoutSpillError)
@@ -612,12 +548,12 @@ export async function runAgentBrowserProcess(options) {
612
548
  });
613
549
  };
614
550
  const childEnv = buildAgentBrowserProcessEnv(parentEnv, effectiveEnv);
615
- const spawnPolicyError = getManagedPreSpawnPolicyError(managedSessionRestoreOptions, childEnv, allowManagedSessionTarget, managedStateCurrentPageUrl, managedStatePageUrlUnknown, trustedFirstBatchTabSelection, managedSessionRestoreConfigEnv.AGENT_BROWSER_CONFIG !== undefined);
551
+ const spawnPolicyError = getManagedPreSpawnPolicyError(managedSessionRestoreOptions, managedStateCurrentPageUrl, managedStatePageUrlUnknown, trustedFirstBatchTabSelection);
616
552
  if (spawnPolicyError) {
617
553
  resolve({ aborted: false, agentBrowserStarted: false, exitCode: 1, spawnError: new Error(spawnPolicyError), stderr: "", stdout: "", timedOut: false });
618
554
  return;
619
555
  }
620
- const spawnCommand = buildAgentBrowserSpawnCommand(pinAgentBrowserFileAccessDisabled(args, ownedManagedSessionCompatibilityEnv.AGENT_BROWSER_USER_AGENT, preserveAttachedBrowserSession));
556
+ const spawnCommand = buildAgentBrowserSpawnCommand(prepareAgentBrowserSpawnArgs(args, ownedManagedSessionCompatibilityEnv.AGENT_BROWSER_USER_AGENT, preserveAttachedBrowserSession));
621
557
  const child = spawn(spawnCommand.command, spawnCommand.args, {
622
558
  cwd,
623
559
  env: childEnv,
@@ -1,9 +1,6 @@
1
- import { extractUpstreamCommandTokens } from "./argv-descriptor.js";
2
- import { getAgentBrowserSessionIdentityKey, isAgentBrowserSessionIdentityKeyInNamespace } from "./argv-grammar.js";
3
- import { batchHasSuccessfulCloseAll, getSuccessfulBatchCloseLifecycle } from "./batch-lifecycle.js";
4
- import { isCloseAllCommand, isCloseCommand } from "./command-taxonomy.js";
1
+ import { getAgentBrowserSessionIdentityKey } from "./argv-grammar.js";
5
2
  import { isRecord } from "./parsing.js";
6
- import { isPendingRecordingArtifact, isPendingRecordingCommand, isSessionArtifactManifest } from "./results/artifact-manifest.js";
3
+ import { isPendingRecordingArtifact } from "./results/artifact-manifest.js";
7
4
  export const RECORDING_RESERVATION_ENTRY_TYPE = "agent-browser-recording-reservation";
8
5
  function getReservationKey(reservation) {
9
6
  return getAgentBrowserSessionIdentityKey(reservation.sessionName, reservation.namespace);
@@ -100,83 +97,19 @@ export function restoreRecordingReservationStateFromBranch(branch) {
100
97
  const reservations = new Map();
101
98
  const terminal = new Map();
102
99
  for (const entry of branch) {
103
- if (!isRecord(entry))
100
+ if (!isRecord(entry) || entry.type !== "custom" || entry.customType !== RECORDING_RESERVATION_ENTRY_TYPE)
104
101
  continue;
105
- if (entry.type === "custom" && entry.customType === RECORDING_RESERVATION_ENTRY_TYPE) {
106
- const transition = parseReservationTransition(entry.data);
107
- if (!transition)
108
- continue;
109
- const key = getReservationKey(transition.reservation);
110
- if (transition.state === "active") {
111
- reservations.set(key, transition.reservation);
112
- terminal.delete(key);
113
- }
114
- else {
115
- reservations.delete(key);
116
- terminal.set(key, transition.reservation);
117
- }
102
+ const transition = parseReservationTransition(entry.data);
103
+ if (!transition)
118
104
  continue;
105
+ const key = getReservationKey(transition.reservation);
106
+ if (transition.state === "active") {
107
+ reservations.set(key, transition.reservation);
108
+ terminal.delete(key);
119
109
  }
120
- if (entry.type !== "message")
121
- continue;
122
- const message = isRecord(entry.message) ? entry.message : undefined;
123
- const details = message?.toolName === "agent_browser" && isRecord(message.details) ? message.details : undefined;
124
- if (!details)
125
- continue;
126
- if (isSessionArtifactManifest(details.artifactManifest)) {
127
- const artifacts = [...details.artifactManifest.entries]
128
- .sort((left, right) => left.createdAtMs - right.createdAtMs
129
- || Number(isPendingRecordingCommand(left.command, left.subcommand, left.kind)) - Number(isPendingRecordingCommand(right.command, right.subcommand, right.kind))
130
- || left.path.localeCompare(right.path))
131
- .filter((manifestEntry) => manifestEntry.command === "record" && manifestEntry.kind === "video" && manifestEntry.session)
132
- .map((manifestEntry) => ({
133
- absolutePath: manifestEntry.absolutePath ?? manifestEntry.path,
134
- command: manifestEntry.command,
135
- cwd: manifestEntry.cwd,
136
- kind: "video",
137
- namespace: manifestEntry.namespace,
138
- path: manifestEntry.path,
139
- session: manifestEntry.session,
140
- status: isPendingRecordingCommand(manifestEntry.command, manifestEntry.subcommand, "video") ? "pending" : "saved",
141
- subcommand: manifestEntry.subcommand,
142
- }));
143
- for (const artifact of artifacts) {
144
- const reservation = getArtifactReservation(artifact);
145
- if (!reservation)
146
- continue;
147
- const key = getReservationKey(reservation);
148
- applyRecordingArtifactsToReservations(reservations, [artifact]);
149
- if (isPendingRecordingArtifact(artifact))
150
- terminal.delete(key);
151
- else
152
- terminal.set(key, reservation);
153
- }
154
- }
155
- const messageSucceeded = typeof message?.isError === "boolean"
156
- ? !message.isError
157
- : typeof details.exitCode !== "number" || details.exitCode === 0;
158
- const args = Array.isArray(details.args) && details.args.every((arg) => typeof arg === "string") ? details.args : [];
159
- const namespace = typeof details.namespace === "string" ? details.namespace : undefined;
160
- const closeAllApplied = details.closeAllApplied === true
161
- || (messageSucceeded && isCloseAllCommand(extractUpstreamCommandTokens(args)))
162
- || batchHasSuccessfulCloseAll(details.batchSteps);
163
- if (closeAllApplied) {
164
- for (const [key, reservation] of reservations) {
165
- if (!isAgentBrowserSessionIdentityKeyInNamespace(key, namespace))
166
- continue;
167
- terminal.set(key, reservation);
168
- reservations.delete(key);
169
- }
170
- continue;
171
- }
172
- const directCloseSucceeded = messageSucceeded && isCloseCommand(typeof details.command === "string" ? details.command : undefined);
173
- const batchRecordingClosed = getSuccessfulBatchCloseLifecycle(details.batchSteps)?.recordingClosedAfterBatch === true;
174
- if ((directCloseSucceeded || batchRecordingClosed) && typeof details.sessionName === "string") {
175
- const key = getAgentBrowserSessionIdentityKey(details.sessionName, namespace);
176
- const reservation = reservations.get(key);
177
- if (reservation)
178
- terminal.set(key, reservation);
110
+ else {
179
111
  reservations.delete(key);
112
+ terminal.set(key, transition.reservation);
180
113
  }
181
114
  }
182
115
  return { active: reservations, terminal };
@@ -106,7 +106,7 @@ export function buildAgentBrowserNextActions(options) {
106
106
  reason: "Inspect the opened page before choosing interactive refs.",
107
107
  }));
108
108
  }
109
- else if (isPageMutationCommand(options.command)) {
109
+ else if (isPageMutationCommand(options.command, options.subcommand)) {
110
110
  actions.push(buildNextToolAction({
111
111
  args: ["snapshot", "-i"],
112
112
  id: "inspect-after-mutation",
@@ -3,12 +3,13 @@ import { isRecord } from "../../parsing.js";
3
3
  import { getAgentBrowserSessionIdentityKey } from "../../argv-grammar.js";
4
4
  import { extractUpstreamCommandTokens, parseCommandInfo, redactInvocationArgs, redactSensitiveText, redactSensitiveValue } from "../../runtime.js";
5
5
  import { buildAgentBrowserNextActions } from "../action-recommendations.js";
6
+ import { buildPendingWebMcpNextActions } from "../recovery-next-actions.js";
6
7
  import { formatSessionArtifactRetentionSummary, isPendingRecordingArtifact } from "../artifact-manifest.js";
7
8
  import { classifyAgentBrowserFailureCategory } from "../categories.js";
8
9
  import { detectConfirmationRequired } from "../confirmation.js";
9
10
  import { applyNetworkRouteRecords, buildNetworkRouteDiagnostics } from "../network-routes.js";
10
11
  import { appendUniqueAgentBrowserNextActions, applyNamespaceToNextActions, withOptionalSessionArgs } from "../next-actions.js";
11
- import { stringifyModelFacing } from "./common.js";
12
+ import { extractAgentBrowserLifecycle, stringifyModelFacing } from "./common.js";
12
13
  import { buildArtifactVerificationSummary, classifyPresentationSuccessCategory, manifestHasNewNoticeWorthyEntries } from "./artifacts.js";
13
14
  import { formatBatchStepCommand, getPresentationImages, getPresentationPaths, getPresentationText, isStringArray } from "./content.js";
14
15
  import { buildPageChangeSummary } from "./navigation.js";
@@ -94,6 +95,11 @@ function redactExactValues(value, sensitiveValues) {
94
95
  return value;
95
96
  return redactSensitiveValue(Object.fromEntries(Object.entries(value).map(([key, entryValue]) => [key, redactExactValues(entryValue, sensitiveValues)])));
96
97
  }
98
+ export function redactBatchStepErrorData(command, value) {
99
+ return command?.[0] === "clipboard"
100
+ ? redactSensitiveValue(redactClipboardPermissionErrorValue({ command: "clipboard", subcommand: command[1] }, value, getClipboardWritePayloadCandidates(command)))
101
+ : redactExactValues(value, getStatefulCommandSensitiveValues(command));
102
+ }
97
103
  function getTypedTextLength(command) {
98
104
  return command?.[0] === "keyboard" && command[1] === "type" && typeof command[2] === "string"
99
105
  ? Array.from(command[2]).length
@@ -187,11 +193,9 @@ async function buildBatchStepPresentation(options) {
187
193
  const command = isStringArray(item.command) ? item.command : undefined;
188
194
  const redactedCommand = command ? redactInvocationArgs(command) : undefined;
189
195
  const commandText = formatBatchStepCommand(hasModelFacingArgRedaction(redactedCommand) ? redactedCommand : command, index);
190
- const lifecycle = extractBatchStepLifecycle(item.result);
196
+ const lifecycle = extractAgentBrowserLifecycle(item.result);
191
197
  if (item.success === false) {
192
- const redactedErrorData = command?.[0] === "clipboard"
193
- ? redactSensitiveValue(redactClipboardPermissionErrorValue({ command: "clipboard", subcommand: command[1] }, item.error, getClipboardWritePayloadCandidates(command)))
194
- : redactExactValues(item.error, getStatefulCommandSensitiveValues(command));
198
+ const redactedErrorData = redactBatchStepErrorData(command, item.error);
195
199
  const errorText = formatBatchStepError(redactedErrorData);
196
200
  const failureCategory = classifyAgentBrowserFailureCategory({
197
201
  args: command,
@@ -206,6 +210,7 @@ async function buildBatchStepPresentation(options) {
206
210
  failureCategory,
207
211
  resultCategory: "failure",
208
212
  sessionName,
213
+ subcommand: command?.[1],
209
214
  })), namespace);
210
215
  const presentation = {
211
216
  content: [{ type: "text", text: errorText }],
@@ -262,7 +267,12 @@ async function buildBatchStepPresentation(options) {
262
267
  });
263
268
  const text = getPresentationText(presentation) || presentation.summary;
264
269
  const stepSucceeded = presentation.resultCategory !== "failure";
265
- const nextActions = applyNamespaceToNextActions(presentation.nextActions ?? buildAgentBrowserNextActions({
270
+ const pendingWebMcpMutation = stepSucceeded
271
+ && commandInfo.command === "webmcp"
272
+ && ["invoke", "result"].includes(commandInfo.subcommand ?? "")
273
+ && isRecord(presentation.data)
274
+ && presentation.data.status === "pending";
275
+ const nextActions = applyNamespaceToNextActions(pendingWebMcpMutation ? buildPendingWebMcpNextActions(sessionName) : presentation.nextActions ?? buildAgentBrowserNextActions({
266
276
  artifacts: presentation.artifacts,
267
277
  args: command,
268
278
  command: command?.[0],
@@ -270,6 +280,7 @@ async function buildBatchStepPresentation(options) {
270
280
  resultCategory: stepSucceeded ? "success" : "failure",
271
281
  savedFilePath: presentation.savedFilePath,
272
282
  sessionName,
283
+ subcommand: command?.[1],
273
284
  successCategory: presentation.successCategory,
274
285
  }), namespace);
275
286
  const pageChangeSummary = buildPageChangeSummary({
@@ -308,12 +319,6 @@ async function buildBatchStepPresentation(options) {
308
319
  presentation,
309
320
  };
310
321
  }
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
322
  function abandonedRecordingArtifact(artifact) {
318
323
  const { recordingState: _recordingState, willExistOnStop: _willExistOnStop, ...terminal } = artifact;
319
324
  return { ...terminal, exists: false, status: "missing", subcommand: "close-abandoned" };
@@ -394,6 +399,10 @@ export async function buildBatchPresentation(options) {
394
399
  const redactedBatchData = steps.map(({ details }) => (details.success
395
400
  ? { command: details.command, result: details.data, success: true }
396
401
  : { command: details.command, error: details.text, success: false }));
402
+ const unverifiedMutationCount = steps.filter((step) => step.details.pageChangeSummary?.changeType === "mutation" && step.details.pageChangeSummary.observed === false).length;
403
+ const mutationEvidenceText = unverifiedMutationCount > 0
404
+ ? `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.`
405
+ : undefined;
397
406
  const stepText = formatBatchStepsText(steps);
398
407
  const batchSummary = batchFailure === undefined
399
408
  ? summary
@@ -405,7 +414,7 @@ export async function buildBatchPresentation(options) {
405
414
  `First failing step: ${batchFailure.failedStep.index + 1} — ${batchFailure.failedStep.commandText}`,
406
415
  batchFailure.failureCount > 1 ? `${batchFailure.failureCount} steps failed. See the per-step results below.` : "See the per-step results below.",
407
416
  ].join("\n");
408
- const text = failureHeader ? `${failureHeader}\n\n${stepText}` : stepText;
417
+ const text = [failureHeader, mutationEvidenceText, stepText].filter((line) => line !== undefined).join("\n\n");
409
418
  const artifactRetentionSummary = currentArtifactManifest ? formatSessionArtifactRetentionSummary(currentArtifactManifest) : undefined;
410
419
  const contentText = artifactRetentionSummary && manifestHasNewNoticeWorthyEntries(options.artifactManifest, currentArtifactManifest)
411
420
  ? `${text}\n\n${artifactRetentionSummary}`
@@ -415,6 +424,7 @@ export async function buildBatchPresentation(options) {
415
424
  ? appendUniqueAgentBrowserNextActions([...(batchFailure.failedStep.nextActions ?? [])], artifactLifecycleActions)
416
425
  : artifactLifecycleActions;
417
426
  const changedSteps = steps.map((step) => step.details).filter((details) => details.pageChangeSummary !== undefined);
427
+ const observedChangeCount = changedSteps.filter((details) => details.pageChangeSummary?.observed === true).length;
418
428
  const pageChangeSummary = artifacts.length > 0
419
429
  ? buildPageChangeSummary({
420
430
  artifacts,
@@ -428,7 +438,10 @@ export async function buildBatchPresentation(options) {
428
438
  changeType: "mutation",
429
439
  command: "batch",
430
440
  nextActionIds: nextActions?.map((action) => action.id),
431
- summary: `batch mutation → ${changedSteps.length} changed step${changedSteps.length === 1 ? "" : "s"}`,
441
+ observed: observedChangeCount > 0,
442
+ summary: observedChangeCount > 0
443
+ ? `batch → ${observedChangeCount} observed change${observedChangeCount === 1 ? "" : "s"}${unverifiedMutationCount > 0 ? `; ${unverifiedMutationCount} dispatched action${unverifiedMutationCount === 1 ? "" : "s"} unverified` : ""}`
444
+ : `batch → ${unverifiedMutationCount} action${unverifiedMutationCount === 1 ? "" : "s"} dispatched → application change unverified`,
432
445
  }
433
446
  : undefined;
434
447
  return {
@@ -1,4 +1,4 @@
1
- import { containsManagedSessionRestoreKey } from "../../managed-session-capabilities.js";
1
+ import { isRecord } from "../../parsing.js";
2
2
  import { redactSensitiveText, redactSensitiveValue } from "../../runtime.js";
3
3
  import { stringifyUnknown, truncateText } from "../text.js";
4
4
  const UNTITLED_PAGE_SUMMARY = "(untitled page)";
@@ -24,7 +24,7 @@ export function redactModelFacingText(text) {
24
24
  return redactSensitiveText(text);
25
25
  }
26
26
  export function redactModelFacingTextIfSensitive(text) {
27
- return containsManagedSessionRestoreKey(text) || /(?:@|\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)
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
28
  ? redactModelFacingText(text)
29
29
  : text;
30
30
  }
@@ -37,6 +37,24 @@ export function getStringField(data, key) {
37
37
  }
38
38
  // `lifecycle` is upstream launch/reuse bookkeeping, never page content, so it must not be the
39
39
  // answer an agent reads when a command has no dedicated presenter.
40
+ export function extractAgentBrowserLifecycle(result) {
41
+ if (Array.isArray(result)) {
42
+ let latest;
43
+ for (const row of result) {
44
+ const nested = isRecord(row) ? row.result ?? row.data ?? row : row;
45
+ latest = extractAgentBrowserLifecycle(nested) ?? latest;
46
+ }
47
+ return latest;
48
+ }
49
+ if (!isRecord(result))
50
+ return undefined;
51
+ if (isRecord(result.lifecycle) && isRecord(result.lifecycle.effectiveLaunch)) {
52
+ const browserLaunched = result.lifecycle.effectiveLaunch.browserLaunched;
53
+ if (typeof browserLaunched === "boolean")
54
+ return { effectiveLaunch: { browserLaunched } };
55
+ }
56
+ return extractAgentBrowserLifecycle(result.result ?? result.data);
57
+ }
40
58
  export function omitUpstreamLifecycle(data) {
41
59
  const { lifecycle: _lifecycle, ...rest } = data;
42
60
  return rest;
@@ -4,7 +4,6 @@ import { classifyNetworkRequestFailure, isApiLikeNetworkRequest, isNetworkArtifa
4
4
  import { withOptionalSessionArgs } from "../next-actions.js";
5
5
  import { stringifyUnknown, truncateText } from "../text.js";
6
6
  import { firstLine, formatCount, getArrayField, getStringField, parseJsonPreviewString, redactModelFacingText, redactModelFacingTextIfSensitive, stringifyModelFacing, } from "./common.js";
7
- import { filterCallerOwnedSessionListItems, filterCallerOwnedStateListItems, filterManagedSessionListRows, filterManagedStateListRows, } from "./managed-list-filter.js";
8
7
  const DIAGNOSTIC_REQUEST_PREVIEW_LIMIT = 40;
9
8
  const DIAGNOSTIC_LOG_PREVIEW_LIMIT = 80;
10
9
  const NETWORK_BODY_PREVIEW_MAX_CHARS = 280;
@@ -112,7 +111,7 @@ export function formatDiagnosticSummary(commandInfo, data) {
112
111
  if (commandInfo.command === "session") {
113
112
  const sessions = getArrayField(data, "sessions");
114
113
  if (sessions)
115
- return `Sessions: ${filterCallerOwnedSessionListItems(sessions).length}`;
114
+ return `Sessions: ${sessions.length}`;
116
115
  const session = getStringField(data, "session");
117
116
  if (session)
118
117
  return `Session: ${session}`;
@@ -170,10 +169,8 @@ export function formatDiagnosticSummary(commandInfo, data) {
170
169
  }
171
170
  if (commandInfo.command === "state") {
172
171
  const states = getArrayField(data, "states") ?? getArrayField(data, "files");
173
- if (states) {
174
- const visibleStates = commandInfo.subcommand === "list" ? filterCallerOwnedStateListItems(states) : states;
175
- return `States: ${visibleStates.length}`;
176
- }
172
+ if (states)
173
+ return `States: ${states.length}`;
177
174
  if (commandInfo.subcommand === "load")
178
175
  return undefined;
179
176
  const stateName = getStringField(data, "name") ?? getStringField(data, "file") ?? getStringField(data, "filename") ?? getStringField(data, "path") ?? commandInfo.subcommand;
@@ -261,10 +258,9 @@ export function formatDiagnosticSummary(commandInfo, data) {
261
258
  function formatSessionText(data) {
262
259
  const sessions = getArrayField(data, "sessions");
263
260
  if (sessions) {
264
- const visibleSessions = filterCallerOwnedSessionListItems(sessions);
265
- if (visibleSessions.length === 0)
266
- return sessions.length === 0 ? "No active sessions." : "No caller-owned active sessions.";
267
- return visibleSessions
261
+ if (sessions.length === 0)
262
+ return "No active sessions.";
263
+ return sessions
268
264
  .map((item, index) => {
269
265
  if (!isRecord(item))
270
266
  return `${index + 1}. ${stringifyModelFacing(item)}`;
@@ -932,10 +928,9 @@ function formatStateText(data, subcommand) {
932
928
  }
933
929
  const states = getArrayField(data, "states") ?? getArrayField(data, "files");
934
930
  if (states) {
935
- const visibleStates = filterCallerOwnedStateListItems(states);
936
- if (visibleStates.length === 0)
937
- return "No caller-owned saved states.";
938
- return visibleStates
931
+ if (states.length === 0)
932
+ return "No saved states.";
933
+ return states
939
934
  .map((item, index) => {
940
935
  if (!isRecord(item))
941
936
  return `${index + 1}. ${redactModelFacingTextIfSensitive(stringifyModelFacing(item))}`;
@@ -979,9 +974,9 @@ export function redactPresentationData(commandInfo, data) {
979
974
  if (commandInfo.command === "storage")
980
975
  return redactStorageData(data);
981
976
  if (commandInfo.command === "session" && commandInfo.subcommand === "list")
982
- return redactStructuredPresentationValue(filterManagedSessionListRows(data));
977
+ return redactStructuredPresentationValue(data);
983
978
  if (commandInfo.command === "state" && commandInfo.subcommand === "list")
984
- return redactStructuredPresentationValue(filterManagedStateListRows(data));
979
+ return redactStructuredPresentationValue(data);
985
980
  if (commandInfo.command === "state" && commandInfo.subcommand === "show")
986
981
  return redactStatefulValues(data, new Set(["value"]));
987
982
  return redactStructuredPresentationValue(data);
@@ -1,4 +1,4 @@
1
- import { isNavigationObservableCommandName, isPageChangeSummaryCommand } from "../../command-taxonomy.js";
1
+ import { isNavigationObservableCommandName, isOpenNavigationCommand, isPageChangeSummaryCommand } from "../../command-taxonomy.js";
2
2
  import { isRecord } from "../../parsing.js";
3
3
  import { detectConfirmationRequired } from "../confirmation.js";
4
4
  import { omitUpstreamLifecycle, redactModelFacingText, stringifyModelFacing } from "./common.js";
@@ -74,8 +74,8 @@ export function formatExtractionText(commandInfo, data) {
74
74
  const safeOrigin = origin ? redactModelFacingText(origin) : undefined;
75
75
  return safeOrigin && safeOrigin !== safeScalarResult ? `${safeScalarResult}\n\nOrigin: ${safeOrigin}` : safeScalarResult;
76
76
  }
77
- export function isNavigationObservableCommand(command) {
78
- return isNavigationObservableCommandName(command);
77
+ export function isNavigationObservableCommand(command, subcommand) {
78
+ return isNavigationObservableCommandName(command, subcommand);
79
79
  }
80
80
  function isNavigationSummary(value) {
81
81
  return isRecord(value) && (typeof value.title === "string" || typeof value.url === "string");
@@ -95,7 +95,7 @@ function getTopLevelNavigationSummary(data) {
95
95
  function getNormalizedNavigationSummary(summary) {
96
96
  const title = typeof summary?.title === "string" && summary.title.trim().length > 0 ? summary.title.trim() : undefined;
97
97
  const url = typeof summary?.url === "string" && summary.url.trim().length > 0 ? summary.url.trim() : undefined;
98
- return title || url ? { title, url } : undefined;
98
+ return title || url ? { title, url, ...(typeof summary?.urlChanged === "boolean" ? { urlChanged: summary.urlChanged } : {}) } : undefined;
99
99
  }
100
100
  export function formatNavigationSummary(summary) {
101
101
  const normalized = getNormalizedNavigationSummary(summary);
@@ -109,20 +109,22 @@ export function buildPageChangeSummary(options) {
109
109
  const { artifacts, commandInfo, data, nextActions, savedFilePath } = options;
110
110
  const artifactCount = artifacts?.length ?? 0;
111
111
  const navigation = isRecord(data)
112
- ? getNormalizedNavigationSummary(getNavigationSummary(data) ?? (isPageChangeSummaryCommand(commandInfo.command) ? getTopLevelNavigationSummary(data) : undefined))
112
+ ? getNormalizedNavigationSummary(getNavigationSummary(data) ?? (isPageChangeSummaryCommand(commandInfo.command, commandInfo.subcommand) ? getTopLevelNavigationSummary(data) : undefined))
113
113
  : undefined;
114
114
  const confirmationRequired = detectConfirmationRequired(data) !== undefined;
115
- if (!navigation && !confirmationRequired && artifactCount === 0 && !savedFilePath && !isPageChangeSummaryCommand(commandInfo.command)) {
115
+ if (!navigation && !confirmationRequired && artifactCount === 0 && !savedFilePath && !isPageChangeSummaryCommand(commandInfo.command, commandInfo.subcommand)) {
116
116
  return undefined;
117
117
  }
118
+ const navigationObserved = navigation && (navigation.urlChanged === true || isOpenNavigationCommand(commandInfo.command) || ["back", "forward", "pushstate", "reload"].includes(commandInfo.command ?? ""));
118
119
  const changeType = savedFilePath || artifactCount > 0
119
120
  ? "artifact"
120
- : navigation
121
+ : navigationObserved
121
122
  ? "navigation"
122
123
  : confirmationRequired
123
124
  ? "confirmation"
124
125
  : "mutation";
125
- const parts = [commandInfo.command ?? "agent-browser", changeType];
126
+ const observed = changeType !== "mutation";
127
+ const parts = [commandInfo.command ?? "agent-browser", observed ? changeType : "action dispatched"];
126
128
  if (navigation?.title)
127
129
  parts.push(navigation.title);
128
130
  if (navigation?.url)
@@ -136,8 +138,9 @@ export function buildPageChangeSummary(options) {
136
138
  changeType,
137
139
  ...(commandInfo.command ? { command: commandInfo.command } : {}),
138
140
  ...(nextActions ? { nextActionIds: nextActions.map((action) => action.id) } : {}),
141
+ observed,
139
142
  ...(savedFilePath ? { savedFilePath } : {}),
140
- summary: parts.join(" → "),
143
+ summary: `${parts.join(" → ")}${observed ? "" : " → application change unverified"}`,
141
144
  ...(navigation?.title ? { title: navigation.title } : {}),
142
145
  ...(navigation?.url ? { url: navigation.url } : {}),
143
146
  };