pi-agent-browser-native 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/CHANGELOG.md +127 -0
  2. package/README.md +63 -20
  3. package/dist/extensions/agent-browser/index.js +787 -105
  4. package/dist/extensions/agent-browser/lib/argv-descriptor.js +35 -3
  5. package/dist/extensions/agent-browser/lib/argv-grammar.js +44 -2
  6. package/dist/extensions/agent-browser/lib/batch-lifecycle.js +71 -0
  7. package/dist/extensions/agent-browser/lib/command-policy.js +1 -1
  8. package/dist/extensions/agent-browser/lib/command-taxonomy.js +35 -2
  9. package/dist/extensions/agent-browser/lib/input-modes/job.js +61 -4
  10. package/dist/extensions/agent-browser/lib/input-modes/lookups.js +2 -2
  11. package/dist/extensions/agent-browser/lib/input-modes/params.js +22 -23
  12. package/dist/extensions/agent-browser/lib/input-modes/script.js +462 -0
  13. package/dist/extensions/agent-browser/lib/input-modes/semantic-action.js +51 -12
  14. package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +8 -0
  15. package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +3 -1
  16. package/dist/extensions/agent-browser/lib/managed-session-restore.js +26 -36
  17. package/dist/extensions/agent-browser/lib/managed-session-snapshots.js +2 -4
  18. package/dist/extensions/agent-browser/lib/managed-session-state-policy.js +47 -29
  19. package/dist/extensions/agent-browser/lib/managed-session-storage.js +50 -24
  20. package/dist/extensions/agent-browser/lib/orchestration/batch-stdin.js +26 -5
  21. package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.js +110 -30
  22. package/dist/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.js +2 -1
  23. package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +26 -25
  24. package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +17 -3
  25. package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +2 -1
  26. package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +6 -4
  27. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +129 -32
  28. package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +191 -55
  29. package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +57 -29
  30. package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +15 -11
  31. package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +36 -18
  32. package/dist/extensions/agent-browser/lib/orchestration/script-mode.js +299 -0
  33. package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +32 -10
  34. package/dist/extensions/agent-browser/lib/playbook.js +18 -15
  35. package/dist/extensions/agent-browser/lib/process-environment.js +14 -0
  36. package/dist/extensions/agent-browser/lib/process-identity.js +4 -4
  37. package/dist/extensions/agent-browser/lib/process.js +131 -41
  38. package/dist/extensions/agent-browser/lib/recording-reservations.js +183 -0
  39. package/dist/extensions/agent-browser/lib/results/action-recommendations.js +62 -5
  40. package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +62 -4
  41. package/dist/extensions/agent-browser/lib/results/categories.js +6 -1
  42. package/dist/extensions/agent-browser/lib/results/next-actions.js +19 -5
  43. package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +85 -38
  44. package/dist/extensions/agent-browser/lib/results/presentation/batch.js +66 -11
  45. package/dist/extensions/agent-browser/lib/results/presentation/common.js +18 -0
  46. package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +7 -1
  47. package/dist/extensions/agent-browser/lib/results/presentation/errors.js +2 -1
  48. package/dist/extensions/agent-browser/lib/results/presentation/navigation.js +26 -11
  49. package/dist/extensions/agent-browser/lib/results/presentation/registry.js +58 -13
  50. package/dist/extensions/agent-browser/lib/results/presentation/semantic-action.js +1 -10
  51. package/dist/extensions/agent-browser/lib/results/presentation.js +6 -3
  52. package/dist/extensions/agent-browser/lib/results/recovery-actions.js +2 -0
  53. package/dist/extensions/agent-browser/lib/results/selector-recovery.js +51 -8
  54. package/dist/extensions/agent-browser/lib/results/snapshot-high-value-controls.js +13 -7
  55. package/dist/extensions/agent-browser/lib/runtime.js +116 -39
  56. package/dist/extensions/agent-browser/lib/session-page-state.js +62 -10
  57. package/dist/extensions/agent-browser/lib/upstream-version.js +14 -0
  58. package/dist/extensions/agent-browser/script-worker.js +169 -0
  59. package/dist/scripts/agent-browser-target.mjs +3 -0
  60. package/docs/ARCHITECTURE.md +40 -21
  61. package/docs/COMMAND_REFERENCE.md +90 -35
  62. package/docs/RELEASE.md +3 -3
  63. package/docs/REQUIREMENTS.md +4 -2
  64. package/docs/SUPPORT_MATRIX.md +26 -19
  65. package/docs/TOOL_CONTRACT.md +93 -52
  66. package/package.json +3 -1
  67. package/platform-smoke.config.mjs +2 -2
  68. package/scripts/agent-browser-capability-baseline.mjs +24 -6
  69. package/scripts/agent-browser-target.mjs +3 -0
  70. package/scripts/build.mjs +41 -0
  71. package/scripts/doctor.mjs +7 -6
  72. package/scripts/platform-smoke/browser-dogfood-windows.ps1 +9 -3
  73. package/scripts/platform-smoke/targets.mjs +12 -6
