pi-agent-browser-native 0.6.5 → 0.6.7

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 (37) hide show
  1. package/CHANGELOG.md +30 -1
  2. package/README.md +20 -5
  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/final-result.js +2 -3
  11. package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +10 -3
  12. package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +4 -1
  13. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/wait-timeouts.js +1 -1
  14. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +104 -117
  15. package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +61 -35
  16. package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +100 -135
  17. package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +3 -1
  18. package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +3 -1
  19. package/dist/extensions/agent-browser/lib/page-target-validation.js +10 -10
  20. package/dist/extensions/agent-browser/lib/parsing.js +7 -0
  21. package/dist/extensions/agent-browser/lib/playbook.js +5 -8
  22. package/dist/extensions/agent-browser/lib/process-identity.js +10 -2
  23. package/dist/extensions/agent-browser/lib/process.js +23 -7
  24. package/dist/extensions/agent-browser/lib/recording-reservations.js +3 -1
  25. package/dist/extensions/agent-browser/lib/results/envelope.js +4 -1
  26. package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +45 -43
  27. package/dist/extensions/agent-browser/lib/results/recovery-actions.js +4 -4
  28. package/dist/extensions/agent-browser/lib/runtime.js +18 -2
  29. package/dist/extensions/agent-browser/lib/session-page-state.js +29 -10
  30. package/docs/ARCHITECTURE.md +7 -4
  31. package/docs/COMMAND_REFERENCE.md +22 -15
  32. package/docs/ELECTRON.md +6 -6
  33. package/docs/RELEASE.md +14 -5
  34. package/docs/REQUIREMENTS.md +1 -1
  35. package/docs/SUPPORT_MATRIX.md +13 -3
  36. package/docs/TOOL_CONTRACT.md +33 -22
  37. package/package.json +1 -1
@@ -143,7 +143,6 @@ const COMMAND_CAPABILITIES = [
143
143
  command: "keyboard",
144
144
  eligibleForElectronHealthProbe: true,
145
145
  eligibleForPageChangeSummary: true,
146
- guardsPageRefs: true,
147
146
  invalidatesBatchRefs: true,
148
147
  triggersPostMutationSnapshot: true,
149
148
  },
