pi-agent-browser-native 0.3.0 → 0.6.5

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 (89) hide show
  1. package/CHANGELOG.md +265 -0
  2. package/README.md +130 -54
  3. package/dist/extensions/agent-browser/index.js +781 -169
  4. package/dist/extensions/agent-browser/lib/argv-descriptor.js +35 -3
  5. package/dist/extensions/agent-browser/lib/argv-grammar.js +50 -2
  6. package/dist/extensions/agent-browser/lib/batch-lifecycle.js +71 -0
  7. package/dist/extensions/agent-browser/lib/command-policy.js +5 -8
  8. package/dist/extensions/agent-browser/lib/command-taxonomy.js +53 -12
  9. package/dist/extensions/agent-browser/lib/config-policy.js +25 -1
  10. package/dist/extensions/agent-browser/lib/config.js +1 -1
  11. package/dist/extensions/agent-browser/lib/input-modes/job.js +61 -13
  12. package/dist/extensions/agent-browser/lib/input-modes/lookups.js +2 -2
  13. package/dist/extensions/agent-browser/lib/input-modes/params.js +23 -24
  14. package/dist/extensions/agent-browser/lib/input-modes/script.js +462 -0
  15. package/dist/extensions/agent-browser/lib/input-modes/semantic-action.js +51 -12
  16. package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +26 -4
  17. package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +6 -139
  18. package/dist/extensions/agent-browser/lib/managed-session-restore.js +26 -116
  19. package/dist/extensions/agent-browser/lib/managed-session-snapshots.js +2 -4
  20. package/dist/extensions/agent-browser/lib/managed-session-storage.js +54 -25
  21. package/dist/extensions/agent-browser/lib/orchestration/batch-stdin.js +26 -5
  22. package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.js +110 -30
  23. package/dist/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.js +2 -1
  24. package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +54 -48
  25. package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +71 -5
  26. package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +2 -1
  27. package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +6 -7
  28. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/snapshot-filter.js +119 -2
  29. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/wait-timeouts.js +7 -6
  30. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +152 -64
  31. package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +244 -102
  32. package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +63 -37
  33. package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +20 -21
  34. package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +36 -18
  35. package/dist/extensions/agent-browser/lib/orchestration/output-file.js +41 -21
  36. package/dist/extensions/agent-browser/lib/orchestration/script-mode.js +299 -0
  37. package/dist/extensions/agent-browser/lib/page-target-validation.js +270 -0
  38. package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +32 -10
  39. package/dist/extensions/agent-browser/lib/playbook.js +29 -25
  40. package/dist/extensions/agent-browser/lib/process-environment.js +14 -0
  41. package/dist/extensions/agent-browser/lib/process-identity.js +5 -12
  42. package/dist/extensions/agent-browser/lib/process.js +130 -104
  43. package/dist/extensions/agent-browser/lib/recording-reservations.js +116 -0
  44. package/dist/extensions/agent-browser/lib/results/action-recommendations.js +63 -6
  45. package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +62 -4
  46. package/dist/extensions/agent-browser/lib/results/categories.js +6 -1
  47. package/dist/extensions/agent-browser/lib/results/next-actions.js +19 -5
  48. package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +85 -38
  49. package/dist/extensions/agent-browser/lib/results/presentation/batch.js +86 -18
  50. package/dist/extensions/agent-browser/lib/results/presentation/common.js +38 -2
  51. package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +18 -17
  52. package/dist/extensions/agent-browser/lib/results/presentation/errors.js +2 -1
  53. package/dist/extensions/agent-browser/lib/results/presentation/navigation.js +38 -20
  54. package/dist/extensions/agent-browser/lib/results/presentation/registry.js +60 -15
  55. package/dist/extensions/agent-browser/lib/results/presentation/semantic-action.js +1 -10
  56. package/dist/extensions/agent-browser/lib/results/presentation.js +36 -6
  57. package/dist/extensions/agent-browser/lib/results/recovery-actions.js +3 -1
  58. package/dist/extensions/agent-browser/lib/results/recovery-next-actions.js +9 -0
  59. package/dist/extensions/agent-browser/lib/results/selector-recovery.js +54 -11
  60. package/dist/extensions/agent-browser/lib/results/snapshot-high-value-controls.js +13 -7
  61. package/dist/extensions/agent-browser/lib/results/snapshot-spill.js +2 -1
  62. package/dist/extensions/agent-browser/lib/results/snapshot.js +4 -4
  63. package/dist/extensions/agent-browser/lib/runtime.js +186 -108
  64. package/dist/extensions/agent-browser/lib/session-page-state.js +71 -10
  65. package/dist/extensions/agent-browser/lib/temp.js +1 -2
  66. package/dist/extensions/agent-browser/lib/upstream-version.js +14 -0
  67. package/dist/extensions/agent-browser/lib/web-search.js +108 -24
  68. package/dist/extensions/agent-browser/script-worker.js +169 -0
  69. package/dist/scripts/agent-browser-target.mjs +21 -0
  70. package/docs/ARCHITECTURE.md +57 -34
  71. package/docs/COMMAND_REFERENCE.md +255 -68
  72. package/docs/ELECTRON.md +2 -2
  73. package/docs/RELEASE.md +12 -10
  74. package/docs/REQUIREMENTS.md +11 -8
  75. package/docs/SUPPORT_MATRIX.md +36 -24
  76. package/docs/TOOL_CONTRACT.md +169 -95
  77. package/package.json +3 -1
  78. package/platform-smoke.config.mjs +2 -2
  79. package/scripts/agent-browser-capability-baseline.mjs +87 -9
  80. package/scripts/agent-browser-target.mjs +21 -0
  81. package/scripts/build.mjs +41 -0
  82. package/scripts/config.mjs +1 -0
  83. package/scripts/doctor.mjs +16 -9
  84. package/scripts/platform-smoke/browser-dogfood-windows.ps1 +9 -3
  85. package/scripts/platform-smoke/targets.mjs +12 -6
  86. package/dist/extensions/agent-browser/lib/managed-session-capabilities.js +0 -20
  87. package/dist/extensions/agent-browser/lib/managed-session-state-policy.js +0 -583
  88. package/dist/extensions/agent-browser/lib/navigation-policy.js +0 -78
  89. package/dist/extensions/agent-browser/lib/results/presentation/managed-list-filter.js +0 -37