@@ -1,14 +1,17 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import { basename } from "node:path";
3
- import { findCommandStartIndex, parseArgvDescriptor, parseCommandInfo, } from "./argv-descriptor.js";
4
- import { canonicalizeAgentBrowserNamespace, extractExplicitNamespace, extractExplicitSessionName, getAgentBrowserSessionIdentityKey, getBooleanFlagValue, GLOBAL_VALUE_FLAGS_ALLOWING_DASH_VALUE, isUpstreamEnvFlagEnabled, PREVALIDATED_VALUE_FLAGS, resolveAgentBrowserNamespace, scanUpstreamGlobalFlagOccurrences, } from "./argv-grammar.js";
3
+ import { extractUpstreamCommandTokens, findCommandStartIndex, parseArgvDescriptor, parseCommandInfo, } from "./argv-descriptor.js";
4
+ import { batchHasSuccessfulCloseAll, getSuccessfulBatchCloseLifecycle } from "./batch-lifecycle.js";
5
+ import { canonicalizeAgentBrowserNamespace, extractExplicitNamespace, extractExplicitSessionName, getAgentBrowserSessionIdentityKey, getBooleanFlagValue, GLOBAL_VALUE_FLAGS_ALLOWING_DASH_VALUE, isAgentBrowserSessionIdentityKeyInNamespace, isUpstreamEnvFlagEnabled, PREVALIDATED_VALUE_FLAGS, resolveAgentBrowserNamespace, scanUpstreamGlobalFlagOccurrences, } from "./argv-grammar.js";
5
6
  import { needsManagedSession } from "./command-policy.js";
6
7
  import { isWrapperManagedSessionName, redactManagedSessionRestoreKeys } from "./managed-session-capabilities.js";
7
- import { isCloseCommand, isOpenNavigationCommand } from "./command-taxonomy.js";
8
+ import { isCloseAllCommand, isCloseCommand, isOpenNavigationCommand } from "./command-taxonomy.js";
8
9
  import { hasLaunchScopedFlagToken, LAUNCH_SCOPED_FLAG_DEFINITIONS, LAUNCH_SCOPED_FLAG_LABEL, } from "./launch-scoped-flags.js";
9
10
  import { MANAGED_SESSION_NAME_PREFIX, } from "./managed-session-restore.js";
10
- export { extractCommandTokens, findCommandStartIndex, parseArgvDescriptor, parseCommandInfo } from "./argv-descriptor.js";
11
+ export { extractCommandTokens, extractUpstreamCommandTokens, findCommandStartIndex, parseArgvDescriptor, parseCommandInfo, parseWaitCommandTokens } from "./argv-descriptor.js";
11
12
  import { isRecord } from "./parsing.js";
13
+ import { getAgentBrowserProcessEnvironment } from "./process-environment.js";
14
+ import { TARGET_AGENT_BROWSER_VERSION } from "./upstream-version.js";
12
15
  const OPENAI_HEADLESS_COMPAT_HOSTS = new Set(["chat.com", "chat.openai.com", "chatgpt.com"]);
13
16
  const CLOUDFLARE_HEADLESS_COMPAT_HOST = "dash.cloudflare.com";
14
17
  const AGENT_BROWSER_IDLE_TIMEOUT_ENV = "AGENT_BROWSER_IDLE_TIMEOUT_MS";
@@ -28,6 +31,7 @@ const DEFAULT_HEADLESS_COMPAT_USER_AGENT_BY_PLATFORM = {
28
31
  };
29
32
  const FALLBACK_HEADLESS_COMPAT_USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36";
30
33
  const SHELL_OPERATOR_TOKENS = new Set(["&&", "||", "|", ";", ">", ">>", "<"]);
34
+ const ANDROID_SESSION_NAME_IDENTITY_LENGTH = 20;
31
35
  const MAX_PROJECT_SLUG_LENGTH = 24;
32
36
  const SESSION_NAME_CWD_HASH_LENGTH = 8;
33
37
  const SESSION_NAME_SESSION_ID_LENGTH = 12;
@@ -311,7 +315,7 @@ function parseTimeoutMs(rawValue, minimumValue) {
311
315
  }
312
316
  return parsedValue;
313
317
  }
