pi-agent-browser-native 0.6.6 → 0.6.8

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 (42) hide show
  1. package/CHANGELOG.md +40 -1
  2. package/README.md +21 -6
  3. package/dist/extensions/agent-browser/index.js +138 -75
  4. package/dist/extensions/agent-browser/lib/command-taxonomy.js +6 -5
  5. package/dist/extensions/agent-browser/lib/electron/cleanup.js +10 -1
  6. package/dist/extensions/agent-browser/lib/input-modes/params.js +20 -7
  7. package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +0 -1
  8. package/dist/extensions/agent-browser/lib/managed-session-restore.js +13 -12
  9. package/dist/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.js +6 -25
  10. package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +2 -2
  11. package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +2 -3
  12. package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +10 -3
  13. package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +3 -0
  14. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/wait-timeouts.js +1 -1
  15. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +104 -117
  16. package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +64 -36
  17. package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +100 -135
  18. package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +3 -1
  19. package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +3 -1
  20. package/dist/extensions/agent-browser/lib/page-target-validation.js +10 -10
  21. package/dist/extensions/agent-browser/lib/parsing.js +7 -0
  22. package/dist/extensions/agent-browser/lib/playbook.js +6 -9
  23. package/dist/extensions/agent-browser/lib/process.js +28 -113
  24. package/dist/extensions/agent-browser/lib/recording-reservations.js +3 -1
  25. package/dist/extensions/agent-browser/lib/results/action-recommendations.js +5 -2
  26. package/dist/extensions/agent-browser/lib/results/envelope.js +9 -4
  27. package/dist/extensions/agent-browser/lib/results/next-actions.js +8 -0
  28. package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +45 -43
  29. package/dist/extensions/agent-browser/lib/results/presentation/batch.js +2 -1
  30. package/dist/extensions/agent-browser/lib/results/presentation/errors.js +10 -2
  31. package/dist/extensions/agent-browser/lib/results/presentation.js +9 -2
  32. package/dist/extensions/agent-browser/lib/results/recovery-actions.js +4 -4
  33. package/dist/extensions/agent-browser/lib/runtime.js +18 -2
  34. package/dist/extensions/agent-browser/lib/session-page-state.js +29 -10
  35. package/docs/ARCHITECTURE.md +8 -5
  36. package/docs/COMMAND_REFERENCE.md +25 -16
  37. package/docs/ELECTRON.md +6 -6
  38. package/docs/RELEASE.md +14 -5
  39. package/docs/REQUIREMENTS.md +1 -1
  40. package/docs/SUPPORT_MATRIX.md +18 -5
  41. package/docs/TOOL_CONTRACT.md +38 -25
  42. package/package.json +5 -1
@@ -1,6 +1,6 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
2
  import { extractUpstreamCommandTokens, parseCommandInfo } from "./argv-descriptor.js";
3
- import { canonicalizeAgentBrowserNamespace, extractExplicitNamespace, extractExplicitSessionName, extractRequestedRestoreKey, getAgentBrowserSessionIdentityKey, isUpstreamEnvFlagEnabled, scanUpstreamGlobalFlagOccurrences, } from "./argv-grammar.js";
3
+ import { canonicalizeAgentBrowserNamespace, extractExplicitNamespace, extractExplicitSessionName, extractRequestedRestoreKey, getAgentBrowserSessionIdentityKey, isUpstreamEnvFlagEnabled, resolveAgentBrowserNamespace, scanUpstreamGlobalFlagOccurrences, } from "./argv-grammar.js";
4
4
  import { hasLaunchScopedFlagToken, MANAGED_RESTORE_INCOMPATIBLE_BOOLEAN_ENVS, MANAGED_RESTORE_INCOMPATIBLE_ENVS, MANAGED_RESTORE_INCOMPATIBLE_FLAGS, } from "./launch-scoped-flags.js";
5
5
  import { createManagedSessionRestoreKey, ensureManagedSessionRestoreStorageIsSecure, getManagedSessionRestoreScope, getManagedSessionRestoreProtectedStorageEnv, hasManagedSessionRestoreProjectIdentity, resolveManagedSessionRestoreHome, } from "./managed-session-storage.js";
6
6
  import { parseUserBatchStdin } from "./orchestration/batch-stdin.js";
@@ -11,7 +11,7 @@ const AGENT_BROWSER_CONFIG_ENV = "AGENT_BROWSER_CONFIG";
11
11
  const AGENT_BROWSER_RESTORE_ENV = "AGENT_BROWSER_RESTORE";
12
12
  const MANAGED_SESSION_RESTORE_ENV = "PI_AGENT_BROWSER_MANAGED_SESSION_RESTORE";
13
13
  export const MANAGED_SESSION_NAME_PREFIX = "piab-";
14
- const MANAGED_SESSION_RESTORE_SPAWN_PINNED_ENVS = new Set([AGENT_BROWSER_CONFIG_ENV, AGENT_BROWSER_RESTORE_ENV, "AGENT_BROWSER_NAMESPACE"]);
14
+ const MANAGED_SESSION_RESTORE_SPAWN_PINNED_ENVS = new Set([AGENT_BROWSER_CONFIG_ENV, AGENT_BROWSER_RESTORE_ENV]);
15
15
  function isDisabledEnvFlag(value) {
16
16
  if (value === undefined)
17
17
  return false;
@@ -86,12 +86,13 @@ export function resolveOwnedManagedSessionContext(options) {
86
86
  }
87
87
  return undefined;
88
88
  }