@@ -3,9 +3,10 @@ import { getCompiledSemanticActionCommandIndex, getCompiledSemanticActionSession
3
3
  import { redactNetworkSourceLookupSurface } from "../../input-modes/lookups.js";
4
4
  import { buildAgentBrowserNextActions } from "../../results/action-recommendations.js";
5
5
  import { buildAgentBrowserResultCategoryDetails } from "../../results/categories.js";
6
+ import { extractAgentBrowserLifecycle } from "../../results/presentation/common.js";
6
7
  import { formatSessionArtifactRetentionSummary } from "../../results/artifact-manifest.js";
7
8
  import { alignPageChangeSummaryNextActionIds, appendUniqueAgentBrowserNextActions, applyNamespaceToNextActions, isStandaloneSnapshotNextAction, withOptionalSessionArgs, } from "../../results/next-actions.js";
8
- import { buildConnectedSessionNextActions, buildNoActivePageNextActions, buildSessionAwareStaleRefNextActions, buildSessionTabRecoveryNextActions, } from "../../results/recovery-next-actions.js";
9
+ import { buildConnectedSessionNextActions, buildNoActivePageNextActions, buildPendingWebMcpNextActions, buildSessionAwareStaleRefNextActions, buildSessionTabRecoveryNextActions, } from "../../results/recovery-next-actions.js";
9
10
  import { buildRichInputRecoveryDiagnostic, buildRichInputRecoveryNextActions, buildVisibleRefFallbackNextActions, formatRichInputRecoveryText, formatVisibleRefFallbackText, sanitizeVisibleRefFallbackDiagnostic, } from "../../results/selector-recovery.js";
10
11
  import { buildNoActivePageRefSnapshotInvalidation, isNoActivePageSnapshotFailure, } from "../../session-page-state.js";
11
12
  import { extractExplicitSessionName } from "../../argv-grammar.js";
@@ -195,7 +196,7 @@ export async function prepareFinalResultRecoveryState(options) {
195
196
  }
196
197
  }
197
198
  const richInputRecoveryDiagnostic = buildRichInputRecoveryDiagnostic(visibleRefFallbackDiagnostic);
198
- const noActivePageSnapshotFailure = categoryDetails.resultCategory === "failure" && (isNoActivePageSnapshotFailure(options.executionPlan.commandInfo.command, options.errorText ?? options.presentation.summary) || options.batchRefSnapshotState?.invalidation !== undefined);
199
+ const noActivePageSnapshotFailure = categoryDetails.resultCategory === "failure" && (isNoActivePageSnapshotFailure(options.executionPlan.commandInfo.command, options.errorText ?? options.presentation.summary) || options.batchRefSnapshotState?.invalidation?.reason === "no-active-page");
199
200
  const executionSessionKey = getSessionContextKey(options.executionPlan.sessionName, options.executionPlan.namespace);