@@ -157,7 +156,6 @@ const COMMAND_CAPABILITIES = [
157
156
  {
158
157
  command: "mouse",
159
158
  eligibleForElectronHealthProbe: true,
160
- guardsPageRefs: true,
161
159
  invalidatesBatchRefs: true,
162
160
  },
163
161
  {
@@ -181,7 +179,6 @@ const COMMAND_CAPABILITIES = [
181
179
  command: "press",
182
180
  eligibleForElectronHealthProbe: true,
183
181
  eligibleForPageChangeSummary: true,
184
- guardsPageRefs: true,
185
182
  invalidatesBatchRefs: true,
186
183
  triggersPostMutationSnapshot: true,
187
184
  },
@@ -330,8 +327,11 @@ export function isRecordPageTransitionCommand(tokens) {
330
327
  export function isWebMcpPageMutationCommand(tokens) {
331
328
  return isWebMcpPageMutation(tokens[0], tokens[1]);
332
329
  }
330
+ export function isWindowOrDiffPageTransitionCommand(command, subcommand) {
331
+ return (command === "window" && subcommand === "new") || (command === "diff" && subcommand === "url");
332
+ }
333
333
  export function isRefInvalidatingBatchCommand(step) {
334
- return hasCommandCapability(step[0], "invalidatesBatchRefs") || isRecordPageTransitionCommand(step) || isWebMcpPageMutationCommand(step);
334
+ return hasCommandCapability(step[0], "invalidatesBatchRefs") || isRecordPageTransitionCommand(step) || isWebMcpPageMutationCommand(step) || isWindowOrDiffPageTransitionCommand(step[0], step[1]);
335
335
  }
336
336
  export function isRefGuardedCommand(command) {
337
337
  return hasCommandCapability(command, "guardsPageRefs");
@@ -343,12 +343,13 @@ function isWebMcpPageMutation(command, subcommand) {
343
343
  return command === "webmcp" && WEBMCP_PAGE_MUTATION_SUBCOMMANDS.has(subcommand ?? "");
344
344
  }
345
345
  export function isNavigationObservableCommandName(command, subcommand) {
346
- return hasCommandCapability(command, "navigationObservable") || isWebMcpPageMutation(command, subcommand);
346
+ return hasCommandCapability(command, "navigationObservable") || isWebMcpPageMutation(command, subcommand) || isWindowOrDiffPageTransitionCommand(command, subcommand);
347
347
  }
348
348
  export function isUnverifiedPageTransitionCommand(command, subcommand) {
349
349
  return ["back", "connect", "eval", "forward", "reload"].includes(command ?? "")
350
350
  || (command === "state" && subcommand === "load")
351
351
  || (command === "tab" && subcommand !== undefined && !["list", "new"].includes(subcommand))
352
+ || isWindowOrDiffPageTransitionCommand(command, subcommand)
352
353
  || isWebMcpPageMutation(command, subcommand);
353
354
  }
354
355
  export function isPageMutationCommand(command, subcommand) {
@@ -1,5 +1,5 @@
1
1
  import { execFile } from "node:child_process";
2
- import { rm } from "node:fs/promises";
2
+ import { lstat, rm } from "node:fs/promises";
3
3
  import { promisify } from "node:util";
4
4
  import { fetchCdpJson, parseCdpTargets, parseCdpVersion } from "./cdp.js";
5
5
  import { ELECTRON_PROFILE_DIR_PREFIX } from "./launch.js";
@@ -33,6 +33,14 @@ async function isPortAlive(port) {
33
33
  }
34
34
  export async function inspectElectronLaunchStatus(record) {
35
35
  const cdp = await isPortAlive(record.port);
36
+ let userDataDirState;
37
+ try {
38
+ await lstat(record.userDataDir);
39
+ userDataDirState = "present";
40
+ }
41
+ catch (error) {
42
+ userDataDirState = error.code === "ENOENT" ? "absent" : "unknown";
43
+ }
36
44
  return {
37
45
  cleanupState: record.cleanupState,
38
46
  launchId: record.launchId,
@@ -41,6 +49,7 @@ export async function inspectElectronLaunchStatus(record) {
41
49
  port: record.port,
42
50
  portAlive: cdp.version !== undefined,
43
51
  targets: cdp.targets,
52
+ userDataDirState,
44
53
  version: cdp.version,
45
54
  };
46
55
  }
@@ -11,7 +11,7 @@ export function createAgentBrowserParamsSchema(Type = JsonSchema, StringEnum = l
11
11
  maxLength: AGENT_BROWSER_SCRIPT_CODE_MAX_BYTES,
12
12
  })),
13
13
  args: Type.Optional(Type.Array(Type.String(), {
14
- description: "Raw agent-browser argv only: no binary, shell operators, or --json. Start with open → snapshot -i → act on current @refs; re-snapshot after page changes.",
14
+ description: "Raw agent-browser argv only: no binary, shell operators, or --json. Start with open → snapshot -i → act on current @refs; re-snapshot after page changes. Input: type <selector> <text>, or keyboard type <text> at current focus. Wait duration: wait <ms> (no --time). Artifacts: screenshot [selector] [path] [--full/-f]; record start <path> [url]; record restart <path> [url]; record stop. Paths are positional (no --path); use --full, not --full-page.",
15
15
  minItems: 1,
16
16
  })),
17
17
  semanticAction: Type.Optional(Type.Object({
@@ -21,10 +21,23 @@ export function createAgentBrowserParamsSchema(Type = JsonSchema, StringEnum = l
21
21
  values: Type.Optional(Type.Array(Type.String(), { description: "Select options; required for select by label.", minItems: 1 })),
22
22
  selector: Type.Optional(Type.String({ description: "Direct selector or @ref." })),
23
23
  text: Type.Optional(Type.String({ description: "Fill text." })),
24
- role: Type.Optional(Type.String({ description: "Role locator; alternative to value." })),
24
+ role: Type.Optional(Type.String({ description: "Role for locator=role; select needs combobox or listbox." })),
25
25
  name: Type.Optional(Type.String({ description: "Accessible name." })),
26
26
  session: Type.Optional(Type.String({ description: "Upstream session name." })),
27
- }, { additionalProperties: false, description: "Stable locator or direct-selector action." })),
27
+ }, {
28
+ additionalProperties: false,
29
+ // Pi normalizes optional nulls through properties, not union branches.
30
+ anyOf: [
31
+ Type.Object({
32
+ action: StringEnum(["select"]),
33
+ locator: Type.Optional(StringEnum(["role", "label"])),
34
+ }, { not: { required: ["text"] } }),
35
+ Type.Object({
36
+ action: StringEnum(["check", "click", "fill"]),
37
+ }, { not: { required: ["values"] } }),
38
+ ],
39
+ description: "Stable locator or direct-selector action. values only with action=select.",
40
+ })),
28
41
  qa: Type.Optional(Type.Union([
29
42
  Type.Object({
30
43
  attached: Type.Literal(true),
@@ -112,7 +125,7 @@ export function createAgentBrowserParamsSchema(Type = JsonSchema, StringEnum = l
112
125
  locator: Type.Optional(StringEnum(AGENT_BROWSER_SEMANTIC_LOCATORS, { description: "Locator when selector is omitted." })),
113
126
  role: Type.Optional(Type.String({ description: "Role locator." })),
114
127
  name: Type.Optional(Type.String({ description: "Accessible name." })),
115
- text: Type.Optional(Type.String({ description: "Fill text or assertText target." })),
128
+ text: Type.Optional(Type.String({ description: "Fill/type text; assertText uses only text, not selector/locator fields." })),
116
129
  value: Type.Optional(Type.String({ description: "Select option or locator value." })),
117
130
  values: Type.Optional(Type.Array(Type.String(), { description: "Select options.", minItems: 1 })),
118
131
  path: Type.Optional(Type.String({ description: "Download or screenshot path." })),
@@ -120,10 +133,10 @@ export function createAgentBrowserParamsSchema(Type = JsonSchema, StringEnum = l
120
133
  press: Type.Optional(Type.String({ description: "Key to press after typing." })),
121
134
  milliseconds: Type.Optional(Type.Number({ description: "Wait duration in milliseconds." })),
122
135
  }, { additionalProperties: false }), { minItems: 1 }),
123
- }, { additionalProperties: false, description: "Constrained multi-step batch." })),
124
- stdin: Type.Optional(Type.String({ description: "Raw stdin for batch, eval --stdin, or auth save --password-stdin; unavailable with structured modes and electron." })),
136
+ }, { additionalProperties: false, description: "Constrained multi-step batch. Clicks can stale later refs; split and re-snapshot before using them." })),
137
+ stdin: Type.Optional(Type.String({ description: "For batch, a JSON array of token arrays, e.g. [[\"get\",\"title\"]]. Raw text only for eval --stdin or auth save --password-stdin; unavailable with structured modes and electron." })),
125
138
  outputPath: Type.Optional(Type.String({ description: "Workspace-relative or absolute result-data path; keep it distinct from screenshot, download, recording, and other browser artifact destinations.", minLength: 1 })),