314
- export function getImplicitSessionIdleTimeoutMs(env = process.env) {
318
+ export function getImplicitSessionIdleTimeoutMs(env = getAgentBrowserProcessEnvironment()) {
315
319
  return parseTimeoutMs(env[IMPLICIT_SESSION_IDLE_TIMEOUT_ENV], 0) ??
316
320
  parseTimeoutMs(env[AGENT_BROWSER_IDLE_TIMEOUT_ENV], 0) ??
317
321
  DEFAULT_IMPLICIT_SESSION_IDLE_TIMEOUT_MS;
@@ -333,7 +337,7 @@ function getUnsupportedLeadingIdentityAssignment(args) {
333
337
  }
334
338
  return undefined;
335
339
  }
336
- export function getImplicitSessionCloseTimeoutMs(env = process.env) {
340
+ export function getImplicitSessionCloseTimeoutMs(env = getAgentBrowserProcessEnvironment()) {
337
341
  return parseTimeoutMs(env[IMPLICIT_SESSION_CLOSE_TIMEOUT_ENV], 0) ?? DEFAULT_IMPLICIT_SESSION_CLOSE_TIMEOUT_MS;
338
342
  }
339
343
  export function resolveManagedSessionState(options) {
@@ -381,15 +385,18 @@ function getManagedSessionRestoreRank(options) {
381
385
  function getRestorableManagedSessionName(value, fallbackSessionName) {
382
386
  return typeof value === "string" && isRestorableManagedSessionName(value, fallbackSessionName) ? value : undefined;
383
387
  }
384
- function getElectronCleanupClosedManagedSessionNames(details, fallbackSessionName) {
388
+ function getElectronCleanupClosedManagedSessions(details, fallbackSessionName) {
385
389
  const electron = isRecord(details.electron) ? details.electron : undefined;
386
390
  const cleanup = isRecord(electron?.cleanup) ? electron.cleanup : undefined;
387
391
  const results = Array.isArray(cleanup?.results) ? cleanup.results : [];
388
- const closedSessionNames = new Set();
392
+ const closedSessions = [];
389
393
  for (const result of results) {
390
394
  if (!isRecord(result) || !Array.isArray(result.steps))
391
395
  continue;
392
396
  const record = isRecord(result.record) ? result.record : undefined;
397
+ const fallbackNamespace = typeof record?.namespace === "string"
398
+ ? record.namespace
399
+ : typeof details.namespace === "string" ? details.namespace : undefined;
393
400
  for (const step of result.steps) {
394
401
  if (!isRecord(step) || step.resource !== "managed-session")
395
402
  continue;
@@ -397,11 +404,12 @@ function getElectronCleanupClosedManagedSessionNames(details, fallbackSessionNam
397
404
  continue;
398
405
  const sessionName = getRestorableManagedSessionName(step.sessionName, fallbackSessionName)
399
406
  ?? getRestorableManagedSessionName(record?.sessionName, fallbackSessionName);
407
+ const namespace = typeof step.namespace === "string" ? step.namespace : fallbackNamespace;
400
408
  if (sessionName)
401
- closedSessionNames.add(sessionName);
409
+ closedSessions.push({ namespace, sessionName });
402
410
  }
403
411
  }
404
- return [...closedSessionNames];
412
+ return closedSessions;
405
413
  }
406
414
  export function restoreManagedSessionStateFromBranch(branch, fallbackSessionName) {
407
415
  const restoreDisabledIdentities = new Map();
@@ -441,16 +449,23 @@ export function restoreManagedSessionStateFromBranch(branch, fallbackSessionName
441
449
  if (isPlainTextInspectionArgs(args)) {
442
450
  continue;
443
451
  }
444
- for (const sessionName of getElectronCleanupClosedManagedSessionNames(details, fallbackSessionName)) {
445
- applyManagedClose(sessionName);
452
+ for (const cleanupSession of getElectronCleanupClosedManagedSessions(details, fallbackSessionName)) {
453
+ applyManagedClose(cleanupSession.sessionName, cleanupSession.namespace);
446
454
  }
447
455
  const explicitSessionName = extractExplicitSessionName(args);
448
456
  const sessionName = typeof details.sessionName === "string" ? details.sessionName : undefined;
449
457
  const namespace = canonicalizeAgentBrowserNamespace(typeof details.namespace === "string" ? details.namespace : undefined);
450
458
  const sessionMode = details.sessionMode === "fresh" || details.sessionMode === "auto" ? details.sessionMode : undefined;
451
459
  const usedImplicitSession = details.usedImplicitSession === true;
460
+ const commandTokens = extractUpstreamCommandTokens(args);
452
461
  const command = typeof details.command === "string" ? details.command : parseCommandInfo(args).command;
453
- const commandClosesSession = isCloseCommand(command);
462
+ const batchCloseLifecycle = getSuccessfulBatchCloseLifecycle(details.batchSteps);
463
+ const nestedBatchEndsClosed = batchCloseLifecycle?.endsClosed === true;
464
+ const nestedBatchRemainsActive = batchCloseLifecycle?.endsClosed === false;
465
+ const closeAllApplied = details.closeAllApplied === true
466
+ || (message.isError !== true && isCloseAllCommand(commandTokens))
467
+ || batchHasSuccessfulCloseAll(details.batchSteps);
468
+ const commandClosesSession = isCloseCommand(command) || nestedBatchEndsClosed;
454
469
  const outcome = typeof details.managedSessionOutcome === "object" && details.managedSessionOutcome !== null ? details.managedSessionOutcome : undefined;
455
470
  const outcomeStatus = typeof outcome?.status === "string" ? outcome.status : undefined;
456
471
  const outcomeCurrentSessionName = typeof outcome?.currentSessionName === "string" ? outcome.currentSessionName : undefined;
@@ -459,6 +474,14 @@ export function restoreManagedSessionStateFromBranch(branch, fallbackSessionName
459
474
  ? outcomeAttemptedSessionName ?? getRestorableManagedSessionName(outcomeCurrentSessionName, fallbackSessionName) ?? getRestorableManagedSessionName(sessionName, fallbackSessionName)
460
475
  : undefined;
461
476
  const restorableDetailSessionName = getRestorableManagedSessionName(sessionName, fallbackSessionName);
477
+ if (closeAllApplied && restoredState.active) {
478
+ const restoredKey = getAgentBrowserSessionIdentityKey(restoredState.sessionName, restoredState.namespace);
479
+ const resultKey = restorableDetailSessionName ? getAgentBrowserSessionIdentityKey(restorableDetailSessionName, namespace) : undefined;
480
+ const retainsCurrentSession = nestedBatchRemainsActive && resultKey === restoredKey;
481
+ if (isAgentBrowserSessionIdentityKeyInNamespace(restoredKey, namespace) && !retainsCurrentSession) {
482
+ applyManagedClose(restoredState.sessionName, restoredState.namespace);
483
+ }
484
+ }
462
485
  const explicitCloseSessionName = commandClosesSession && explicitSessionName && restorableDetailSessionName === explicitSessionName
463
486
  ? restorableDetailSessionName
464
487
  : undefined;
@@ -467,13 +490,13 @@ export function restoreManagedSessionStateFromBranch(branch, fallbackSessionName
467
490
  if (details.managedSessionRestoreDisabled === true && typeof sessionName === "string") {
468
491
  restoreDisabledIdentities.set(getAgentBrowserSessionIdentityKey(sessionName, namespace), { namespace, sessionName });
469
492
  }
470
- const managedSessionName = !explicitSessionName &&
493
+ const managedSessionName = outcomeClosedSessionName ?? (!explicitSessionName &&
471
494
  restorableDetailSessionName &&
472
495
  (usedImplicitSession || sessionMode === "fresh")
473
496
  ? restorableDetailSessionName
474
497
  : commandClosesSession
475
- ? outcomeClosedSessionName ?? explicitCloseSessionName
476
- : undefined;
498
+ ? explicitCloseSessionName
499
+ : undefined);
477
500
  if (!managedSessionName) {
478
501
  continue;
479
502
  }
@@ -490,9 +513,11 @@ export function restoreManagedSessionStateFromBranch(branch, fallbackSessionName
490
513
  const exitCode = typeof details.exitCode === "number" ? details.exitCode : undefined;
491
514
  const outcomeActiveAfter = outcome?.activeAfter === true;
492
515
  const outcomeRepresentsActiveCurrentSession = outcomeActiveAfter && outcomeCurrentSessionName === managedSessionName && (outcomeStatus === "created" || outcomeStatus === "replaced" || outcomeStatus === "unchanged");
493
- const succeeded = outcomeRepresentsActiveCurrentSession ? true : messageIsError === undefined ? exitCode === undefined || exitCode === 0 : !messageIsError;
494
- if (commandClosesSession) {
495
- if (succeeded) {
516
+ const succeeded = outcomeRepresentsActiveCurrentSession || nestedBatchRemainsActive
517
+ ? true
518
+ : messageIsError === undefined ? exitCode === undefined || exitCode === 0 : !messageIsError;
519
+ if (commandClosesSession || outcomeClosedSessionName) {
520
+ if (nestedBatchEndsClosed || outcomeClosedSessionName || succeeded) {
496
521
  restoreDisabledIdentities.delete(getAgentBrowserSessionIdentityKey(managedSessionName, namespace));
497
522
  applyManagedClose(managedSessionName, namespace);
498
523
  }
@@ -529,15 +554,24 @@ export function createEphemeralSessionSeed() {
529
554
  function createCwdHash(cwd) {
530
555
  return createHash("sha256").update(`cwd:${cwd}`).digest("hex").slice(0, SESSION_NAME_CWD_HASH_LENGTH);
531
556
  }
532
- export function createImplicitSessionName(sessionId, cwd, ephemeralSeed) {
557
+ export function createImplicitSessionName(sessionId, cwd, ephemeralSeed, platform = process.platform) {
558
+ const normalizedSessionId = sessionId?.replaceAll("-", "").toLowerCase();
559
+ if (platform === "android") {
560
+ const identity = normalizedSessionId ? `session:${normalizedSessionId}:cwd:${cwd}` : `ephemeral:${cwd}:${ephemeralSeed}`;
561
+ const digest = createHash("sha256").update(identity).digest("hex").slice(0, ANDROID_SESSION_NAME_IDENTITY_LENGTH);
562
+ return `${MANAGED_SESSION_NAME_PREFIX}${digest}`;
563
+ }
533
564
  const slug = basename(cwd)
534
565
  .toLowerCase()
535
566
  .replace(/[^a-z0-9]+/g, "-")
536
567
  .replace(/^-+|-+$/g, "")
537
568
  .slice(0, MAX_PROJECT_SLUG_LENGTH) || "project";
538
569
  const cwdHash = createCwdHash(cwd);
539
- const stableSessionId = sessionId?.replace(/-/g, "").slice(0, SESSION_NAME_SESSION_ID_LENGTH);
540
- if (stableSessionId && stableSessionId.length > 0) {
570
+ if (normalizedSessionId) {
571
+ const stableSessionId = createHash("sha256")
572
+ .update(`session:${normalizedSessionId}`)
573
+ .digest("hex")
574
+ .slice(0, SESSION_NAME_SESSION_ID_LENGTH);
541
575
  return `${MANAGED_SESSION_NAME_PREFIX}${slug}-${stableSessionId}-${cwdHash}`;
542
576
  }
543
577
  const digest = createHash("sha256")
@@ -554,7 +588,7 @@ export function createFreshSessionName(baseSessionName, ephemeralSeed, ordinal)
554
588
  return `${baseSessionName}-fresh-${suffix}`;
555
589
  }
556
590
  function getSingleKeyCommandValidationError(args) {
557
- const { commandInfo, commandTokens } = parseArgvDescriptor(args);
591
+ const { commandInfo, upstreamCommandTokens: commandTokens } = parseArgvDescriptor(args);
558
592
  const command = commandInfo.command;
559
593
  if (command !== "press" && command !== "key" && command !== "keydown" && command !== "keyup")
560
594
  return undefined;
@@ -564,13 +598,19 @@ function getSingleKeyCommandValidationError(args) {
564
598
  return `agent-browser ${label} accepts exactly one key argument. Do not pass a selector or ref to ${label}; focus or click the target first, then run ${command} <key> (for example: focus @e1, then press Enter).`;
565
599
  }
566
600
  function getBareMcpValidationError(args) {
567
- const { commandInfo, commandTokens } = parseArgvDescriptor(args);
601
+ const { commandInfo, upstreamCommandTokens: commandTokens } = parseArgvDescriptor(args);
568
602
  if (commandInfo.command !== "mcp")
569
603
  return undefined;
570
604
  if (commandTokens.includes("--help") || commandTokens.includes("-h"))
571
605
  return undefined;
572
606
  return "agent-browser mcp starts a stdio MCP server for external MCP clients, not a one-shot native agent_browser tool workflow. Use the native agent_browser tool modes directly, or configure an MCP client to launch `agent-browser mcp`. Use `mcp --help` for help.";
573
607
  }
608
+ function getUnsupportedInlineWaitDownloadError(args) {
609
+ const descriptor = parseArgvDescriptor(args);
610
+ if (descriptor.commandInfo.command !== "wait" || !descriptor.upstreamCommandTokens.some((token) => token.startsWith("--download=")))
611
+ return undefined;
612
+ return `agent-browser ${TARGET_AGENT_BROWSER_VERSION} does not support \`wait --download=<path>\`. Pass the optional path as a separate argument: \`wait --download <path>\` (or \`wait -d <path>\`).`;
613
+ }
574
614
  export function validateToolArgs(args) {
575
615
  if (args.length === 0) {
576
616
  return "`args` must contain at least one agent-browser command token.";
@@ -583,7 +623,7 @@ export function validateToolArgs(args) {
583
623
  if (sessionModeArg) {
584
624
  return "Do not pass `--session-mode` in args. Use the top-level agent_browser `sessionMode` field instead, for example { args: [\"--profile\", \"Default\", \"open\", \"https://example.com\"], sessionMode: \"fresh\" }.";
585
625
  }
586
- return getBareMcpValidationError(args) ?? getSingleKeyCommandValidationError(args);
626
+ return getBareMcpValidationError(args) ?? getSingleKeyCommandValidationError(args) ?? getUnsupportedInlineWaitDownloadError(args);
587
627
  }
588
628
  function getInvalidValueFlagDetails(args) {
589
629
  for (let index = 0; index < args.length; index += 1) {
@@ -607,7 +647,7 @@ function getInvalidValueFlagDetails(args) {
607
647
  continue;
608
648
  }
609
649
  const receivedToken = args[index + 1];
610
- if (receivedToken === undefined) {
650
+ if (receivedToken === undefined || (normalizedToken === "--args" && receivedToken.length === 0)) {
611
651
  return {
612
652
  flag: normalizedToken,
613
653
  index,
@@ -683,7 +723,7 @@ function parseComparableNavigationUrl(url) {
683
723
  export function getDefaultHeadlessCompatUserAgent(platform = process.platform) {
684
724
  return DEFAULT_HEADLESS_COMPAT_USER_AGENT_BY_PLATFORM[platform] ?? FALLBACK_HEADLESS_COMPAT_USER_AGENT;
685
725
  }
686
- export function canUseHeadlessCompatibilityUserAgent(args, env = process.env) {
726
+ export function canUseHeadlessCompatibilityUserAgent(args, env = getAgentBrowserProcessEnvironment()) {
687
727
  if (hasFlagToken(args, "--user-agent") || hasFlagToken(args, "--args"))
688
728
  return false;
689
729
  if (hasFlagToken(args, "--cdp") || hasFlagToken(args, "--provider") || hasFlagToken(args, "-p"))
@@ -716,6 +756,14 @@ function getCompatibilityWorkaround(args, commandInfo) {
716
756
  reason: "OpenAI web properties currently challenge the default headless Chrome user agent; inject a normal Chrome user agent to preserve the default headless workflow without requiring headed mode or auto-connect.",
717
757
  };
718
758
  }
759
+ function stripExplicitSessionArgs(args) {
760
+ const sessionTokenIndexes = new Set();
761
+ for (const occurrence of scanUpstreamGlobalFlagOccurrences(args, "--session")) {
762
+ sessionTokenIndexes.add(occurrence.index);
763
+ sessionTokenIndexes.add(occurrence.index + 1);
764
+ }
765
+ return args.filter((_token, index) => !sessionTokenIndexes.has(index));
766
+ }
719
767
  function stripExplicitNamespaceArgs(args) {
720
768
  const namespaceTokenIndexes = new Set();
721
769
  for (const occurrence of scanUpstreamGlobalFlagOccurrences(args, "--namespace")) {
@@ -741,7 +789,7 @@ export function buildExecutionPlan(args, options) {
741
789
  const commandInfo = argvDescriptor.commandInfo;
742
790
  const commandNeedsManagedSession = !plainTextInspection && needsManagedSession(argvDescriptor);
743
791
  const effectiveArgs = plainTextInspection ? [...args] : args.includes("--json") ? [] : ["--json"];
744
- let namespace = explicitNamespace;
792
+ let namespace = explicitNamespacePresent ? explicitNamespace ?? "" : undefined;
745
793
  if (plainTextInspection) {
746
794
  return {
747
795
  commandInfo,
@@ -770,7 +818,7 @@ export function buildExecutionPlan(args, options) {
770
818
  plainTextInspection: false,
771
819
  startupScopedFlags: [],
772
820
  usedImplicitSession: false,
773
- validationError: `${unsupportedIdentityAssignment}=... is not supported by agent-browser 0.33.2. Pass ${unsupportedIdentityAssignment} and its value as separate arguments.`,
821
+ validationError: `${unsupportedIdentityAssignment}=... is not supported by agent-browser ${TARGET_AGENT_BROWSER_VERSION}. Pass ${unsupportedIdentityAssignment} and its value as separate arguments.`,
774
822
  };
775
823
  }
776
824
  for (const flag of ["--session", "--namespace"]) {
@@ -787,7 +835,7 @@ export function buildExecutionPlan(args, options) {
787
835
  }
788
836
  const explicitSessionName = extractExplicitSessionName(args);
789
837
  if (explicitSessionName && !isWrapperManagedSessionName(explicitSessionName)) {
790
- namespace = resolveAgentBrowserNamespace(args, process.env.AGENT_BROWSER_NAMESPACE);
838
+ namespace = resolveAgentBrowserNamespace(args, getAgentBrowserProcessEnvironment().AGENT_BROWSER_NAMESPACE);
791
839
  }
792
840
  const shouldCreateFreshManagedSession = !explicitSessionName && options.sessionMode === "fresh" && commandInfo.command !== undefined && !isCloseCommand(commandInfo.command);
793
841
  let argsToAppend = args;
@@ -816,8 +864,8 @@ export function buildExecutionPlan(args, options) {
816
864
  ].join(" ");
817
865
  }
818
866
  else {
819
- namespace = explicitNamespacePresent ? explicitNamespace : managedSessionNamespace;
820
- if (namespace)
867
+ namespace = explicitNamespacePresent ? explicitNamespace ?? "" : managedSessionNamespace;
868
+ if (namespace !== undefined)
821
869
  effectiveArgs.push("--namespace", namespace);
822
870
  effectiveArgs.push("--session", options.managedSessionName);
823
871
  if (explicitNamespacePresent)
@@ -828,7 +876,7 @@ export function buildExecutionPlan(args, options) {
828
876
  }
829
877
  }
830
878
  else if (shouldCreateFreshManagedSession && commandNeedsManagedSession) {
831
- if (namespace)
879
+ if (namespace !== undefined)
832
880
  effectiveArgs.push("--namespace", namespace);
833
881
  effectiveArgs.push("--session", options.freshSessionName);
834
882
  if (explicitNamespacePresent)
@@ -836,14 +884,43 @@ export function buildExecutionPlan(args, options) {
836
884
  managedSessionName = options.freshSessionName;
837
885
  sessionName = options.freshSessionName;
838
886
  }
839
- if (!compatibilityWorkaround
840
- && canUseHeadlessCompatibilityUserAgent(args)
841
- && options.managedSessionActive
887
+ const targetsActiveManagedSession = options.managedSessionActive
842
888
  && sessionName
843
- && getAgentBrowserSessionIdentityKey(sessionName, namespace) === getAgentBrowserSessionIdentityKey(options.managedSessionName, options.managedSessionNamespace)) {
844
- compatibilityWorkaround = options.managedSessionCompatibilityWorkaround;
889
+ && getAgentBrowserSessionIdentityKey(sessionName, namespace) === getAgentBrowserSessionIdentityKey(options.managedSessionName, options.managedSessionNamespace);
890
+ if (targetsActiveManagedSession && startupScopedFlags.length > 0 && !isCloseCommand(commandInfo.command) && !validationError) {
891
+ const recoveryArgs = explicitSessionName ? stripExplicitSessionArgs(args) : args;
892
+ recoveryHint = {
893
+ exampleArgs: recoveryArgs,
894
+ exampleParams: { args: recoveryArgs, sessionMode: "fresh" },
895
+ reason: `Launch-scoped flags (${LAUNCH_SCOPED_FLAG_LABEL}) need a fresh upstream launch once the extension-managed session is already active.`,
896
+ recommendedSessionMode: "fresh",
897
+ };
898
+ validationError = [
899
+ `The current extension-managed agent-browser session is already running, so launch-scoped flags ${startupScopedFlags.join(", ")} would replace or be ignored by upstream agent-browser.`,
900
+ explicitSessionName
901
+ ? "Remove the explicit `--session` and retry with `sessionMode: \"fresh\"` to force a fresh upstream launch."
902
+ : "Retry this call with `sessionMode: \"fresh\"` to force a fresh upstream launch.",
903
+ ].join(" ");
904
+ }
905
+ if (targetsActiveManagedSession && canUseHeadlessCompatibilityUserAgent(args)) {
906
+ if (requestedCompatibilityWorkaround && !options.managedSessionCompatibilityWorkaround && !validationError) {
907
+ compatibilityWorkaround = undefined;
908
+ const recoveryArgs = explicitSessionName ? stripExplicitSessionArgs(args) : args;
909
+ recoveryHint = {
910
+ exampleArgs: recoveryArgs,
911
+ exampleParams: { args: recoveryArgs, sessionMode: "fresh" },
912
+ reason: "The requested site compatibility user agent is launch-scoped and needs a fresh browser session.",
913
+ recommendedSessionMode: "fresh",
914
+ };
915
+ validationError = explicitSessionName
916
+ ? "The current extension-managed agent-browser session is already running without the user agent required by this site. Remove the explicit `--session` and retry with `sessionMode: \"fresh\"` so the compatibility user agent is applied at launch."
917
+ : "The current extension-managed agent-browser session is already running without the user agent required by this site. Retry this call with `sessionMode: \"fresh\"` so the compatibility user agent is applied at launch.";
918
+ }
919
+ else {
920
+ compatibilityWorkaround = requestedCompatibilityWorkaround ?? options.managedSessionCompatibilityWorkaround;
921
+ }
845
922
  }
846
- if (compatibilityWorkaround) {
923
+ if (compatibilityWorkaround && !targetsActiveManagedSession) {
847
924
  effectiveArgs.push("--user-agent", getDefaultHeadlessCompatUserAgent());
848
925
  }
849
926
  effectiveArgs.push(...argsToAppend);
@@ -1,5 +1,7 @@
1
- import { getAgentBrowserSessionIdentityKey } from "./argv-grammar.js";
2
- import { isCloseCommand, isReadOnlyDiagnosticSessionTargetCommand, isUnverifiedPageTransitionCommand } from "./command-taxonomy.js";
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, isReadOnlyDiagnosticSessionTargetCommand, isRecordPageTransitionCommand, isUnverifiedPageTransitionCommand } from "./command-taxonomy.js";
3
5
  import { isRecord } from "./parsing.js";
4
6
  import { getEditableRefEvidence } from "./results/editable-ref-evidence.js";
5
7
  import { enrichSnapshotRefEntries, getSnapshotRefEntries } from "./results/snapshot-refs.js";
@@ -93,6 +95,11 @@ export function extractSessionTabTargetFromBatchResults(data) {
93
95
  }
94
96
  const [name, subcommand] = extractBatchResultCommand(item);
95
97
  const result = item.result;
98
+ if (isCloseCommand(name)) {
99
+ currentTarget = undefined;
100
+ pendingTitle = undefined;
101
+ continue;
102
+ }
96
103
  if (name === "get" && subcommand === "title") {
97
104
  pendingTitle = extractStringResultField(result, "title");
98
105
  continue;
@@ -197,6 +204,12 @@ export function buildNoActivePageRefSnapshotInvalidation() {
197
204
  summary: "The latest snapshot for this session reported No active page. Old page-scoped refs are invalid until snapshot -i succeeds.",
198
205
  };
199
206
  }
207
+ export function buildPageTransitionRefSnapshotInvalidation(summary) {
208
+ return {
209
+ reason: "page-transition",
210
+ summary: summary ?? "A recording command (record start, or record restart with a URL) replaced or navigated the active page and invalidated the prior snapshot. Run snapshot -i before using page-scoped refs.",
211
+ };
212
+ }
200
213
  export function isNoActivePageSnapshotFailure(command, text) {
201
214
  return command === "snapshot" && /\bno active page\b/i.test(text ?? "");
202
215
  }
@@ -207,7 +220,16 @@ export function extractLatestRefSnapshotStateFromBatchResults(data) {
207
220
  for (const item of data) {
208
221
  if (!isRecord(item))
209
222
  continue;
210
- const [name] = extractBatchResultCommand(item);
223
+ const commandTokens = extractBatchResultCommand(item);
224
+ const [name] = commandTokens;
225
+ if (item.success !== false && isCloseCommand(name)) {
226
+ latestState = undefined;
227
+ continue;
228
+ }
229
+ if (isRecordPageTransitionCommand(commandTokens)) {
230
+ latestState = { invalidation: buildPageTransitionRefSnapshotInvalidation() };
231
+ continue;
232
+ }
211
233
  if (name !== "snapshot")
212
234
  continue;
213
235
  if (item.success === false) {
@@ -225,9 +247,10 @@ export function extractLatestRefSnapshotStateFromBatchResults(data) {
225
247
  }
226
248
  function getRestoredRefSnapshotInvalidation(details, command) {
227
249
  const invalidation = isRecord(details.refSnapshotInvalidation) ? details.refSnapshotInvalidation : undefined;
228
- if (invalidation && invalidation.reason === "no-active-page") {
250
+ if (invalidation?.reason === "no-active-page")
229
251
  return buildNoActivePageRefSnapshotInvalidation();
230
- }
252
+ if (invalidation?.reason === "page-transition")
253
+ return buildPageTransitionRefSnapshotInvalidation(typeof invalidation.summary === "string" ? invalidation.summary : undefined);
231
254
  const errorText = typeof details.error === "string"
232
255
  ? details.error
233
256
  : typeof details.summary === "string"
@@ -316,14 +339,27 @@ export class SessionPageState {
316
339
  const sessionName = typeof details.sessionName === "string" ? details.sessionName : undefined;
317
340
  const namespace = typeof details.namespace === "string" ? details.namespace : undefined;
318
341
  const sessionKey = getSessionPageStateKey(sessionName, namespace);
342
+ const args = Array.isArray(details.args) && details.args.every((arg) => typeof arg === "string") ? details.args : [];
343
+ const commandTokens = extractUpstreamCommandTokens(args);
344
+ const command = typeof details.command === "string" ? details.command : commandTokens[0];
345
+ const subcommand = typeof details.subcommand === "string" ? details.subcommand : commandTokens[1];
346
+ const batchCloseLifecycle = getSuccessfulBatchCloseLifecycle(details.batchSteps);
347
+ const closeAllApplied = details.closeAllApplied === true
348
+ || (message.isError !== true && isCloseAllCommand(commandTokens))
349
+ || batchHasSuccessfulCloseAll(details.batchSteps);
350
+ if (closeAllApplied) {
351
+ restoredOrder += 1;
352
+ state.clearNamespace(namespace);
353
+ if (isCloseCommand(command) || batchCloseLifecycle?.endsClosed === true)
354
+ continue;
355
+ }
319
356
  if (!sessionKey)
320
357
  continue;
321
- const command = typeof details.command === "string" ? details.command : undefined;
322
- const subcommand = typeof details.subcommand === "string" ? details.subcommand : undefined;
323
- if (isCloseCommand(command) && message.isError !== true) {
358
+ if (!closeAllApplied && ((isCloseCommand(command) && message.isError !== true) || batchCloseLifecycle)) {
324
359
  restoredOrder += 1;
325
360
  state.clearSession(sessionKey);
326
- continue;
361
+ if (isCloseCommand(command) || batchCloseLifecycle?.endsClosed === true)
362
+ continue;
327
363
  }
328
364
  const tabTarget = getRestoredSessionTabTarget(details, command, subcommand);
329
365
  const tabTargetUnknown = details.sessionTabTargetUnknown === true;
@@ -333,8 +369,11 @@ export class SessionPageState {
333
369
  continue;
334
370
  restoredOrder += 1;
335
371
  if (tabTargetUnknown) {
336
- state.refSnapshotInvalidations.delete(sessionKey);
337
372
  state.refSnapshots.delete(sessionKey);
373
+ if (refSnapshotInvalidation)
374
+ state.refSnapshotInvalidations.set(sessionKey, { ...refSnapshotInvalidation, order: restoredOrder });
375
+ else
376
+ state.refSnapshotInvalidations.delete(sessionKey);
338
377
  state.tabTargets.delete(sessionKey);
339
378
  state.tabTargetUnknownOrders.set(sessionKey, restoredOrder);
340
379
  continue;
@@ -431,6 +470,19 @@ export class SessionPageState {
431
470
  this.tabTargetUnknownOrders.delete(sessionName);
432
471
  this.tabTargets.delete(sessionName);
433
472
  }
473
+ clearNamespace(namespace) {
474
+ const sessionKeys = new Set([
475
+ ...this.refSnapshotInvalidations.keys(),
476
+ ...this.refSnapshots.keys(),
477
+ ...this.tabPinningReasons.keys(),
478
+ ...this.tabTargetUnknownOrders.keys(),
479
+ ...this.tabTargets.keys(),
480
+ ]);
481
+ for (const sessionKey of sessionKeys) {
482
+ if (isAgentBrowserSessionIdentityKeyInNamespace(sessionKey, namespace))
483
+ this.clearSession(sessionKey);
484
+ }
485
+ }
434
486
  markPinning(sessionName, reason) {
435
487
  this.tabPinningReasons.set(sessionName, reason);
436
488
  }
@@ -0,0 +1,14 @@
1
+ import { TARGET_AGENT_BROWSER_VERSION, TARGET_AGENT_BROWSER_VERSION_LABEL } from "../../../scripts/agent-browser-target.mjs";
2
+ export { TARGET_AGENT_BROWSER_VERSION, TARGET_AGENT_BROWSER_VERSION_LABEL };
3
+ export function parseAgentBrowserVersionOutput(stdout) {
4
+ const match = stdout.trim().match(/^agent-browser\s+(\S+)$/);
5
+ return match?.[1];
6
+ }
7
+ export function getAgentBrowserVersionValidationError(stdout) {
8
+ const observed = parseAgentBrowserVersionOutput(stdout);
9
+ if (observed === TARGET_AGENT_BROWSER_VERSION)
10
+ return undefined;
11
+ return observed
12
+ ? `Installed agent-browser ${observed} does not match this extension's exact ${TARGET_AGENT_BROWSER_VERSION} capability baseline. Install ${TARGET_AGENT_BROWSER_VERSION_LABEL}, run pi-agent-browser-doctor, then reload Pi.`
13
+ : `agent-browser --version returned an unrecognized value; expected exactly ${JSON.stringify(TARGET_AGENT_BROWSER_VERSION_LABEL)}. Run pi-agent-browser-doctor and install the exact supported upstream version.`;
14
+ }