200
201
  if (noActivePageSnapshotFailure && executionSessionKey) {
201
202
  const refUpdate = options.sessionPageState.applyRefSnapshotInvalidation({ invalidation: buildNoActivePageRefSnapshotInvalidation(), sessionName: executionSessionKey, update: options.sessionPageStateUpdate });
@@ -208,6 +209,18 @@ function buildTimeoutPartialProgressNextActions(options) {
208
209
  const retryArgs = options.timeoutPartialProgress?.retryStep?.retry?.args;
209
210
  const stepIndex = options.timeoutPartialProgress?.retryStep?.index;
210
211
  const freshSessionAbandoned = options.sessionMode === "fresh" && options.timeoutPartialProgress?.liveUrlRecovered !== true;
212
+ if (options.currentSessionTabTargetUnknown && !freshSessionAbandoned && options.executionPlan.sessionName) {
213
+ return [{
214
+ id: "verify-page-target-after-timeout",
215
+ params: {
216
+ args: withOptionalSessionArgs(options.executionPlan.sessionName, ["batch", "--bail"]),
217
+ stdin: JSON.stringify([["get", "url"], ["snapshot", "-i"]]),
218
+ },
219
+ reason: `Verify the current URL, then inspect the page after timeout${stepIndex === undefined ? "" : ` before resuming from incomplete step ${stepIndex}`}.`,
220
+ safety: "Fail-fast read-only recovery: snapshot runs only after get url succeeds, satisfying the wrapper page-target guard without trusting the planned URL.",
221
+ tool: "agent_browser",
222
+ }];
223
+ }
211
224
  if (retryArgs) {
212
225
  return [{
213
226
  id: "retry-timeout-step",
@@ -267,6 +280,10 @@ function buildResultNextActions(options) {
267
280
  const appendUnique = (actions) => {
268
281
  appendUniqueAgentBrowserNextActions(nextActions, actions);
269
282
  };
283
+ if (options.unsettledWebMcpMutation && options.currentSessionTabTargetUnknown) {
284
+ nextActions = nextActions.filter((action) => !isStandaloneSnapshotNextAction(action));
285
+ appendUnique(buildPendingWebMcpNextActions(options.executionPlan.sessionName));
286
+ }
270
287
  if (options.categoryDetails.resultCategory === "success" && options.executionPlan.commandInfo.command === "connect" && !options.electronLaunchRecord)
271
288
  appendUnique(buildConnectedSessionNextActions(options.executionPlan.sessionName));
272
289
  if (options.noActivePageSnapshotFailure)
@@ -320,6 +337,8 @@ function buildResultNextActions(options) {
320
337
  if (options.managedSessionOutcome)
321
338
  appendUnique(buildManagedSessionFreshFailureNextActions(options.managedSessionOutcome));
322
339
  if (options.categoryDetails.failureCategory === "timeout" && options.processResult.timedOut) {
340
+ if (options.currentSessionTabTargetUnknown)
341
+ nextActions = nextActions.filter((action) => !isStandaloneSnapshotNextAction(action));
323
342
  appendUnique(buildTimeoutPartialProgressNextActions(options));
324
343
  appendUnique(buildDialogTimeoutNextActions({ command: options.executionPlan.commandInfo.command, sessionName: options.executionPlan.sessionName }));
325
344
  }
@@ -329,7 +348,45 @@ function buildResultNextActions(options) {
329
348
  append(buildAgentBrowserNextActions({ electron: { launchId: options.electronLaunchRecord.launchId, sessionName: options.electronLaunchRecord.sessionName, status: options.electronLaunchRecord.cleanupState }, failureCategory: options.categoryDetails.failureCategory, resultCategory: options.categoryDetails.resultCategory, successCategory: options.categoryDetails.successCategory }));
330
349
  return nextActions.length > 0 ? nextActions : undefined;
331
350
  }
351
+ export function formatAgentBrowserNextActionsText(nextActions) {
352
+ if (!nextActions || nextActions.length === 0)
353
+ return undefined;
354
+ const lines = nextActions.slice(0, 6).map((action) => {
355
+ const params = action.params
356
+ ? { ...action.params, ...(action.params.stdin !== undefined && action.params.stdin.length > 500 ? { stdin: "[omitted; use details.nextActions]" } : {}) }
357
+ : undefined;
358
+ const payload = action.artifactPath ? { artifactPath: action.artifactPath } : params;
359
+ return `- ${action.id}${payload ? ` ${redactSensitiveText(JSON.stringify(payload))}` : ""}: ${redactSensitiveText(action.reason)}`;
360
+ });
361
+ return ["Next actions:", ...lines, "The same redacted payloads are available in details.nextActions."].join("\n");
362
+ }
363
+ function formatFailureNextActionsText(options, nextActions) {
364
+ return options.categoryDetails.resultCategory === "failure" ? formatAgentBrowserNextActionsText(nextActions) : undefined;
365
+ }
366
+ function getReadSource(options) {
367
+ return options.executionPlan.commandInfo.command === "read" && isRecord(options.presentationEnvelope?.data) && typeof options.presentationEnvelope.data.source === "string"
368
+ ? options.presentationEnvelope.data.source
369
+ : undefined;
370
+ }
371
+ function formatReadExecutionText(options, lifecycle) {
372
+ const source = getReadSource(options);
373
+ if (!source)
374
+ return undefined;
375
+ return `Read execution: source ${source}; CLI started: ${options.processResult.agentBrowserStarted ? "yes" : "no"}; managed browser lifecycle active: ${lifecycle?.effectiveLaunch.browserLaunched === true ? "yes" : "no"}; managed session outcome: ${options.managedSessionOutcome?.status ?? "not managed"}.`;
376
+ }
377
+ function buildBrowserWindowStatus(options, lifecycle) {
378
+ if (!options.headedLaunch || options.preserveAttachedBrowserSession || options.providerLaunch || !options.succeeded || lifecycle?.effectiveLaunch.browserLaunched !== true || !options.executionPlan.managedSessionName || !options.managedSessionOutcome || !["created", "replaced"].includes(options.managedSessionOutcome.status))
379
+ return undefined;
380
+ return { mode: "headed", ownership: "wrapper-managed", sessionName: options.executionPlan.managedSessionName, visibility: "unverified" };
381
+ }
382
+ function formatBrowserWindowText(browserWindow) {
383
+ if (!browserWindow)
384
+ return undefined;
385
+ return "Headed browser handoff: wrapper-managed headed window requested; desktop visibility unverified. If login is needed, ask the user to confirm they can see the window and finish signing in there, then continue with sessionMode auto.";
386
+ }
332
387
  function buildAgentBrowserResultDetails(options, nextActions) {
388
+ const lifecycle = extractAgentBrowserLifecycle(options.presentationEnvelope?.data);
389
+ const browserWindow = buildBrowserWindowStatus(options, lifecycle);
333
390
  const publicVisibleRefFallbackDiagnostic = options.visibleRefFallbackDiagnostic ? sanitizeVisibleRefFallbackDiagnostic(options.visibleRefFallbackDiagnostic) : undefined;
334
391
  const rawPageChangeSummary = (options.scrollNoopDiagnostic || options.comboboxFocusDiagnostic) && options.presentation.pageChangeSummary ? { ...options.presentation.pageChangeSummary, nextActionIds: nextActions?.map((action) => action.id) } : options.presentation.pageChangeSummary;
335
392
  const pageChangeSummary = alignPageChangeSummaryNextActionIds(rawPageChangeSummary, nextActions);
@@ -357,6 +414,10 @@ function buildAgentBrowserResultDetails(options, nextActions) {
357
414
  navigationSummary: options.navigationSummary,
358
415
  electron: options.electronLaunchRecord ? { action: "launch", cleanup: options.electronFailedConnectCleanup, handoff: options.electronHandoff, identifiers: buildElectronIdentifiers(options.electronLaunchRecord), launch: options.electronLaunchRecord, profileIsolation: options.electronProfileIsolationDetails, status: options.succeeded ? "succeeded" : "failed", targets: options.electronLaunch?.targets, version: options.electronLaunch?.version } : undefined,
359
416
  ...options.categoryDetails,
417
+ agentBrowserStarted: options.processResult.agentBrowserStarted,
418
+ browserWindow,
419
+ lifecycle,
420
+ readSource: getReadSource(options),
360
421
  aboutBlankSessionMismatch: options.aboutBlankSessionMismatch,
361
422
  electronPostCommandHealth: options.electronPostCommandHealth,
362
423
  electronRefFreshness: options.electronRefFreshnessDiagnostic,
@@ -416,6 +477,8 @@ function buildAgentBrowserResultDetails(options, nextActions) {
416
477
  }
417
478
  export function buildFinalAgentBrowserToolResult(options) {
418
479
  const nextActions = applyNamespaceToNextActions(buildResultNextActions(options), options.executionPlan.namespace);
480
+ const lifecycle = extractAgentBrowserLifecycle(options.presentationEnvelope?.data);
481
+ const browserWindow = buildBrowserWindowStatus(options, lifecycle);
419
482
  const details = buildAgentBrowserResultDetails(options, nextActions);
420
483
  const visibleRefFallbackText = formatVisibleRefFallbackText(options.visibleRefFallbackDiagnostic);
421
484
  const richInputRecoveryText = formatRichInputRecoveryText(options.richInputRecoveryDiagnostic);
@@ -432,9 +495,12 @@ export function buildFinalAgentBrowserToolResult(options) {
432
495
  const evalStdinHintText = formatEvalStdinHintText(options.evalStdinHint);
433
496
  const evalResultWarningText = formatEvalResultWarningText(options.evalResultWarning);
434
497
  const artifactCleanupText = formatArtifactCleanupGuidanceText(options.artifactCleanup);
435
- const timeoutPartialProgressText = options.timeoutPartialProgress ? formatTimeoutPartialProgressText(options.timeoutPartialProgress) : undefined;
498
+ const timeoutPartialProgressText = options.timeoutPartialProgress ? formatTimeoutPartialProgressText(options.timeoutPartialProgress, options.currentSessionTabTargetUnknown === true && !(options.sessionMode === "fresh" && options.timeoutPartialProgress.liveUrlRecovered !== true)) : undefined;
436
499
  const managedSessionOutcomeText = formatManagedSessionOutcomeText(options.managedSessionOutcome);
437
- const rawAppendedDiagnosticText = [visibleRefFallbackText, richInputRecoveryText, semanticActionCandidateText, clickDispatchText, overlayBlockerText, fillVerificationText, electronRefFreshnessText, selectorTextVisibilityText, electronBroadGetTextScopeText, scrollNoopDiagnosticText, comboboxFocusDiagnosticText, recordingDependencyWarningText, evalStdinHintText, evalResultWarningText, artifactCleanupText, timeoutPartialProgressText, managedSessionOutcomeText].filter((item) => item !== undefined).join("\n\n");
500
+ const readExecutionText = formatReadExecutionText(options, lifecycle);
501
+ const browserWindowText = formatBrowserWindowText(browserWindow);
502
+ const failureNextActionsText = formatFailureNextActionsText(options, nextActions);
503
+ const rawAppendedDiagnosticText = [visibleRefFallbackText, richInputRecoveryText, semanticActionCandidateText, clickDispatchText, overlayBlockerText, fillVerificationText, electronRefFreshnessText, selectorTextVisibilityText, electronBroadGetTextScopeText, scrollNoopDiagnosticText, comboboxFocusDiagnosticText, recordingDependencyWarningText, evalStdinHintText, evalResultWarningText, artifactCleanupText, timeoutPartialProgressText, managedSessionOutcomeText, readExecutionText, browserWindowText, failureNextActionsText].filter((item) => item !== undefined).join("\n\n");
438
504
  const appendedDiagnosticText = redactSensitiveText(redactExactSensitiveText(rawAppendedDiagnosticText, options.exactSensitiveValues));
439
505
  const shouldAppendDiagnosticText = appendedDiagnosticText.length > 0 && (!options.userRequestedJson || options.plainTextInspection);
440
506
  let content = shouldAppendDiagnosticText && options.redactedContent[0]?.type === "text" ? [{ ...options.redactedContent[0], text: `${options.redactedContent[0].text}\n\n${appendedDiagnosticText}` }, ...options.redactedContent.slice(1)] : options.redactedContent;
@@ -462,5 +528,5 @@ export async function buildMissingBinaryFailureResult(options) {
462
528
  missingBinaryElectronRecord = missingBinaryElectronCleanup.record;
463
529
  }
464
530
  const textParts = [errorText, managedSessionOutcomeText, missingBinaryElectronCleanup ? `Electron cleanup after failed attach: ${missingBinaryElectronCleanup.summary}` : undefined].filter((part) => part !== undefined && part.length > 0);
465
- return { content: [{ type: "text", text: textParts.join("\n\n") }], details: { args: options.redactedArgs, compatibilityWorkaround: options.compatibilityWorkaround, effectiveArgs: options.redactedProcessArgs, electron: missingBinaryElectronRecord ? { action: "launch", cleanup: missingBinaryElectronCleanup, launch: missingBinaryElectronRecord, status: "failed", targets: options.electronLaunch?.targets, version: options.electronLaunch?.version } : undefined, managedSessionOutcome, namespace: options.executionPlan.namespace, nextActions: managedSessionRecoveryNextActions.length > 0 ? managedSessionRecoveryNextActions : undefined, sessionMode: options.sessionMode, sessionTabCorrection: options.sessionTabCorrection, ...buildAgentBrowserResultCategoryDetails({ args: options.redactedProcessArgs, command: options.executionPlan.commandInfo.command, errorText, failureCategory: "missing-binary", spawnError, succeeded: false }), spawnError }, isError: true };
531
+ return { content: [{ type: "text", text: textParts.join("\n\n") }], details: { agentBrowserStarted: options.processResult.agentBrowserStarted, args: options.redactedArgs, compatibilityWorkaround: options.compatibilityWorkaround, effectiveArgs: options.redactedProcessArgs, electron: missingBinaryElectronRecord ? { action: "launch", cleanup: missingBinaryElectronCleanup, launch: missingBinaryElectronRecord, status: "failed", targets: options.electronLaunch?.targets, version: options.electronLaunch?.version } : undefined, managedSessionOutcome, namespace: options.executionPlan.namespace, nextActions: managedSessionRecoveryNextActions.length > 0 ? managedSessionRecoveryNextActions : undefined, sessionMode: options.sessionMode, sessionTabCorrection: options.sessionTabCorrection, ...buildAgentBrowserResultCategoryDetails({ args: options.redactedProcessArgs, command: options.executionPlan.commandInfo.command, errorText, failureCategory: "missing-binary", spawnError, succeeded: false }), spawnError }, isError: true };
466
532
  }
@@ -20,6 +20,7 @@ async function runAgentBrowserToolInContext(options) {
20
20
  const ownedManagedSession = prepared.ownedManagedSessionContext;
21
21
  return await withOwnedManagedSessionContext(ownedManagedSession, async () => {
22
22
  try {
23
+ const artifactRunStartedAtMs = Date.now();
23
24
  const processResult = await runAgentBrowserProcess({
24
25
  args: prepared.processArgs,
25
26
  cwd: options.cwd,
@@ -51,7 +52,7 @@ async function runAgentBrowserToolInContext(options) {
51
52
  });
52
53
  if (missingBinaryResult)
53
54
  return missingBinaryResult;
54
- const output = await processBrowserOutput({ ...options, prepared, processResult });
55
+ const output = await processBrowserOutput({ ...options, artifactRunStartedAtMs, prepared, processResult });
55
56
  applyBrowserRunStatePatch(options.state, output.statePatch);
56
57
  return output.result;
57
58
  }
@@ -3,13 +3,14 @@ import { acquireManagedSessionPolicyLock } from "../../managed-session-policy-lo
3
3
  import { pruneOwnedManagedSessionRestoreSnapshots, resolveExplicitAutosaveInterval, } from "../../managed-session-restore.js";
4
4
  import { isManagedSessionRestoreKey } from "../../managed-session-storage.js";
5
5
  import { isRecord } from "../../parsing.js";
6
+ import { getAgentBrowserProcessEnvironment } from "../../process-environment.js";
6
7
  import { runAgentBrowserProcess } from "../../process.js";
7
8
  import { getAgentBrowserErrorText, parseAgentBrowserEnvelope } from "../../results/envelope.js";
8
9
  import { redactInvocationArgs } from "../../runtime.js";
9
10
  const MANAGED_SESSION_DAEMON_INSPECTION_TIMEOUT_MS = 35_000;
10
11
  const RUNNING_HEADED_AUTOSAVE_POLICY_CHANGE_ERROR = "AGENT_BROWSER_AUTOSAVE_INTERVAL_MS cannot change a running wrapper-owned headed session's launch-time periodic autosave interval. Close that session first, then retry with sessionMode: \"fresh\" so the new daemon starts with the requested interval.";
11
12
  export function getRunningHeadedAutosavePolicyChangeError(recordedInterval, closeCommand = false) {
12
- const explicitInterval = resolveExplicitAutosaveInterval(process.env.AGENT_BROWSER_AUTOSAVE_INTERVAL_MS);
13
+ const explicitInterval = resolveExplicitAutosaveInterval(getAgentBrowserProcessEnvironment().AGENT_BROWSER_AUTOSAVE_INTERVAL_MS);
13
14
  return recordedInterval !== undefined && !closeCommand && explicitInterval !== undefined && explicitInterval !== recordedInterval
14
15
  ? RUNNING_HEADED_AUTOSAVE_POLICY_CHANGE_ERROR
15
16
  : undefined;
@@ -19,7 +20,6 @@ function getHeadedManagedAutosaveEnv(interval) {
19
20
  }
20
21
  export async function inspectManagedSessionDaemon(options) {
21
22
  const processResult = await runAgentBrowserProcess({
22
- allowManagedSessionTarget: options.allowManagedSessionTarget,
23
23
  args: ["--json", "--namespace", options.namespace ?? "", "--session", options.sessionName, "session", "info"],
24
24
  cwd: options.cwd,
25
25
  env: getHeadedManagedAutosaveEnv(options.headedManagedAutosaveInterval),
@@ -67,7 +67,6 @@ export async function acquireOwnedManagedSessionDaemonPolicy(options) {
67
67
  }
68
68
  try {
69
69
  const daemon = await inspectManagedSessionDaemon({
70
- allowManagedSessionTarget: true,
71
70
  cwd: context.cwd,
72
71
  headedManagedAutosaveInterval: context.headedManagedAutosaveInterval,
73
72
  namespace: context.namespace,
@@ -79,7 +78,7 @@ export async function acquireOwnedManagedSessionDaemonPolicy(options) {
79
78
  if (options.mode === "close") {
80
79
  if (daemon.status === "active")
81
80
  context.restoreState.recordDaemonRestoreKey(context.sessionName, context.namespace, daemon.restoreKey);
82
- return { lock };
81
+ return { daemonStatus: daemon.status, lock };
83
82
  }
84
83
  const stickyDisabled = context.restoreState.isDisabled(context.sessionName, context.namespace);
85
84
  const hasKnownDaemonRestoreKey = context.restoreState.hasDaemonRestoreKey(context.sessionName, context.namespace);
@@ -101,13 +100,14 @@ export async function acquireOwnedManagedSessionDaemonPolicy(options) {
101
100
  context.restoreState.recordDaemonRestoreKey(context.sessionName, context.namespace, daemon.restoreKey);
102
101
  return !["inactive", "missing-binary"].includes(daemon.status) && !activePolicyMatches
103
102
  ? {
103
+ daemonStatus: daemon.status,
104
104
  error: [
105
105
  "This wrapper-owned session's live daemon does not match the requested managed-restore policy.",
106
106
  "Close that session first, retry with sessionMode: \"fresh\", or use a distinct explicit --session.",
107
107
  ].join(" "),
108
108
  lock,
109
109
  }
110
- : { lock };
110
+ : { daemonStatus: daemon.status, lock };
111
111
  }
112
112
  catch (error) {
113
113
  await lock.release();
@@ -118,7 +118,7 @@ export async function closeManagedSession(options) {
118
118
  const controller = new AbortController();
119
119
  const timer = setTimeout(() => controller.abort(), options.timeoutMs);
120
120
  let stdoutSpillPath;
121
- const closeArgs = [...(options.namespace ? ["--namespace", options.namespace] : []), "--session", options.sessionName, "close"];
121
+ const closeArgs = [...(options.namespace !== undefined ? ["--namespace", options.namespace] : []), "--session", options.sessionName, "close"];
122
122
  const policyLock = options.policyLock ?? await acquireManagedSessionPolicyLock({
123
123
  namespace: options.namespace,
124
124
  sessionName: options.sessionName,
@@ -131,7 +131,6 @@ export async function closeManagedSession(options) {
131
131
  }
132
132
  try {
133
133
  const daemon = await inspectManagedSessionDaemon({
134
- allowManagedSessionTarget: true,
135
134
  cwd: options.cwd,
136
135
  headedManagedAutosaveInterval: options.headedManagedAutosaveInterval,
137
136
  namespace: options.namespace,
@@ -2,6 +2,7 @@ import { isRecord } from "../../../parsing.js";
2
2
  import { buildAgentBrowserResultCategoryDetails } from "../../../results/categories.js";
3
3
  import { buildSnapshotPresentation } from "../../../results/snapshot.js";
4
4
  import { extractRefSnapshotFromData } from "../../../session-page-state.js";
5
+ import { redactSensitiveText } from "../../../runtime.js";
5
6
  import { collectScrollPositionSnapshot } from "../diagnostics.js";
6
7
  import { buildSessionDetailFields, runSessionCommandData } from "../session-state.js";
7
8
  function parseSnapshotFilterRequest(commandTokens) {
@@ -42,6 +43,116 @@ function parseSnapshotFilterRequest(commandTokens) {
42
43
  return undefined;
43
44
  return { cleanArgs, diff, role, search, viewport };
44
45
  }
46
+ const RENDERED_TEXT_SEARCH_MAX_MATCHES = 8;
47
+ function buildRenderedTextSearchEval(search) {
48
+ return `(() => {
49
+ const query = ${JSON.stringify(search.trim().toLowerCase())};
50
+ const limit = ${RENDERED_TEXT_SEARCH_MAX_MATCHES};
51
+ const normalize = (value) => String(value ?? "").replace(/\\s+/g, " ").trim();
52
+ const attributeTextOf = (element) => [element.getAttribute("aria-label"), element.getAttribute("title"), element.getAttribute("placeholder")].filter(Boolean).join(" ");
53
+ const searchableTextOf = (element) => normalize([attributeTextOf(element), element.textContent].filter(Boolean).join(" "));
54
+ const renderedTextOf = (element) => normalize([attributeTextOf(element), element instanceof HTMLElement ? element.innerText : element.textContent].filter(Boolean).join(" "));
55
+ const isRendered = (element) => {
56
+ if (!(element instanceof Element) || element.getClientRects().length === 0) return false;
57
+ const visibility = getComputedStyle(element).visibility;
58
+ if (visibility === "hidden" || visibility === "collapse") return false;
59
+ for (let current = element; current; current = current.parentElement) {
60
+ if (Number(getComputedStyle(current).opacity) === 0) return false;
61
+ }
62
+ return true;
63
+ };
64
+ const validationPattern = /(?:^|[-_\\s])(?:error|invalid|validation|warning)(?:$|[-_\\s])/i;
65
+ const validationAncestor = (element) => {
66
+ for (let current = element; current && current !== document.body; current = current.parentElement) {
67
+ const role = current.getAttribute("role");
68
+ const live = current.getAttribute("aria-live");
69
+ const descriptor = [current.id, current.className, current.getAttribute("data-testid")].map(normalize).join(" ");
70
+ if (["alert", "alertdialog", "status"].includes(role || "") || current.getAttribute("aria-invalid") === "true" || (live && live !== "off") || validationPattern.test(descriptor)) return current;
71
+ }
72
+ return null;
73
+ };
74
+ const candidates = Array.from(document.querySelectorAll("body *")).filter((element) => searchableTextOf(element).toLowerCase().includes(query));
75
+ const candidateSet = new Set(candidates);
76
+ const minimal = candidates.filter((element) => !Array.from(element.children).some((child) => candidateSet.has(child)));
77
+ const rendered = minimal.filter((element) => isRendered(element) && renderedTextOf(element).toLowerCase().includes(query));
78
+ const matches = rendered.map((element, index) => {
79
+ const semantic = validationAncestor(element);
80
+ const source = semantic || element;
81
+ const text = renderedTextOf(element);
82
+ const matchIndex = text.toLowerCase().indexOf(query);
83
+ const start = Math.max(0, matchIndex - 80);
84
+ const snippet = text.slice(start, start + 240);
85
+ const rect = element.getBoundingClientRect();
86
+ const name = normalize(element.getAttribute("aria-label") || element.getAttribute("title") || element.getAttribute("placeholder")) || undefined;
87
+ return {
88
+ index,
89
+ kind: semantic ? "validation" : "text",
90
+ name,
91
+ offscreen: rect.bottom < 0 || rect.right < 0 || rect.top > innerHeight || rect.left > innerWidth,
92
+ role: source.getAttribute("role") || undefined,
93
+ tagName: element.tagName.toLowerCase(),
94
+ text: snippet,
95
+ };
96
+ }).sort((left, right) => (left.kind === right.kind ? left.index - right.index : left.kind === "validation" ? -1 : 1));
97
+ const unique = [];
98
+ const seen = new Set();
99
+ for (const match of matches) {
100
+ const key = [match.kind, match.role, match.name, match.text].join("\\n");
101
+ if (seen.has(key)) continue;
102
+ seen.add(key);
103
+ const { index, ...visible } = match;
104
+ unique.push(visible);
105
+ }
106
+ return { matches: unique.slice(0, limit), totalMatches: unique.length, truncated: unique.length > limit };
107
+ })()`;
108
+ }
109
+ function extractRenderedTextSearchResult(data) {
110
+ const result = isRecord(data) && isRecord(data.result) ? data.result : data;
111
+ if (!isRecord(result) || !Array.isArray(result.matches))
112
+ return undefined;
113
+ const matches = result.matches.flatMap((value) => {
114
+ if (!isRecord(value) || (value.kind !== "text" && value.kind !== "validation") || typeof value.offscreen !== "boolean" || typeof value.tagName !== "string" || typeof value.text !== "string")
115
+ return [];
116
+ return [{
117
+ kind: value.kind,
118
+ ...(typeof value.name === "string" ? { name: redactSensitiveText(value.name) } : {}),
119
+ offscreen: value.offscreen,
120
+ ...(typeof value.role === "string" ? { role: value.role } : {}),
121
+ tagName: value.tagName,
122
+ text: redactSensitiveText(value.text),
123
+ }];
124
+ }).slice(0, RENDERED_TEXT_SEARCH_MAX_MATCHES);
125
+ const totalMatches = typeof result.totalMatches === "number" && Number.isInteger(result.totalMatches) && result.totalMatches >= matches.length ? result.totalMatches : matches.length;
126
+ return { matches, totalMatches, truncated: result.truncated === true || totalMatches > matches.length };
127
+ }
128
+ function attachRenderedTextMatchRefs(matches, snapshotData) {
129
+ if (!isRecord(snapshotData) || !isRecord(snapshotData.refs))
130
+ return matches;
131
+ const refs = snapshotData.refs;
132
+ return matches.map((match) => {
133
+ if (!match.name)
134
+ return match;
135
+ const normalizedName = match.name.replace(/\s+/g, " ").trim().toLowerCase();
136
+ const candidates = Object.entries(refs).filter(([, value]) => {
137
+ if (!isRecord(value) || typeof value.name !== "string")
138
+ return false;
139
+ const nameMatches = value.name.replace(/\s+/g, " ").trim().toLowerCase() === normalizedName;
140
+ return nameMatches && (!match.role || typeof value.role !== "string" || value.role.toLowerCase() === match.role.toLowerCase());
141
+ });
142
+ return candidates.length === 1 ? { ...match, ref: candidates[0][0] } : match;
143
+ });
144
+ }
145
+ function formatRenderedTextSearchMatches(result) {
146
+ if (!result || result.matches.length === 0)
147
+ return undefined;
148
+ const lines = result.matches.map((match) => {
149
+ const context = [match.kind === "validation" ? "validation" : undefined, match.offscreen ? "outside viewport" : undefined, match.role, match.tagName, match.ref ? `@${match.ref}` : undefined].filter(Boolean).join(", ");
150
+ return `- ${JSON.stringify(match.text)}${context ? ` (${context})` : ""}`;
151
+ });
152
+ if (result.truncated)
153
+ lines.push(`- ... (${result.totalMatches - result.matches.length} additional rendered-text matches omitted)`);
154
+ return ["Rendered page text matches:", ...lines].join("\n");
155
+ }
45
156
  function buildSnapshotDiff(previous, current) {
46
157
  if (!current)
47
158
  return undefined;
@@ -113,6 +224,11 @@ export async function trySnapshotFilter(options) {
113
224
  const filtered = request.role || request.search ? filterSnapshotData(snapshotData, request) : isRecord(snapshotData) ? { data: snapshotData, matchedRefs: isRecord(snapshotData.refs) ? Object.keys(snapshotData.refs).length : 0, totalLines: typeof snapshotData.snapshot === "string" ? snapshotData.snapshot.split(/\r?\n/).filter((line) => line.length > 0).length : 0, totalRefs: isRecord(snapshotData.refs) ? Object.keys(snapshotData.refs).length : 0, visibleLines: typeof snapshotData.snapshot === "string" ? snapshotData.snapshot.split(/\r?\n/).filter((line) => line.length > 0).length : 0 } : undefined;
114
225
  if (!filtered)
115
226
  return undefined;
227
+ const renderedTextSearch = request.search
228
+ ? extractRenderedTextSearchResult(await runSessionCommandData({ args: ["eval", "--stdin"], cwd: options.cwd, namespace: options.namespace, sessionName: options.sessionName, signal: options.signal, stdin: buildRenderedTextSearchEval(request.search) }))
229
+ : undefined;
230
+ if (renderedTextSearch)
231
+ renderedTextSearch.matches = attachRenderedTextMatchRefs(renderedTextSearch.matches, snapshotData);
116
232
  const viewport = request.viewport ? await collectScrollPositionSnapshot({ cwd: options.cwd, namespace: options.namespace, sessionName: options.sessionName, signal: options.signal }) : undefined;
117
233
  const fullSnapshot = extractRefSnapshotFromData(snapshotData);
118
234
  const diff = request.diff ? buildSnapshotDiff(options.previousRefSnapshot, fullSnapshot) : undefined;
@@ -126,7 +242,8 @@ export async function trySnapshotFilter(options) {
126
242
  : "Snapshot viewport metadata collected.";
127
243
  const viewportText = viewport ? `Viewport: ${viewport.innerWidth}×${viewport.innerHeight}, scroll ${viewport.scrollX},${viewport.scrollY}, document ${viewport.scrollWidth}×${viewport.scrollHeight}, sampled scroll containers ${viewport.containers.length}/${viewport.containerCount}.` : undefined;
128
244
  const diffText = diff && (request.role || request.search) ? diff.summary : undefined;
129
- const prefix = [summary, diffText, viewportText].filter((line) => line !== undefined).join("\n");
245
+ const renderedTextSearchText = formatRenderedTextSearchMatches(renderedTextSearch);
246
+ const prefix = [summary, renderedTextSearchText, diffText, viewportText].filter((line) => line !== undefined).join("\n\n");
130
247
  if (presentation.content[0]?.type === "text")
131
248
  presentation.content[0] = { ...presentation.content[0], text: `${prefix}\n\n${presentation.content[0].text}` };
132
249
  return {
@@ -146,7 +263,7 @@ export async function trySnapshotFilter(options) {
146
263
  refSnapshot: fullSnapshot,
147
264
  sessionMode: options.sessionMode,
148
265
  snapshotDiff: diff,
149
- snapshotFilter: request.role || request.search ? { cleanArgs: request.cleanArgs, matchedRefs: filtered.matchedRefs, role: request.role, search: request.search, totalLines: filtered.totalLines, totalRefs: filtered.totalRefs, visibleLines: filtered.visibleLines } : undefined,
266
+ snapshotFilter: request.role || request.search ? { cleanArgs: request.cleanArgs, matchedRefs: filtered.matchedRefs, renderedTextMatches: renderedTextSearch?.matches, renderedTextTotalMatches: renderedTextSearch?.totalMatches, renderedTextTruncated: renderedTextSearch?.truncated, role: request.role, search: request.search, totalLines: filtered.totalLines, totalRefs: filtered.totalRefs, visibleLines: filtered.visibleLines } : undefined,
150
267
  snapshotViewport: viewport,
151
268
  ...buildAgentBrowserResultCategoryDetails({ args: options.effectiveArgs, command: "snapshot", succeeded: true }),
152
269
  ...buildSessionDetailFields(options.sessionName, options.usedImplicitSession, options.namespace, options.managedSessionRestoreDisabled()),
@@ -1,7 +1,7 @@
1
1
  import { GLOBAL_BOOLEAN_FLAGS_WITH_OPTIONAL_VALUES, VALUE_FLAGS } from "../../../argv-grammar.js";
2
2
  import { isOpenNavigationCommand } from "../../../command-taxonomy.js";
3
3
  import { getAgentBrowserProcessTimeoutMs } from "../../../process.js";
4
- import { parseValidBatchStepEntries } from "../../batch-stdin.js";
4
+ import { getUpstreamEffectiveBatchSteps } from "../../batch-stdin.js";
5
5
  const POSITIONAL_VALUE_FLAGS = new Set([...VALUE_FLAGS, "--llms"]);
6
6
  const COMMAND_PROCESS_TIMEOUT_GRACE_MS = 5_000;
7
7
  function parseMillisecondsToken(token) {
@@ -12,7 +12,8 @@ function parseMillisecondsToken(token) {
12
12
  return Number.isSafeInteger(parsed) ? parsed : undefined;
13
13
  }
14
14
  function findCommandTimeoutMs(commandTokens) {
15
- if (commandTokens[0] !== "wait" && commandTokens[0] !== "read")
15
+ const [command, subcommand] = commandTokens;
16
+ if (command !== "wait" && command !== "read" && !(command === "webmcp" && ["invoke", "result"].includes(subcommand ?? "")))
16
17
  return undefined;
17
18
  for (let index = 1; index < commandTokens.length; index += 1) {
18
19
  const token = commandTokens[index];
@@ -73,11 +74,11 @@ function findCommandTimeoutBudgetMs(commandTokens, stdin, activePageUrl) {
73
74
  const directTimeout = commandTimeoutBudgetMs(commandTokens, activePageUrl);
74
75
  if (directTimeout !== undefined)
75
76
  return directTimeout;
76
- if (commandTokens[0] !== "batch" || stdin === undefined)
77
+ if (commandTokens[0] !== "batch")
77
78
  return undefined;
78
79
  let batchTimeoutTotal = 0;
79
80
  let batchPageUrl = activePageUrl;
80
- for (const { step } of parseValidBatchStepEntries(stdin)) {
81
+ for (const step of getUpstreamEffectiveBatchSteps(commandTokens, stdin)) {
81
82
  batchTimeoutTotal += commandTimeoutBudgetMs(step, batchPageUrl) ?? 0;
82
83
  if (isOpenNavigationCommand(step[0]))
83
84
  batchPageUrl = findFirstPositionalArgument(step);
@@ -87,10 +88,10 @@ function findCommandTimeoutBudgetMs(commandTokens, stdin, activePageUrl) {
87
88
  export function commandTimeoutNeedsActivePageUrl(commandTokens, stdin) {
88
89
  if (commandTokens[0] === "read")
89
90
  return findCommandTimeoutMs(commandTokens) !== undefined && readUsesActivePageUrl(commandTokens);
90
- if (commandTokens[0] !== "batch" || stdin === undefined)
91
+ if (commandTokens[0] !== "batch")
91
92
  return false;
92
93
  let hasKnownPageUrl = false;
93
- for (const { step } of parseValidBatchStepEntries(stdin)) {
94
+ for (const step of getUpstreamEffectiveBatchSteps(commandTokens, stdin)) {
94
95
  if (isOpenNavigationCommand(step[0]) && findFirstPositionalArgument(step))
95
96
  hasKnownPageUrl = true;
96
97
  if (!hasKnownPageUrl && findCommandTimeoutMs(step) !== undefined && step[0] === "read" && readUsesActivePageUrl(step))