89
- function ownedContextMatches(sessionName, namespace) {
89
+ function ownedContextMatches(sessionName, args) {
90
90
  const owned = ownedManagedSessionStorage.getStore();
91
- return owned && sessionName === owned.sessionName && canonicalizeAgentBrowserNamespace(namespace) === owned.namespace ? owned : undefined;
91
+ return owned && sessionName === owned.sessionName
92
+ && canonicalizeAgentBrowserNamespace(resolveAgentBrowserNamespace(args, owned.namespace)) === owned.namespace ? owned : undefined;
92
93
  }
93
94
  export function isOwnedManagedSessionTarget(args) {
94
- return ownedContextMatches(extractExplicitSessionName(args), extractExplicitNamespace(args)) !== undefined;
95
+ return ownedContextMatches(extractExplicitSessionName(args), args) !== undefined;
95
96
  }
96
97
  function hasExplicitConfigArg(args) {
97
98
  return scanUpstreamGlobalFlagOccurrences(args, "--config").length > 0;
@@ -141,7 +142,7 @@ function managedSessionRestoreOptedOut(options) {
141
142
  const effectiveEnv = { ...(options.parentEnv ?? getAgentBrowserProcessEnvironment()), ...options.env };
142
143
  return isDisabledEnvFlag(effectiveEnv[MANAGED_SESSION_RESTORE_ENV]);
143
144
  }
144
- function isManagedSessionRestoreIncompatible(options) {
145
+ function isManagedSessionRestoreIncompatible(options, namespace = extractExplicitNamespace(options.args)) {
145
146
  if (hasManagedSessionRestoreLaunchConflict(options))
146
147
  return true;
147
148
  if (managedSessionRestoreOptedOut(options))
@@ -152,13 +153,13 @@ function isManagedSessionRestoreIncompatible(options) {
152
153
  return true;
153
154
  if (options.cwd && agentBrowserConfigBlocksManagedRestore(options.cwd, effectiveEnv, args))
154
155
  return true;
155
- return !ensureManagedSessionRestoreStorageIsSecure(effectiveEnv, process.platform, extractExplicitNamespace(args));
156
+ return !ensureManagedSessionRestoreStorageIsSecure(effectiveEnv, process.platform, namespace);
156
157
  }
157
158
  function resolveManagedSessionRestorePolicy(options) {
158
159
  const parentEnv = options.parentEnv ?? getAgentBrowserProcessEnvironment();
159
160
  const sessionName = extractExplicitSessionName(options.args);
160
- const namespace = extractExplicitNamespace(options.args);
161
- const ownedContext = ownedContextMatches(sessionName, namespace);
161
+ const ownedContext = ownedContextMatches(sessionName, options.args);
162
+ const namespace = ownedContext ? ownedContext.namespace : extractExplicitNamespace(options.args);
162
163
  const restoreState = ownedContext?.restoreState ?? options.restoreState;
163
164
  const owned = (options.ownedManagedSession || ownedContext !== undefined) && restoreState !== undefined;
164
165
  return { namespace, owned, ownedContext, parentEnv, restoreState, sessionName };
@@ -234,7 +235,7 @@ export function getManagedSessionRestoreEnv(options) {
234
235
  return { [AGENT_BROWSER_RESTORE_ENV]: ownedContext.restoreKey };
235
236
  }
236
237
  const policyOptions = { ...options, parentEnv };
237
- if (managedSessionRestoreOptedOut(policyOptions) || ownedContext?.restoreSuppressed || isManagedSessionRestoreIncompatible(policyOptions))
238
+ if (managedSessionRestoreOptedOut(policyOptions) || ownedContext?.restoreSuppressed || isManagedSessionRestoreIncompatible(policyOptions, namespace))
238
239
  return {};
239
240
  if (restoreState.isDisabled(sessionName, namespace) || !sessionName)
240
241
  return {};
@@ -263,7 +264,7 @@ export function commitManagedSessionRestoreSuppression(options) {
263
264
  return;
264
265
  }
265
266
  const policyOptions = { ...options, parentEnv };
266
- if (managedSessionRestoreOptedOut(policyOptions) || ownedContext?.restoreSuppressed || isManagedSessionRestoreIncompatible(policyOptions))
267
+ if (managedSessionRestoreOptedOut(policyOptions) || ownedContext?.restoreSuppressed || isManagedSessionRestoreIncompatible(policyOptions, namespace))
267
268
  restoreState.disable(sessionName, namespace);
268
269
  }
269
270
  export function buildOwnedManagedSessionRestoreContext(options) {
@@ -281,7 +282,7 @@ export function buildOwnedManagedSessionRestoreContext(options) {
281
282
  };
282
283
  const optedOut = managedSessionRestoreOptedOut(policyOptions);
283
284
  const projectIdentityAvailable = !optedOut && hasManagedSessionRestoreProjectIdentity(ownedCwd);
284
- const incompatible = !optedOut && isManagedSessionRestoreIncompatible(policyOptions);
285
+ const incompatible = !optedOut && isManagedSessionRestoreIncompatible(policyOptions, owned.namespace);
285
286
  const enabled = !optedOut && !incompatible;
286
287
  const effectiveEnv = { ...(options.parentEnv ?? getAgentBrowserProcessEnvironment()), ...options.env };
287
288
  const restoreScope = getManagedSessionRestoreScope(owned.sessionName);
@@ -1,45 +1,28 @@
1
- import { isRecord } from "../../parsing.js";
1
+ import { isRecord, parseRefId } from "../../parsing.js";
2
2
  import { redactSensitiveText } from "../../runtime.js";
3
3
  import { withOptionalSessionArgs } from "../../results/next-actions.js";
4
4
  import { runSessionCommandData } from "./session-state.js";
5
5
  const CLICK_DISPATCH_MARKER_PREFIX = "__piAgentBrowserClickDispatchProbe_";
6
6
  const CLICK_DISPATCH_CLEANUP_TIMEOUT_MS = 2_000;
7
7
  const ACCESSIBLE_REF_CLICK_DISPATCH_ROLES = new Set(["button", "checkbox", "menuitem", "radio", "switch", "tab"]);
8
- function parseClickRefId(selector) {
9
- const trimmed = selector.trim();
10
- const candidate = trimmed.startsWith("@") ? trimmed.slice(1) : trimmed.startsWith("ref=") ? trimmed.slice(4) : trimmed;
11
- return /^e\d+$/.test(candidate) ? candidate : undefined;
12
- }
13
8
  function normalizeAccessibleName(name) {
14
9
  return name.replace(/\s+/g, " ").trim().toLowerCase();
15
10
  }
16
- function getAccessibleRefDuplicateIndex(refSnapshot, refId, role, name) {
17
- if (!refSnapshot?.refs)
18
- return undefined;
19
- const normalizedRole = role.toLowerCase();
20
- const normalizedName = normalizeAccessibleName(name);
21
- const matchingRefIds = refSnapshot.refIds.filter((candidateRefId) => {
22
- const candidate = refSnapshot.refs?.[candidateRefId];
23
- return candidate?.role.toLowerCase() === normalizedRole && normalizeAccessibleName(candidate.name) === normalizedName;
24
- });
25
- if (matchingRefIds.length <= 1)
26
- return undefined;
27
- const duplicateIndex = matchingRefIds.indexOf(refId);
28
- return duplicateIndex >= 0 ? duplicateIndex : undefined;
29
- }
30
11
  function getClickDispatchProbeTarget(commandTokens, refSnapshot) {
31
12
  if (commandTokens[0] !== "click" || commandTokens.includes("--new-tab"))
32
13
  return undefined;
33
14
  const selector = commandTokens[1];
34
15
  if (!selector || selector.startsWith("-"))
35
16
  return undefined;
36
- const refId = parseClickRefId(selector);
17
+ const refId = parseRefId(selector);
37
18
  if (refId) {
38
19
  const ref = refSnapshot?.refs?.[refId];
39
20
  if (!ref || !ACCESSIBLE_REF_CLICK_DISPATCH_ROLES.has(ref.role))
40
21
  return undefined;
41
- const duplicateIndex = getAccessibleRefDuplicateIndex(refSnapshot, refId, ref.role, ref.name);
42
- return { ...(duplicateIndex === undefined ? {} : { duplicateIndex }), kind: "accessible", name: ref.name, refId, role: ref.role };
22
+ const matchingRefs = Object.values(refSnapshot?.refs ?? {}).filter((candidate) => candidate.role.toLowerCase() === ref.role.toLowerCase() && normalizeAccessibleName(candidate.name) === normalizeAccessibleName(ref.name));
23
+ if (matchingRefs.length !== 1)
24
+ return undefined;
25
+ return { kind: "accessible", name: ref.name, refId, role: ref.role };
43
26
  }
44
27
  if (selector.startsWith("xpath="))
45
28
  return { kind: "xpath", selector: selector.slice("xpath=".length) };
@@ -58,7 +41,6 @@ function buildClickDispatchProbeInstallScript(probe) {
58
41
  const normalize = (value) => String(value ?? "").replace(/\\s+/g, " ").trim();
59
42
  const expectedRole = ${JSON.stringify(target.role)};
60
43
  const expectedName = normalize(${JSON.stringify(target.name)});
61
- const duplicateIndex = ${JSON.stringify(target.duplicateIndex)};
62
44
  const inferRole = (element) => {
63
45
  const explicit = element.getAttribute("role");
64
46
  if (explicit) return explicit;
@@ -81,7 +63,6 @@ function buildClickDispatchProbeInstallScript(probe) {
81
63
  return element.getClientRects().length > 0;
82
64
  };
83
65
  const candidates = Array.from(document.querySelectorAll("button,a[href],input,select,textarea,summary,[role],[onclick],[tabindex]")).filter((element) => inferRole(element) === expectedRole && inferName(element) === expectedName && isVisible(element));
84
- if (typeof duplicateIndex === "number") return candidates[duplicateIndex] || null;
85
66
  return candidates.length === 1 ? candidates[0] : null;
86
67
  })()`;
87
68
  return `(() => {
@@ -4,7 +4,7 @@ import { isCloseCommand, isOpenNavigationCommand } from "../../command-taxonomy.
4
4
  import { boundElectronProbeString } from "../../electron/cdp.js";
5
5
  import { executableExistsOnPath } from "../../executable-path.js";
6
6
  import { formatSessionArtifactRetentionSummary } from "../../results/artifact-manifest.js";
7
- import { buildNextToolAction, withOptionalSessionArgs } from "../../results/next-actions.js";
7
+ import { buildInspectOverlayStateAction, buildNextToolAction, withOptionalSessionArgs } from "../../results/next-actions.js";
8
8
  import { buildVisibleRefFallbackDiagnosticFromSnapshot, getVisibleRefFallbackTarget } from "../../results/selector-recovery.js";
9
9
  import { extractRefSnapshotFromData, isAboutBlankUrl, normalizeComparableUrl } from "../../session-page-state.js";
10
10
  import { extractUpstreamCommandTokens, parseWaitCommandTokens, redactInvocationArgs, redactSensitiveText } from "../../runtime.js";
@@ -266,7 +266,7 @@ export function formatOverlayBlockerText(diagnostic) {
266
266
  return ["Possible overlay blockers:", ...diagnostic.candidates.map((candidate) => `- ${candidate.ref}${candidate.role ? ` ${candidate.role}` : ""}${candidate.name ? ` ${JSON.stringify(candidate.name)}` : ""}: ${candidate.reason}`)].join("\n");
267
267
  }
268
268
  export function buildOverlayBlockerNextActions(options) {
269
- return [{ id: "inspect-overlay-state", params: { args: withOptionalSessionArgs(options.sessionName, ["snapshot", "-i"]) }, reason: "Refresh interactive refs and inspect whether an overlay, banner, modal, or dialog is blocking the intended click.", safety: "Read-only inspection; use current refs from this snapshot before interacting.", tool: "agent_browser" }, ...options.diagnostic.candidates.map((candidate, index) => ({ id: `try-overlay-blocker-candidate-${index + 1}`, params: { args: withOptionalSessionArgs(options.sessionName, candidate.args) }, reason: candidate.reason, safety: "Only click this if the candidate is clearly a close/dismiss control for an overlay that blocks the intended workflow.", tool: "agent_browser" }))];
269
+ return [buildInspectOverlayStateAction(options.sessionName), ...options.diagnostic.candidates.map((candidate, index) => ({ id: `try-overlay-blocker-candidate-${index + 1}`, params: { args: withOptionalSessionArgs(options.sessionName, candidate.args) }, reason: candidate.reason, safety: "Only click this if the candidate is clearly a close/dismiss control for an overlay that blocks the intended workflow.", tool: "agent_browser" }))];
270
270
  }
271
271
  export function collectSnapshotOverlayBlockerDiagnostic(data) {
272
272
  const candidates = getOverlayBlockerCandidates(data);
@@ -54,10 +54,9 @@ export function buildSemanticActionCandidateActions(compiled) {
54
54
  return [];
55
55
  }
56
56
  export function buildWrapperRecoveryHint(options) {
57
- const wrapperManagedContexts = [options.sessionTabCorrection ? "session tab correction" : undefined, options.pinnedBatchUnwrapMode ? "pinned batch routing" : undefined].filter((item) => item !== undefined);
58
- if (wrapperManagedContexts.length === 0)
57
+ if (!options.sessionTabCorrection)
59
58
  return undefined;
60
- return `Wrapper recovery hint: this call used ${wrapperManagedContexts.join(" and ")}. Inspect details.effectiveArgs and details.sessionTabCorrection; if the selected tab looks wrong, run tab list for the same session before retrying.`;
59
+ return "Wrapper recovery hint: this call used session tab correction. Inspect details.effectiveArgs and details.sessionTabCorrection; if the selected tab looks wrong, run tab list for the same session before retrying.";
61
60
  }
62
61
  export function redactExactSensitiveText(text, sensitiveValues) {
63
62
  let redacted = text;
@@ -1,14 +1,22 @@
1
1
  import { runAgentBrowserProcess, withAttachedBrowserSessionContext } from "../../process.js";
2
+ import { isRecord } from "../../parsing.js";
2
3
  import { withOwnedManagedSessionContext } from "../../managed-session-restore.js";
3
4
  import { cleanupClickDispatchProbe } from "./click-dispatch.js";
4
- import { applyBrowserRunStatePatch } from "./session-state.js";
5
+ import { applyBrowserRunStatePatch, getSessionContextKey } from "./session-state.js";
5
6
  import { buildMissingBinaryFailureResult } from "./final-result.js";
6
7
  import { prepareBrowserRun } from "./prepare.js";
7
8
  import { processBrowserOutput } from "./process-output.js";
8
9
  export { closeManagedSession } from "./managed-session-daemon-policy.js";
9
10
  export { getSessionContextKey } from "./session-state.js";
10
11
  export async function runAgentBrowserTool(options) {
11
- return await withAttachedBrowserSessionContext(options.preserveAttachedBrowserSession === true, () => runAgentBrowserToolInContext(options));
12
+ const result = await withAttachedBrowserSessionContext(options.preserveAttachedBrowserSession === true, () => runAgentBrowserToolInContext(options));
13
+ const details = isRecord(result.details) ? result.details : undefined;
14
+ const sessionKey = getSessionContextKey(typeof details?.sessionName === "string" ? details.sessionName : undefined, typeof details?.namespace === "string" ? details.namespace : undefined);
15
+ const page = options.state.sessionPageState.get(sessionKey);
16
+ return page.tabReopenPending === undefined ? result : {
17
+ ...result,
18
+ details: { ...details, sessionTabReopenPending: page.tabReopenPending, ...(page.refSnapshotInvalidation ? { refSnapshotInvalidation: page.refSnapshotInvalidation } : {}) },
19
+ };
12
20
  }
13
21
  async function runAgentBrowserToolInContext(options) {
14
22
  const preparedResult = await prepareBrowserRun(options);
@@ -34,7 +42,6 @@ async function runAgentBrowserToolInContext(options) {
34
42
  signal: options.signal,
35
43
  stdin: prepared.processStdin,
36
44
  timeoutMs: prepared.processTimeoutMs,
37
- trustedFirstBatchTabSelection: prepared.pinnedBatchUnwrapMode !== undefined,
38
45
  });
39
46
  const missingBinaryResult = await buildMissingBinaryFailureResult({
40
47
  compatibilityWorkaround: prepared.compatibilityWorkaround,
@@ -100,6 +100,9 @@ export async function acquireOwnedManagedSessionDaemonPolicy(options) {
100
100
  context.restoreState.recordDaemonRestoreKey(context.sessionName, context.namespace, daemon.restoreKey);
101
101
  return !["inactive", "missing-binary"].includes(daemon.status) && !activePolicyMatches
102
102
  ? {
103
+ cleanupOnlyReason: daemon.status === "active" && restoreDisabledPolicyNeedsProvenance && !hasKnownDaemonRestoreKey
104
+ ? "restore-disabled-daemon-without-provenance"
105
+ : undefined,
103
106
  daemonStatus: daemon.status,
104
107
  error: [
105
108
  "This wrapper-owned session's live daemon does not match the requested managed-restore policy.",
@@ -25,7 +25,7 @@ function findCommandTimeoutMs(commandTokens) {
25
25
  const firstWaitArgument = commandTokens[0] === "wait" ? commandTokens[1] : undefined;
26
26
  return firstWaitArgument && !firstWaitArgument.startsWith("-") ? parseMillisecondsToken(firstWaitArgument) : undefined;
27
27
  }
28
- function findFirstPositionalArgument(commandTokens) {
28
+ export function findFirstPositionalArgument(commandTokens) {
29
29
  for (let index = 1; index < commandTokens.length; index += 1) {
30
30
  const token = commandTokens[index];
31
31
  const flag = token.split("=", 1)[0];
@@ -16,13 +16,13 @@ import { buildAgentBrowserResultCategoryDetails } from "../../results/categories
16
16
  import { applyNamespaceToNextActions } from "../../results/next-actions.js";
17
17
  import { buildSessionAwareStaleRefNextActions, buildSessionTabRecoveryNextActions } from "../../results/recovery-next-actions.js";
18
18
  import { resolveVisibleRefActionFromSnapshot } from "../../results/selector-recovery.js";
19
- import { extractRefSnapshotFromData } from "../../session-page-state.js";
20
- import { buildExecutionPlan, canUseHeadlessCompatibilityUserAgent, createFreshSessionName, extractCommandTokens, extractUpstreamCommandTokens, getDefaultHeadlessCompatUserAgent, parseWaitCommandTokens, redactInvocationArgs, } from "../../runtime.js";
19
+ import { buildPageTransitionRefSnapshotInvalidation, extractRefSnapshotFromData, normalizeComparableUrl } from "../../session-page-state.js";
20
+ import { buildExecutionPlan, canUseHeadlessCompatibilityUserAgent, createFreshSessionName, extractCommandTokens, extractUpstreamCommandTokens, getDefaultHeadlessCompatUserAgent, parseWaitCommandTokens, redactInvocationArgs, redactSensitiveText, } from "../../runtime.js";
21
21
  import { buildOwnedManagedSessionRestoreContext, resolveExplicitAutosaveInterval, withOwnedManagedSessionContext, } from "../../managed-session-restore.js";
22
22
  import { getAgentBrowserProcessEnvironment } from "../../process-environment.js";
23
23
  import { getExplicitSessionPageVerificationRequirement, getPageTargetValidationError, } from "../../page-target-validation.js";
24
24
  import { acquireOwnedManagedSessionDaemonPolicy, getRunningHeadedAutosavePolicyChangeError } from "./managed-session-daemon-policy.js";
25
- import { applyOpenResultTabCorrection, buildManagedSessionOutcome, buildPinnedBatchPlan, buildSessionDetailFields, buildStaleRefPreflight, getSessionContextKey, extractStringResultField, collectAnySessionTabSelection, collectSessionTabSelection, getGuardedRefUsage, getTraceOwnerGuardMessage, runSessionCommandData, shouldPinSessionTabForCommand, } from "./session-state.js";
25
+ import { buildManagedSessionOutcome, buildSessionDetailFields, buildStaleRefPreflight, getSessionContextKey, extractStringResultField, ensureSessionTabTarget, getGuardedRefUsage, getTraceOwnerGuardMessage, runSessionCommandData, shouldPinSessionTabForCommand, } from "./session-state.js";
26
26
  import { getUpstreamEffectiveBatchSteps, parseBatchStdinJsonArray } from "../batch-stdin.js";
27
27
  import { buildElectronHostFailureResult, formatAgentBrowserNextActionsText, getElectronLaunchFailureCategory, redactRecoveryHint } from "./final-result.js";
28
28
  import { prepareClickDispatchProbe } from "./click-dispatch.js";
@@ -241,8 +241,8 @@ function getSamePageFreshnessPreflightFailure(options) {
241
241
  const { refIds } = options;
242
242
  if (refIds.length === 0)
243
243
  return undefined;
244
- const previousUrl = options.previousSnapshot.target?.url;
245
- const currentUrl = options.currentSnapshot.target?.url;
244
+ const previousUrl = normalizeComparableUrl(options.previousSnapshot.target?.url);
245
+ const currentUrl = normalizeComparableUrl(options.currentSnapshot.target?.url);
246
246
  if (!previousUrl || !currentUrl || previousUrl !== currentUrl || currentUrl === "about:blank")
247
247
  return undefined;
248
248
  const mismatchedRefs = refIds.filter((refId) => {
@@ -267,8 +267,8 @@ async function collectSamePageRefFreshnessPreflight(options) {
267
267
  const refIds = [...new Set(getGuardedRefUsage(options.commandTokens, options.stdin))];
268
268
  if (!options.previousSnapshot || !options.sessionName || refIds.length === 0)
269
269
  return undefined;
270
- const previousUrl = options.previousSnapshot.target?.url;
271
- const currentTargetUrl = options.currentTarget?.url;
270
+ const previousUrl = normalizeComparableUrl(options.previousSnapshot.target?.url);
271
+ const currentTargetUrl = normalizeComparableUrl(options.currentTarget?.url);
272
272
  if (currentTargetUrl === "about:blank" || (previousUrl && currentTargetUrl && previousUrl !== currentTargetUrl))
273
273
  return undefined;
274
274
  const snapshotData = await runSessionCommandData({ args: ["snapshot", "-i"], cwd: options.cwd, namespace: options.namespace, sessionName: options.sessionName, signal: options.signal });
@@ -401,7 +401,29 @@ export async function prepareBrowserRun(options) {
401
401
  let managedSessionPolicyLockTransferred = false;
402
402
  let electronLaunchTransferred = false;
403
403
  try {
404
- const preparedArgs = await prepareAgentBrowserArgs(runtimeToolArgs, runtimeToolStdin, cwd);
404
+ let preparedArgs;
405
+ try {
406
+ preparedArgs = await prepareAgentBrowserArgs(runtimeToolArgs, runtimeToolStdin, cwd);
407
+ }
408
+ catch (error) {
409
+ signal?.throwIfAborted();
410
+ if (!(error instanceof Error) || !("syscall" in error) || error.syscall !== "mkdir" || !("path" in error) || typeof error.path !== "string")
411
+ throw error;
412
+ const guidance = "Choose a writable artifact path whose parent components are directories. Use absolute paths in raw batch artifact rows.";
413
+ const validationError = redactSensitiveText(`Could not prepare artifact directory ${error.path}: ${error.message}. ${guidance}`);
414
+ const nextActions = [{ artifactPath: redactSensitiveText(error.path), id: "verify-artifact-path", reason: guidance, safety: "The requested browser command did not run; inspect the directory with host file tools before retrying.", tool: "agent_browser" }];
415
+ return { kind: "early-result", result: {
416
+ content: [{ type: "text", text: validationError }],
417
+ details: {
418
+ agentBrowserStarted: false,
419
+ args: redactedArgs,
420
+ nextActions,
421
+ ...buildAgentBrowserResultCategoryDetails({ args: redactedArgs, succeeded: false, validationError }),
422
+ validationError,
423
+ },
424
+ isError: true,
425
+ } };
426
+ }
405
427
  const userRequestedJson = runtimeToolArgs.includes("--json");
406
428
  let executionPlan = buildExecutionPlan(preparedArgs.args, {
407
429
  freshSessionName,
@@ -480,6 +502,8 @@ export async function prepareBrowserRun(options) {
480
502
  compatibilityUserAgent: compatibilityUserAgentApplied ? compatibilityUserAgent : undefined,
481
503
  wrapperInjectedUserAgent: compatibilityUserAgentApplied,
482
504
  });
505
+ let managedSessionDaemonInactive = false;
506
+ let managedSessionCleanupOnlyReason;
483
507
  if (!executionPlan.validationError && ownedManagedSession) {
484
508
  const closeCommand = isCloseCommand(executionPlan.commandInfo.command);
485
509
  const policy = await acquireOwnedManagedSessionDaemonPolicy({
@@ -488,7 +512,9 @@ export async function prepareBrowserRun(options) {
488
512
  signal,
489
513
  });
490
514
  managedSessionPolicyLock = policy.lock;
515
+ managedSessionDaemonInactive = policy.daemonStatus === "inactive";
491
516
  if (policy.error) {
517
+ managedSessionCleanupOnlyReason = policy.cleanupOnlyReason;
492
518
  executionPlan = {
493
519
  ...executionPlan,
494
520
  recoveryHint: undefined,
@@ -524,10 +550,68 @@ export async function prepareBrowserRun(options) {
524
550
  let priorSessionTabTarget = priorSessionPageState.tabTarget;
525
551
  let priorSessionTabTargetUnknown = priorSessionPageState.tabTargetUnknown;
526
552
  const sessionTabPinningReason = priorSessionPageState.pinningReason;
527
- const priorRefSnapshotState = priorSessionPageState.refSnapshot;
528
- const priorRefSnapshotInvalidation = priorSessionPageState.refSnapshotInvalidation;
553
+ let priorRefSnapshotState = priorSessionPageState.refSnapshot;
554
+ let priorRefSnapshotInvalidation = priorSessionPageState.refSnapshotInvalidation;
555
+ const coldManagedSession = (managedSessionDaemonInactive || priorSessionPageState.tabReopenPending === true)
556
+ && recordedOwnedSession !== undefined
557
+ && sessionTabPinningReason === "restore"
558
+ && ownedManagedSession?.restoreDecision === "enabled"
559
+ && !managedSessionRestoreDisabled()
560
+ && !options.preserveAttachedBrowserSession;
561
+ if (coldManagedSession && sessionStateKey) {
562
+ sessionPageState.setTabReopenPending({ pending: true, sessionName: sessionStateKey, update: options.sessionPageStateUpdate });
563
+ priorRefSnapshotState = undefined;
564
+ priorRefSnapshotInvalidation = buildPageTransitionRefSnapshotInvalidation("The managed browser shut down. Reopening its URL reloads the page; run snapshot -i before using page-scoped refs.");
565
+ sessionPageState.applyRefSnapshotInvalidation({ invalidation: priorRefSnapshotInvalidation, sessionName: sessionStateKey, update: options.sessionPageStateUpdate });
566
+ }
529
567
  let semanticActionVisibleRefResolution;
530
568
  let livePageVerified = false;
569
+ let sessionTabCorrection;
570
+ let sessionTabSelectionError;
571
+ const plannedCommandTokens = extractUpstreamCommandTokens(preparedArgs.args);
572
+ const knownStaleRef = buildStaleRefPreflight({ commandTokens: plannedCommandTokens, currentTarget: priorSessionTabTarget, refSnapshot: priorRefSnapshotState, refSnapshotInvalidation: priorRefSnapshotInvalidation, stdin: runtimeToolStdin });
573
+ const invalidStdin = validateStdinCommandContract({ command: executionPlan.commandInfo.command, commandTokens: plannedCommandTokens, stdin: runtimeToolStdin });
574
+ const pinSessionTab = shouldPinSessionTabForCommand({
575
+ command: executionPlan.commandInfo.command,
576
+ commandTokens: plannedCommandTokens,
577
+ // URL QA clears diagnostics before its explicit open; those clears do not need the old tab.
578
+ pinningRequired: sessionTabPinningReason !== undefined && compiledQaPreset?.checks.url === undefined,
579
+ reopenPending: coldManagedSession,
580
+ sessionName: executionPlan.sessionName,
581
+ stdin: runtimeToolStdin,
582
+ });
583
+ if (!executionPlan.validationError && !executionPlan.plainTextInspection && !knownStaleRef && !invalidStdin && priorSessionTabTarget && pinSessionTab) {
584
+ signal?.throwIfAborted();
585
+ const reopened = !coldManagedSession || await runSessionCommandData({
586
+ args: ["open", priorSessionTabTarget.url], cwd, namespace: executionPlan.namespace, sessionName: executionPlan.sessionName, signal, timeoutMs: params.timeoutMs,
587
+ onProcessResult: ({ agentBrowserStarted }) => {
588
+ // A started open may have navigated even if its CLI was aborted before replying.
589
+ if (agentBrowserStarted && sessionStateKey)
590
+ sessionPageState.setTabReopenPending({ pending: false, sessionName: sessionStateKey, update: options.sessionPageStateUpdate });
591
+ },
592
+ }) !== undefined;
593
+ const selection = signal?.aborted ? undefined : reopened
594
+ ? await ensureSessionTabTarget({ cwd, namespace: executionPlan.namespace, sessionName: executionPlan.sessionName, signal, target: priorSessionTabTarget })
595
+ : { error: "agent-browser could not reopen the remembered URL after the managed browser shut down. Navigate explicitly, then run snapshot -i before retrying." };
596
+ if (coldManagedSession && signal?.aborted) {
597
+ const errorText = "agent_browser was aborted while reopening the remembered page. The requested command did not run.";
598
+ return { kind: "early-result", result: {
599
+ content: [{ type: "text", text: errorText }],
600
+ details: {
601
+ aborted: true, args: redactedArgs, command: executionPlan.commandInfo.command,
602
+ effectiveArgs: redactInvocationArgs(executionPlan.effectiveArgs), sessionMode,
603
+ ...buildSessionDetailFields(executionPlan.sessionName, executionPlan.usedImplicitSession, executionPlan.namespace, managedSessionRestoreDisabled()),
604
+ ...buildAgentBrowserResultCategoryDetails({ args: redactedArgs, command: executionPlan.commandInfo.command, errorText, failureCategory: "aborted", succeeded: false }),
605
+ },
606
+ isError: true,
607
+ } };
608
+ }
609
+ signal?.throwIfAborted();
610
+ sessionTabCorrection = selection?.correction;
611
+ sessionTabSelectionError = selection?.error;
612
+ if (selection?.error)
613
+ executionPlan = { ...executionPlan, recoveryHint: undefined, validationError: selection.error };
614
+ }
531
615
  const isCallerOwnedExplicitSession = () => executionPlan.sessionName !== undefined
532
616
  && executionPlan.usedImplicitSession === false
533
617
  && ownedManagedSession === undefined;
@@ -655,7 +739,7 @@ export async function prepareBrowserRun(options) {
655
739
  freshSessionOrdinal += 1;
656
740
  }
657
741
  if (executionPlan.validationError) {
658
- const nextActions = applyNamespaceToNextActions(unsupportedScrollIntoViewRecovery?.nextActions, executionPlan.namespace);
742
+ const nextActions = applyNamespaceToNextActions(sessionTabSelectionError ? buildSessionTabRecoveryNextActions({ kind: "tab-drift", resultCategory: "failure", sessionName: executionPlan.sessionName, tabCorrection: sessionTabCorrection, target: priorSessionTabTarget }) : unsupportedScrollIntoViewRecovery?.nextActions, executionPlan.namespace);
659
743
  const nextActionsText = formatAgentBrowserNextActionsText(nextActions);
660
744
  return { kind: "early-result", statePatch, result: {
661
745
  content: [{ type: "text", text: [executionPlan.validationError, nextActionsText].filter((text) => text !== undefined).join("\n\n") }],
@@ -667,11 +751,16 @@ export async function prepareBrowserRun(options) {
667
751
  compiledSourceLookup: redactedCompiledSourceLookup,
668
752
  compiledNetworkSourceLookup: redactedCompiledNetworkSourceLookup,
669
753
  invalidValueFlag: executionPlan.invalidValueFlag,
754
+ managedSessionCleanupOnlyReason,
755
+ ...buildSessionDetailFields(executionPlan.sessionName, executionPlan.usedImplicitSession, executionPlan.namespace, managedSessionRestoreDisabled()),
756
+ ...(managedSessionCleanupOnlyReason ? { namespace: ownedManagedSession?.namespace ?? "" } : {}),
670
757
  nextActions,
671
758
  sessionMode,
672
759
  sessionRecoveryHint: redactedRecoveryHint,
673
760
  startupScopedFlags: executionPlan.startupScopedFlags,
674
- ...buildAgentBrowserResultCategoryDetails({ args: redactedArgs, command: executionPlan.commandInfo.command, errorText: executionPlan.validationError, succeeded: false, validationError: executionPlan.validationError }),
761
+ ...(sessionTabSelectionError ? { effectiveArgs: redactedEffectiveArgs, sessionTabCorrection } : {}),
762
+ ...(coldManagedSession ? { refSnapshotInvalidation: priorRefSnapshotInvalidation } : {}),
763
+ ...buildAgentBrowserResultCategoryDetails({ args: redactedArgs, command: executionPlan.commandInfo.command, errorText: executionPlan.validationError, failureCategory: sessionTabSelectionError ? "tab-drift" : undefined, succeeded: false, validationError: executionPlan.validationError }),
675
764
  validationError: executionPlan.validationError,
676
765
  },
677
766
  isError: true,
@@ -911,109 +1000,9 @@ export async function prepareBrowserRun(options) {
911
1000
  });
912
1001
  if (directAnchorDownload)
913
1002
  return { kind: "early-result", statePatch: { ...statePatch, artifactManifest: directAnchorDownload.artifactManifest ?? statePatch.artifactManifest }, result: directAnchorDownload.result };
914
- let pinnedBatchUnwrapMode;
915
- let includePinnedNavigationSummary = false;
916
- let sessionTabCorrection;
917
- let processArgs = executionPlan.effectiveArgs;
918
- let processStdin = preparedArgs.stdin ?? runtimeToolStdin;
919
- if (priorSessionTabTarget &&
920
- shouldPinSessionTabForCommand({
921
- command: executionPlan.commandInfo.command,
922
- commandTokens,
923
- pinningRequired: sessionTabPinningReason !== undefined,
924
- sessionName: executionPlan.sessionName,
925
- stdin: runtimeToolStdin,
926
- })) {
927
- const collectTabSelection = promptRefSnapshot && getGuardedRefUsage(commandTokens, runtimeToolStdin).length > 0 ? collectAnySessionTabSelection : collectSessionTabSelection;
928
- const plannedSessionTabSelection = await collectTabSelection({
929
- cwd,
930
- namespace: executionPlan.namespace,
931
- sessionName: executionPlan.sessionName,
932
- signal,
933
- target: priorSessionTabTarget,
934
- });
935
- if (plannedSessionTabSelection && executionPlan.sessionName) {
936
- if (executionPlan.commandInfo.command === "eval" && runtimeToolStdin !== undefined) {
937
- const appliedSessionTabSelection = await applyOpenResultTabCorrection({
938
- correction: plannedSessionTabSelection,
939
- cwd,
940
- namespace: executionPlan.namespace,
941
- sessionName: executionPlan.sessionName,
942
- signal,
943
- });
944
- if (!appliedSessionTabSelection) {
945
- const error = "agent-browser could not re-select the intended tab before running the command.";
946
- return { kind: "early-result", statePatch, result: {
947
- content: [{ type: "text", text: error }],
948
- details: {
949
- args: redactedArgs,
950
- command: executionPlan.commandInfo.command,
951
- compatibilityWorkaround,
952
- effectiveArgs: redactedEffectiveArgs,
953
- sessionMode,
954
- sessionTabCorrection: plannedSessionTabSelection,
955
- ...buildAgentBrowserResultCategoryDetails({ args: redactedEffectiveArgs, command: executionPlan.commandInfo.command, errorText: error, failureCategory: "tab-drift", succeeded: false, tabDrift: true, validationError: error }),
956
- nextActions: applyNamespaceToNextActions(buildSessionTabRecoveryNextActions({
957
- kind: "tab-drift",
958
- resultCategory: "failure",
959
- sessionName: executionPlan.sessionName,
960
- tabCorrection: plannedSessionTabSelection,
961
- target: priorSessionTabTarget,
962
- }), executionPlan.namespace),
963
- validationError: error,
964
- ...buildSessionDetailFields(executionPlan.sessionName, executionPlan.usedImplicitSession, executionPlan.namespace, managedSessionRestoreDisabled()),
965
- },
966
- isError: true,
967
- } };
968
- }
969
- sessionTabCorrection = appliedSessionTabSelection;
970
- }
971
- else {
972
- const pinnedBatchPlan = buildPinnedBatchPlan({
973
- command: executionPlan.commandInfo.command,
974
- commandTokens,
975
- selectedTab: plannedSessionTabSelection.selectedTab,
976
- stdin: runtimeToolStdin,
977
- });
978
- if (pinnedBatchPlan && "error" in pinnedBatchPlan) {
979
- return { kind: "early-result", statePatch, result: {
980
- content: [{ type: "text", text: pinnedBatchPlan.error }],
981
- details: {
982
- args: redactedArgs,
983
- command: executionPlan.commandInfo.command,
984
- compatibilityWorkaround,
985
- effectiveArgs: redactedEffectiveArgs,
986
- sessionMode,
987
- sessionTabCorrection: plannedSessionTabSelection,
988
- ...buildAgentBrowserResultCategoryDetails({ args: redactedEffectiveArgs, command: executionPlan.commandInfo.command, errorText: pinnedBatchPlan.error, failureCategory: "tab-drift", succeeded: false, tabDrift: true, validationError: pinnedBatchPlan.error }),
989
- nextActions: applyNamespaceToNextActions(buildSessionTabRecoveryNextActions({
990
- kind: "tab-drift",
991
- resultCategory: "failure",
992
- sessionName: executionPlan.sessionName,
993
- tabCorrection: plannedSessionTabSelection,
994
- target: priorSessionTabTarget,
995
- }), executionPlan.namespace),
996
- validationError: pinnedBatchPlan.error,
997
- ...buildSessionDetailFields(executionPlan.sessionName, executionPlan.usedImplicitSession, executionPlan.namespace, managedSessionRestoreDisabled()),
998
- },
999
- isError: true,
1000
- } };
1001
- }
1002
- if (pinnedBatchPlan) {
1003
- sessionTabCorrection = plannedSessionTabSelection;
1004
- // The rewritten batch must keep the caller's fail-fast semantics:
1005
- // upstream reads only the exact --bail token, so re-emit it whenever
1006
- // the original batch tokens carried it (argv or stdin/job/qa mode).
1007
- const pinnedBailArgs = commandTokens.includes("--bail") ? ["--bail"] : [];
1008
- processArgs = ["--json", ...(executionPlan.namespace !== undefined ? ["--namespace", executionPlan.namespace] : []), "--session", executionPlan.sessionName, "batch", ...pinnedBailArgs];
1009
- processStdin = JSON.stringify(pinnedBatchPlan.steps);
1010
- includePinnedNavigationSummary = pinnedBatchPlan.includeNavigationSummary;
1011
- pinnedBatchUnwrapMode = pinnedBatchPlan.unwrapMode;
1012
- }
1013
- }
1014
- }
1015
- }
1016
- const clickDispatchProbe = pinnedBatchUnwrapMode === undefined && compiledElectron === undefined
1003
+ const processArgs = executionPlan.effectiveArgs;
1004
+ const processStdin = preparedArgs.stdin ?? runtimeToolStdin;
1005
+ const clickDispatchProbe = compiledElectron === undefined
1017
1006
  ? await prepareClickDispatchProbe({ commandTokens, cwd, namespace: executionPlan.namespace, refSnapshot: promptRefSnapshot, sessionName: executionPlan.sessionName, signal })
1018
1007
  : undefined;
1019
1008
  let readTimeoutPageUrl = priorSessionTabTarget?.url;
@@ -1061,9 +1050,7 @@ export async function prepareBrowserRun(options) {
1061
1050
  electronLaunch,
1062
1051
  exactSensitiveValues,
1063
1052
  executionPlan,
1064
- includePinnedNavigationSummary,
1065
1053
  ownedManagedSessionContext: ownedManagedSession,
1066
- pinnedBatchUnwrapMode,
1067
1054
  preparedArgs,
1068
1055
  priorRefSnapshotState,
1069
1056
  priorSessionTabTarget,