126
- timeoutMs: Type.Optional(Type.Integer({ description: "Wrapper timeout in ms; exceed explicit waits. Electron uses electron.timeoutMs.", minimum: 1 })),
139
+ timeoutMs: Type.Optional(Type.Integer({ description: "Wrapper timeout in ms; exceed explicit waits. electron.list has no configurable timeout; other Electron actions use electron.timeoutMs.", minimum: 1 })),
127
140
  sessionMode: Type.Optional(StringEnum(["auto", "fresh"], {
128
141
  description: "auto reuses the managed session; fresh starts one for launch-only flags, then makes it the managed session.",
129
142
  default: DEFAULT_SESSION_MODE,
@@ -150,7 +150,6 @@ export const MANAGED_RESTORE_INCOMPATIBLE_ENVS = [
150
150
  "AGENT_BROWSER_PROFILE",
151
151
  "AGENT_BROWSER_STATE",
152
152
  "AGENT_BROWSER_CDP",
153
- "AGENT_BROWSER_NAMESPACE",
154
153
  "AGENT_BROWSER_SESSION_NAME",
155
154
  "AGENT_BROWSER_PROVIDER",
156
155
  "AGENT_BROWSER_EXECUTABLE_PATH",
@@ -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 `(() => {
@@ -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,
@@ -62,7 +62,7 @@ export async function acquireOwnedManagedSessionDaemonPolicy(options) {
62
62
  return signal?.aborted
63
63
  ? {}
64
64
  : {
65
- error: "Managed-session policy coordination is unavailable or busy. Retry after the current operation finishes, repair the private policy-lock directory, and on POSIX verify that /bin/ps or /usr/bin/ps is available.",
65
+ error: "Managed-session policy coordination is unavailable or busy. Retry after the current operation finishes, repair the private policy-lock directory, and on POSIX verify that /bin/ps, /usr/bin/ps, or ps through PATH is available.",
66
66
  };
67
67
  }
68
68
  try {
@@ -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];