pi-agent-browser-native 0.2.25 → 0.2.27

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.
@@ -30,10 +30,12 @@ import {
30
30
  buildAgentBrowserNextActions,
31
31
  buildAgentBrowserResultCategoryDetails,
32
32
  buildToolPresentation,
33
+ compareRefIds,
33
34
  getAgentBrowserErrorText,
34
35
  parseAgentBrowserEnvelope,
35
36
  type AgentBrowserBatchResult,
36
37
  type AgentBrowserEnvelope,
38
+ type AgentBrowserNextAction,
37
39
  } from "./lib/results.js";
38
40
  import {
39
41
  buildExecutionPlan,
@@ -56,6 +58,7 @@ import {
56
58
  resolveManagedSessionState,
57
59
  shouldAppendBrowserSystemPrompt,
58
60
  validateToolArgs,
61
+ type CommandInfo,
59
62
  type CompatibilityWorkaround,
60
63
  type OpenResultTabCorrection,
61
64
  } from "./lib/runtime.js";
@@ -72,6 +75,7 @@ import {
72
75
  formatSessionArtifactRetentionSummary,
73
76
  isSessionArtifactManifest,
74
77
  mergeSessionArtifactManifest,
78
+ summarizeNetworkFailures,
75
79
  } from "./lib/results/shared.js";
76
80
 
77
81
  const DEFAULT_SESSION_MODE = "auto" as const;
@@ -81,6 +85,7 @@ const PACKAGE_NAME = "pi-agent-browser-native";
81
85
  const AGENT_BROWSER_SEMANTIC_ACTIONS = ["check", "click", "fill", "select", "uncheck"] as const;
82
86
  const AGENT_BROWSER_SEMANTIC_LOCATORS = ["alt", "label", "placeholder", "role", "testid", "text", "title"] as const;
83
87
  const AGENT_BROWSER_JOB_STEP_ACTIONS = ["open", "click", "fill", "wait", "assertText", "assertUrl", "waitForDownload", "screenshot"] as const;
88
+ const AGENT_BROWSER_QA_LOAD_STATES = ["domcontentloaded", "load", "networkidle"] as const;
84
89
  const SOURCE_LOOKUP_WORKSPACE_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx"]);
85
90
  const SOURCE_LOOKUP_IGNORED_DIRECTORIES = new Set([".git", "node_modules", "dist", "build", "coverage", ".next", "out", "tmp", "temp"]);
86
91
  const SOURCE_LOOKUP_DEFAULT_MAX_WORKSPACE_FILES = 2_000;
@@ -89,6 +94,7 @@ const SOURCE_LOOKUP_MAX_WORKSPACE_FILES = 5_000;
89
94
  type AgentBrowserSemanticActionName = (typeof AGENT_BROWSER_SEMANTIC_ACTIONS)[number];
90
95
  type AgentBrowserSemanticLocator = (typeof AGENT_BROWSER_SEMANTIC_LOCATORS)[number];
91
96
  type AgentBrowserJobStepAction = (typeof AGENT_BROWSER_JOB_STEP_ACTIONS)[number];
97
+ type AgentBrowserQaLoadState = (typeof AGENT_BROWSER_QA_LOAD_STATES)[number];
92
98
  type AgentBrowserSourceLookupStatus = "candidates-found" | "no-candidates" | "unsupported";
93
99
  type AgentBrowserNetworkSourceLookupStatus = "failed-requests-found" | "no-failed-requests" | "no-candidates";
94
100
 
@@ -99,6 +105,7 @@ interface AgentBrowserSemanticActionInput {
99
105
  text?: string;
100
106
  role?: string;
101
107
  name?: string;
108
+ session?: string;
102
109
  }
103
110
 
104
111
  interface CompiledAgentBrowserSemanticAction {
@@ -123,6 +130,7 @@ interface CompiledAgentBrowserQaPreset extends CompiledAgentBrowserJob {
123
130
  checkConsole: boolean;
124
131
  checkErrors: boolean;
125
132
  checkNetwork: boolean;
133
+ loadState: AgentBrowserQaLoadState;
126
134
  expectedText: string[];
127
135
  expectedSelector?: string;
128
136
  screenshotPath?: string;
@@ -222,6 +230,7 @@ const AGENT_BROWSER_PARAMS = Type.Object({
222
230
  text: Type.Optional(Type.String({ description: "Text/value argument for fill or select actions." })),
223
231
  role: Type.Optional(Type.String({ description: "Role locator value; when set it must match value for locator=role." })),
224
232
  name: Type.Optional(Type.String({ description: "Accessible name filter for locator=role; compiles to --name <name>." })),
233
+ session: Type.Optional(Type.String({ description: "Optional upstream session name; prepends --session <name> before the compiled find command." })),
225
234
  }),
226
235
  ),
227
236
  qa: Type.Optional(
@@ -232,7 +241,8 @@ const AGENT_BROWSER_PARAMS = Type.Object({
232
241
  screenshotPath: Type.Optional(Type.String({ description: "Optional evidence screenshot path captured at the end of the QA preset." })),
233
242
  checkConsole: Type.Optional(Type.Boolean({ description: "Whether to fail on console error messages. Defaults to true." })),
234
243
  checkErrors: Type.Optional(Type.Boolean({ description: "Whether to fail on page errors. Defaults to true." })),
235
- checkNetwork: Type.Optional(Type.Boolean({ description: "Whether to fail on failed network requests. Defaults to true." })),
244
+ checkNetwork: Type.Optional(Type.Boolean({ description: "Whether to inspect network requests and fail on actionable request failures; benign icon misses warn. Defaults to true." })),
245
+ loadState: Type.Optional(StringEnum(AGENT_BROWSER_QA_LOAD_STATES, { description: "Page readiness state for the QA preset before assertions and diagnostics. Defaults to domcontentloaded; use networkidle only for pages without long-lived background requests." })),
236
246
  }),
237
247
  ),
238
248
  sourceLookup: Type.Optional(
@@ -366,6 +376,7 @@ interface AgentBrowserQaPresetAnalysis {
366
376
  failedChecks: string[];
367
377
  passed: boolean;
368
378
  summary: string;
379
+ warnings: string[];
369
380
  }
370
381
 
371
382
  function getBatchResultItems(data: unknown): Array<Record<string, unknown>> {
@@ -381,6 +392,7 @@ function analyzeQaPresetResults(data: unknown): AgentBrowserQaPresetAnalysis | u
381
392
  const items = getBatchResultItems(data);
382
393
  if (items.length === 0) return undefined;
383
394
  const failedChecks: string[] = [];
395
+ const warnings: string[] = [];
384
396
  for (const item of items) {
385
397
  if (item.success === false) {
386
398
  failedChecks.push(`${getCommandNameFromBatchItem(item) ?? "step"} failed`);
@@ -395,15 +407,20 @@ function analyzeQaPresetResults(data: unknown): AgentBrowserQaPresetAnalysis | u
395
407
  if (errorCount > 0) failedChecks.push(`${errorCount} console error message(s)`);
396
408
  }
397
409
  if (commandName === "network" && Array.isArray(result?.requests)) {
398
- const failedRequestCount = result.requests.filter((request) => isRecord(request) && ((typeof request.status === "number" && request.status >= 400) || request.failed === true || typeof request.error === "string")).length;
399
- if (failedRequestCount > 0) failedChecks.push(`${failedRequestCount} failed network request(s)`);
410
+ const networkFailures = summarizeNetworkFailures(result.requests);
411
+ if (networkFailures.actionableCount > 0) failedChecks.push(`${networkFailures.actionableCount} actionable failed network request(s)`);
412
+ if (networkFailures.benignCount > 0) warnings.push(`${networkFailures.benignCount} benign network request failure(s) ignored`);
400
413
  }
401
414
  }
402
415
  const uniqueFailures = [...new Set(failedChecks)];
416
+ const uniqueWarnings = [...new Set(warnings)];
403
417
  return {
404
418
  failedChecks: uniqueFailures,
405
419
  passed: uniqueFailures.length === 0,
406
- summary: uniqueFailures.length === 0 ? "QA preset passed." : `QA preset failed: ${uniqueFailures.join("; ")}.`,
420
+ summary: uniqueFailures.length === 0
421
+ ? uniqueWarnings.length === 0 ? "QA preset passed." : `QA preset passed with warnings: ${uniqueWarnings.join("; ")}.`
422
+ : `QA preset failed: ${uniqueFailures.join("; ")}.`,
423
+ warnings: uniqueWarnings,
407
424
  };
408
425
  }
409
426
 
@@ -438,16 +455,21 @@ function compileAgentBrowserQaPreset(input: unknown): { compiled?: CompiledAgent
438
455
  return { error: `qa.${field} must be a boolean when provided.` };
439
456
  }
440
457
  }
458
+ const rawLoadState = input.loadState;
459
+ if (rawLoadState !== undefined && (typeof rawLoadState !== "string" || !AGENT_BROWSER_QA_LOAD_STATES.includes(rawLoadState as AgentBrowserQaLoadState))) {
460
+ return { error: `qa.loadState must be one of: ${AGENT_BROWSER_QA_LOAD_STATES.join(", ")}.` };
461
+ }
441
462
  const checkConsole = input.checkConsole !== false;
442
463
  const checkErrors = input.checkErrors !== false;
443
464
  const checkNetwork = input.checkNetwork !== false;
465
+ const loadState = (rawLoadState as AgentBrowserQaLoadState | undefined) ?? "domcontentloaded";
444
466
  const steps: CompiledAgentBrowserJobStep[] = [];
445
467
  if (checkNetwork) steps.push({ action: "wait", args: ["network", "requests", "--clear"] });
446
468
  if (checkConsole) steps.push({ action: "wait", args: ["console", "--clear"] });
447
469
  if (checkErrors) steps.push({ action: "wait", args: ["errors", "--clear"] });
448
470
  steps.push(
449
471
  { action: "open", args: ["open", url] },
450
- { action: "wait", args: ["wait", "--load", "networkidle"] },
472
+ { action: "wait", args: ["wait", "--load", loadState] },
451
473
  );
452
474
  for (const text of expectedText) {
453
475
  steps.push({ action: "assertText", args: ["wait", "--text", text] });
@@ -462,7 +484,7 @@ function compileAgentBrowserQaPreset(input: unknown): { compiled?: CompiledAgent
462
484
  return {
463
485
  compiled: {
464
486
  args: ["batch"],
465
- checks: { checkConsole, checkErrors, checkNetwork, expectedSelector, expectedText, screenshotPath, url },
487
+ checks: { checkConsole, checkErrors, checkNetwork, expectedSelector, expectedText, loadState, screenshotPath, url },
466
488
  stdin: JSON.stringify(steps.map((step) => step.args)),
467
489
  steps,
468
490
  },
@@ -878,6 +900,140 @@ async function analyzeNetworkSourceLookupResults(data: unknown, compiled: Compil
878
900
  return { candidates, failedRequests, limitations, status, summary: failedRequests.length === 0 ? "Network source lookup found no failed requests." : candidates.length > 0 ? `Network source lookup found ${failedRequests.length} failed request(s) and ${candidates.length} candidate source hint(s).` : `Network source lookup found ${failedRequests.length} failed request(s) but no source candidates.` };
879
901
  }
880
902
 
903
+ function appendSemanticActionTextArg(args: string[], action: string, text: string | undefined): void {
904
+ if ((action === "fill" || action === "select") && text) {
905
+ args.push(text);
906
+ }
907
+ }
908
+
909
+ function getCompiledSemanticActionCommandIndex(compiled: CompiledAgentBrowserSemanticAction): number {
910
+ return compiled.args[0] === "--session" ? 2 : 0;
911
+ }
912
+
913
+ function getCompiledSemanticActionTextArg(compiled: CompiledAgentBrowserSemanticAction): string | undefined {
914
+ if (compiled.action !== "fill" && compiled.action !== "select") return undefined;
915
+ const commandIndex = getCompiledSemanticActionCommandIndex(compiled);
916
+ if (commandIndex < 0) return undefined;
917
+ const markerIndex = compiled.args.indexOf("--name");
918
+ return markerIndex >= 0 ? compiled.args[markerIndex - 1] : compiled.args[commandIndex + 4];
919
+ }
920
+
921
+ function getCompiledSemanticActionSessionPrefix(compiled: CompiledAgentBrowserSemanticAction): string[] {
922
+ const commandIndex = getCompiledSemanticActionCommandIndex(compiled);
923
+ return commandIndex > 0 ? compiled.args.slice(0, commandIndex) : [];
924
+ }
925
+
926
+ const SEMANTIC_ACTION_CANDIDATE_ACTION_IDS = new Set([
927
+ "try-searchbox-name-candidate",
928
+ "try-textbox-name-candidate",
929
+ "try-button-name-candidate",
930
+ "try-link-name-candidate",
931
+ "try-labeled-textbox-candidate",
932
+ ]);
933
+
934
+ function formatSemanticActionCandidateText(actions: AgentBrowserNextAction[]): string | undefined {
935
+ const candidateActions = actions.filter((action) => SEMANTIC_ACTION_CANDIDATE_ACTION_IDS.has(action.id) && action.params?.args);
936
+ if (candidateActions.length === 0) return undefined;
937
+ return [
938
+ "Agent-browser candidate fallbacks:",
939
+ ...candidateActions.map((action) => `- ${action.id}: agent_browser ${JSON.stringify({ args: action.params?.args })} — ${action.reason}`),
940
+ ].join("\n");
941
+ }
942
+
943
+ function buildSemanticActionCandidateActions(compiled: CompiledAgentBrowserSemanticAction): AgentBrowserNextAction[] {
944
+ const commandIndex = getCompiledSemanticActionCommandIndex(compiled);
945
+ if (commandIndex < 0) return [];
946
+ const locator = compiled.args[commandIndex + 1];
947
+ const value = compiled.args[commandIndex + 2];
948
+ if (!locator || !value) return [];
949
+ const text = getCompiledSemanticActionTextArg(compiled);
950
+ const sessionPrefix = getCompiledSemanticActionSessionPrefix(compiled);
951
+ const buildRoleCandidate = (role: string, id: string, reason: string): AgentBrowserNextAction => {
952
+ const args = [...sessionPrefix, "find", "role", role, compiled.action];
953
+ appendSemanticActionTextArg(args, compiled.action, text);
954
+ args.push("--name", value);
955
+ return {
956
+ id,
957
+ params: { args: redactInvocationArgs(args) },
958
+ reason,
959
+ safety: "Candidate locator fallback only; inspect the page if multiple elements could match the same accessible name.",
960
+ tool: "agent_browser" as const,
961
+ };
962
+ };
963
+
964
+ if (locator === "placeholder" && compiled.action === "fill") {
965
+ return [
966
+ buildRoleCandidate("searchbox", "try-searchbox-name-candidate", "Retry against a searchbox with the same accessible name; many search inputs expose names instead of placeholders."),
967
+ buildRoleCandidate("textbox", "try-textbox-name-candidate", "Retry against a textbox with the same accessible name when placeholder lookup misses."),
968
+ ];
969
+ }
970
+ if (locator === "text" && compiled.action === "click") {
971
+ return [
972
+ buildRoleCandidate("button", "try-button-name-candidate", "Retry against a button with the same accessible name when text lookup misses."),
973
+ buildRoleCandidate("link", "try-link-name-candidate", "Retry against a link with the same accessible name when text lookup misses."),
974
+ ];
975
+ }
976
+ if (locator === "label" && compiled.action === "fill") {
977
+ return [buildRoleCandidate("textbox", "try-labeled-textbox-candidate", "Retry against a textbox with the same accessible name when label lookup misses.")];
978
+ }
979
+ return [];
980
+ }
981
+
982
+ function normalizeSemanticActionAccessibleName(name: string): string {
983
+ return name.replace(/\s+/g, " ").trim().toLowerCase();
984
+ }
985
+
986
+ function semanticActionNameMatches(candidateName: string, targetName: string): boolean {
987
+ const normalizedCandidate = normalizeSemanticActionAccessibleName(candidateName);
988
+ const normalizedTarget = normalizeSemanticActionAccessibleName(targetName);
989
+ return normalizedCandidate === normalizedTarget || normalizedCandidate.startsWith(`${normalizedTarget} `);
990
+ }
991
+
992
+ function getCompiledSemanticActionRoleTarget(compiled: CompiledAgentBrowserSemanticAction): { role: string; targetName: string } | undefined {
993
+ if (compiled.locator !== "role" || !["check", "click", "uncheck"].includes(compiled.action)) return undefined;
994
+ const findIndex = compiled.args.indexOf("find");
995
+ if (findIndex < 0 || compiled.args[findIndex + 1] !== "role") return undefined;
996
+ const role = compiled.args[findIndex + 2];
997
+ const nameFlagIndex = compiled.args.indexOf("--name");
998
+ const targetName = nameFlagIndex >= 0 ? compiled.args[nameFlagIndex + 1] : undefined;
999
+ if (!role || !targetName) return undefined;
1000
+ return { role, targetName };
1001
+ }
1002
+
1003
+ function findSemanticActionRefInSnapshot(compiled: CompiledAgentBrowserSemanticAction, snapshotData: unknown): string | undefined {
1004
+ const target = getCompiledSemanticActionRoleTarget(compiled);
1005
+ const refs = getSnapshotRefRecord(snapshotData);
1006
+ if (!target || !refs) return undefined;
1007
+ const candidates = Object.entries(refs).flatMap(([ref, entry]) => {
1008
+ if (!/^e\d+$/.test(ref) || !isRecord(entry)) return [];
1009
+ const role = typeof entry.role === "string" ? entry.role : undefined;
1010
+ const name = typeof entry.name === "string" ? entry.name : undefined;
1011
+ if (!role || !name || role.toLowerCase() !== target.role.toLowerCase() || !semanticActionNameMatches(name, target.targetName)) return [];
1012
+ return [{ exact: normalizeSemanticActionAccessibleName(name) === normalizeSemanticActionAccessibleName(target.targetName), name, ref }];
1013
+ });
1014
+ candidates.sort((left, right) => Number(right.exact) - Number(left.exact) || left.name.length - right.name.length || compareRefIds(left.ref, right.ref));
1015
+ return candidates[0]?.ref;
1016
+ }
1017
+
1018
+ interface SemanticActionVisibleRefResolution {
1019
+ args: string[];
1020
+ snapshot: SessionRefSnapshot;
1021
+ }
1022
+
1023
+ async function resolveSemanticActionVisibleRefArgs(options: {
1024
+ compiled: CompiledAgentBrowserSemanticAction | undefined;
1025
+ cwd: string;
1026
+ sessionName?: string;
1027
+ signal?: AbortSignal;
1028
+ }): Promise<SemanticActionVisibleRefResolution | undefined> {
1029
+ if (!options.compiled || !options.sessionName || !getCompiledSemanticActionRoleTarget(options.compiled)) return undefined;
1030
+ const snapshotData = await runSessionCommandData({ args: ["snapshot", "-i"], cwd: options.cwd, sessionName: options.sessionName, signal: options.signal });
1031
+ const ref = findSemanticActionRefInSnapshot(options.compiled, snapshotData);
1032
+ const snapshot = extractRefSnapshotFromData(snapshotData);
1033
+ if (!ref || !snapshot) return undefined;
1034
+ return { args: [...getCompiledSemanticActionSessionPrefix(options.compiled), options.compiled.action, `@${ref}`], snapshot };
1035
+ }
1036
+
881
1037
  function compileAgentBrowserSemanticAction(input: unknown): { compiled?: CompiledAgentBrowserSemanticAction; error?: string } {
882
1038
  if (!isRecord(input)) {
883
1039
  return { error: "semanticAction must be an object." };
@@ -888,6 +1044,7 @@ function compileAgentBrowserSemanticAction(input: unknown): { compiled?: Compile
888
1044
  const text = input.text;
889
1045
  const role = input.role;
890
1046
  const name = input.name;
1047
+ const session = input.session;
891
1048
  if (typeof action !== "string" || !AGENT_BROWSER_SEMANTIC_ACTIONS.includes(action as AgentBrowserSemanticActionName)) {
892
1049
  return { error: `semanticAction.action must be one of: ${AGENT_BROWSER_SEMANTIC_ACTIONS.join(", ")}.` };
893
1050
  }
@@ -912,7 +1069,10 @@ function compileAgentBrowserSemanticAction(input: unknown): { compiled?: Compile
912
1069
  if (name !== undefined && (locator !== "role" || typeof name !== "string" || name.length === 0)) {
913
1070
  return { error: "semanticAction.name is only supported as a non-empty string for locator=role." };
914
1071
  }
915
- const args = ["find", locator, value, action];
1072
+ if (session !== undefined && (typeof session !== "string" || session.trim().length === 0)) {
1073
+ return { error: "semanticAction.session must be a non-empty string when provided." };
1074
+ }
1075
+ const args = typeof session === "string" ? ["--session", session, "find", locator, value, action] : ["find", locator, value, action];
916
1076
  if (action === "fill" || action === "select") {
917
1077
  args.push(text as string);
918
1078
  }
@@ -1300,6 +1460,72 @@ interface NavigationSummary {
1300
1460
  url?: string;
1301
1461
  }
1302
1462
 
1463
+ interface OverlayBlockerCandidate {
1464
+ args: string[];
1465
+ name?: string;
1466
+ reason: string;
1467
+ ref: string;
1468
+ role?: string;
1469
+ }
1470
+
1471
+ interface OverlayBlockerDiagnostic {
1472
+ candidates: OverlayBlockerCandidate[];
1473
+ snapshot: SessionRefSnapshot;
1474
+ summary: string;
1475
+ }
1476
+
1477
+ interface SelectorTextVisibilityDiagnostic {
1478
+ firstMatchVisible?: boolean;
1479
+ firstVisibleTextPreview?: string;
1480
+ matchCount: number;
1481
+ selector: string;
1482
+ summary: string;
1483
+ visibleCount: number;
1484
+ }
1485
+
1486
+ interface TimeoutArtifactEvidence {
1487
+ absolutePath: string;
1488
+ exists: boolean;
1489
+ path: string;
1490
+ sizeBytes?: number;
1491
+ stepIndex: number;
1492
+ }
1493
+
1494
+ interface TimeoutPartialProgress {
1495
+ artifacts: TimeoutArtifactEvidence[];
1496
+ currentPage?: {
1497
+ title?: string;
1498
+ url?: string;
1499
+ };
1500
+ steps?: Array<{ args: string[]; index: number }>;
1501
+ summary: string;
1502
+ }
1503
+
1504
+ interface EvalStdinHint {
1505
+ reason: string;
1506
+ suggestion: string;
1507
+ }
1508
+
1509
+ interface ArtifactCleanupGuidance {
1510
+ explicitArtifactPaths: string[];
1511
+ note: string;
1512
+ owner: "host-file-tools";
1513
+ summary: string;
1514
+ }
1515
+
1516
+ interface ManagedSessionOutcome {
1517
+ activeAfter: boolean;
1518
+ activeBefore: boolean;
1519
+ attemptedSessionName?: string;
1520
+ currentSessionName: string;
1521
+ previousSessionName: string;
1522
+ replacedSessionName?: string;
1523
+ sessionMode: "auto" | "fresh";
1524
+ status: "abandoned" | "closed" | "created" | "preserved" | "replaced" | "unchanged";
1525
+ succeeded: boolean;
1526
+ summary: string;
1527
+ }
1528
+
1303
1529
  function isRecord(value: unknown): value is Record<string, unknown> {
1304
1530
  return typeof value === "object" && value !== null;
1305
1531
  }
@@ -1703,7 +1929,7 @@ function shouldCaptureNavigationSummary(command: string | undefined, data: unkno
1703
1929
  );
1704
1930
  }
1705
1931
 
1706
- function extractStringResultField(data: unknown, fieldName: "title" | "url"): string | undefined {
1932
+ function extractStringResultField(data: unknown, fieldName: "result" | "title" | "url"): string | undefined {
1707
1933
  if (typeof data === "string") {
1708
1934
  const text = data.trim();
1709
1935
  return text.length > 0 ? text : undefined;
@@ -1740,6 +1966,21 @@ interface OrderedSessionTabTarget {
1740
1966
  target: SessionTabTarget;
1741
1967
  }
1742
1968
 
1969
+ interface SessionRefSnapshot {
1970
+ refIds: string[];
1971
+ target?: SessionTabTarget;
1972
+ }
1973
+
1974
+ interface OrderedSessionRefSnapshot extends SessionRefSnapshot {
1975
+ order: number;
1976
+ }
1977
+
1978
+ interface StaleRefPreflight {
1979
+ message: string;
1980
+ refIds: string[];
1981
+ snapshot?: SessionRefSnapshot;
1982
+ }
1983
+
1743
1984
  interface AboutBlankSessionMismatch {
1744
1985
  activeUrl: "about:blank";
1745
1986
  recoveryApplied: boolean;
@@ -1748,7 +1989,7 @@ interface AboutBlankSessionMismatch {
1748
1989
  targetUrl: string;
1749
1990
  }
1750
1991
 
1751
- function getLatestSessionTabTargetOrder(targets: Map<string, OrderedSessionTabTarget>): number {
1992
+ function getLatestSessionTabTargetOrder(targets: Map<string, { order: number }>): number {
1752
1993
  let latestOrder = 0;
1753
1994
  for (const target of targets.values()) {
1754
1995
  latestOrder = Math.max(latestOrder, target.order);
@@ -1757,7 +1998,7 @@ function getLatestSessionTabTargetOrder(targets: Map<string, OrderedSessionTabTa
1757
1998
  }
1758
1999
 
1759
2000
  function shouldApplySessionTabTargetUpdate(options: {
1760
- current?: OrderedSessionTabTarget;
2001
+ current?: { order: number };
1761
2002
  updateOrder: number;
1762
2003
  }): boolean {
1763
2004
  return !options.current || options.updateOrder >= options.current.order;
@@ -1903,6 +2144,66 @@ function restoreSessionTabTargetsFromBranch(branch: unknown[]): Map<string, Orde
1903
2144
  return restoredTargets;
1904
2145
  }
1905
2146
 
2147
+ function extractRefSnapshotFromData(data: unknown): SessionRefSnapshot | undefined {
2148
+ if (!isRecord(data)) return undefined;
2149
+ const refIds = isRecord(data.refs) ? Object.keys(data.refs).filter((refId) => /^e\d+$/.test(refId)) : [];
2150
+ if (refIds.length === 0) return undefined;
2151
+ return {
2152
+ refIds,
2153
+ target: extractSessionTabTargetFromData(data),
2154
+ };
2155
+ }
2156
+
2157
+ function extractRefSnapshotFromBatchResults(data: unknown): SessionRefSnapshot | undefined {
2158
+ if (!Array.isArray(data)) return undefined;
2159
+ let latestSnapshot: SessionRefSnapshot | undefined;
2160
+ for (const item of data) {
2161
+ if (!isRecord(item) || item.success === false) continue;
2162
+ const [name] = extractBatchResultCommand(item);
2163
+ if (name !== "snapshot") continue;
2164
+ latestSnapshot = extractRefSnapshotFromData(item.result) ?? latestSnapshot;
2165
+ }
2166
+ return latestSnapshot;
2167
+ }
2168
+
2169
+ function restoreSessionRefSnapshotsFromBranch(branch: unknown[]): Map<string, OrderedSessionRefSnapshot> {
2170
+ const restoredSnapshots = new Map<string, OrderedSessionRefSnapshot>();
2171
+ let restoredOrder = 0;
2172
+ for (const entry of branch) {
2173
+ if (!isRecord(entry) || entry.type !== "message") continue;
2174
+ const message = isRecord(entry.message) ? entry.message : undefined;
2175
+ if (!message || message.toolName !== "agent_browser") continue;
2176
+ const details = isRecord(message.details) ? message.details : undefined;
2177
+ if (!details) continue;
2178
+ const sessionName = typeof details.sessionName === "string" ? details.sessionName : undefined;
2179
+ if (!sessionName) continue;
2180
+ const command = typeof details.command === "string" ? details.command : undefined;
2181
+ if (command === "close" && message.isError !== true) {
2182
+ restoredOrder += 1;
2183
+ restoredSnapshots.delete(sessionName);
2184
+ continue;
2185
+ }
2186
+ const refSnapshot = isRecord(details.refSnapshot)
2187
+ ? {
2188
+ refIds: Array.isArray(details.refSnapshot.refIds)
2189
+ ? details.refSnapshot.refIds.filter((refId): refId is string => typeof refId === "string" && /^e\d+$/.test(refId))
2190
+ : [],
2191
+ target: isRecord(details.refSnapshot.target)
2192
+ ? normalizeSessionTabTarget({
2193
+ title: typeof details.refSnapshot.target.title === "string" ? details.refSnapshot.target.title : undefined,
2194
+ url: typeof details.refSnapshot.target.url === "string" ? details.refSnapshot.target.url : undefined,
2195
+ })
2196
+ : undefined,
2197
+ }
2198
+ : undefined;
2199
+ if (refSnapshot && refSnapshot.refIds.length > 0) {
2200
+ restoredOrder += 1;
2201
+ restoredSnapshots.set(sessionName, { ...refSnapshot, order: restoredOrder });
2202
+ }
2203
+ }
2204
+ return restoredSnapshots;
2205
+ }
2206
+
1906
2207
  function restoreArtifactManifestFromBranch(branch: unknown[]): SessionArtifactManifest | undefined {
1907
2208
  let restoredManifest: SessionArtifactManifest | undefined;
1908
2209
  for (const entry of branch) {
@@ -2052,6 +2353,46 @@ function parseUserBatchStdin(stdin: string | undefined): { error?: string; steps
2052
2353
  }
2053
2354
  }
2054
2355
 
2356
+ const REF_INVALIDATING_BATCH_COMMANDS = new Set([
2357
+ "back",
2358
+ "check",
2359
+ "click",
2360
+ "dblclick",
2361
+ "drag",
2362
+ "fill",
2363
+ "forward",
2364
+ "goto",
2365
+ "keyboard",
2366
+ "mouse",
2367
+ "navigate",
2368
+ "open",
2369
+ "press",
2370
+ "reload",
2371
+ "select",
2372
+ "type",
2373
+ "uncheck",
2374
+ "upload",
2375
+ ]);
2376
+
2377
+ const REF_GUARDED_COMMANDS = new Set([
2378
+ "check",
2379
+ "click",
2380
+ "dblclick",
2381
+ "download",
2382
+ "drag",
2383
+ "fill",
2384
+ "focus",
2385
+ "hover",
2386
+ "keyboard",
2387
+ "mouse",
2388
+ "press",
2389
+ "scrollintoview",
2390
+ "select",
2391
+ "type",
2392
+ "uncheck",
2393
+ "upload",
2394
+ ]);
2395
+
2055
2396
  function getStaleRefArgs(commandTokens: string[], stdin?: string): string[] {
2056
2397
  if (commandTokens[0] !== "batch" || stdin === undefined) {
2057
2398
  return commandTokens;
@@ -2063,6 +2404,101 @@ function getStaleRefArgs(commandTokens: string[], stdin?: string): string[] {
2063
2404
  return parsed.steps.flatMap((step) => step);
2064
2405
  }
2065
2406
 
2407
+ function collectRefsFromTokens(tokens: string[]): string[] {
2408
+ return tokens.filter((token) => /^@e\d+\b/.test(token)).map((token) => token.slice(1));
2409
+ }
2410
+
2411
+ function getGuardedRefUsage(commandTokens: string[], stdin?: string): string[] {
2412
+ const collectFromStep = (step: string[]) => REF_GUARDED_COMMANDS.has(step[0] ?? "") ? collectRefsFromTokens(step) : [];
2413
+ if (commandTokens[0] !== "batch" || stdin === undefined) {
2414
+ return collectFromStep(commandTokens);
2415
+ }
2416
+ const parsed = parseUserBatchStdin(stdin);
2417
+ if (parsed.error || parsed.steps === undefined) {
2418
+ return collectFromStep(commandTokens);
2419
+ }
2420
+ const refsBeforeInBatchSnapshot: string[] = [];
2421
+ for (const step of parsed.steps) {
2422
+ if ((step[0] ?? "") === "snapshot") break;
2423
+ refsBeforeInBatchSnapshot.push(...collectFromStep(step));
2424
+ }
2425
+ return refsBeforeInBatchSnapshot;
2426
+ }
2427
+
2428
+ function targetsMatch(left: SessionTabTarget | undefined, right: SessionTabTarget | undefined): boolean {
2429
+ if (!left || !right) return true;
2430
+ return normalizeComparableUrl(left.url) === normalizeComparableUrl(right.url);
2431
+ }
2432
+
2433
+ function getBatchRefInvalidationMessage(commandTokens: string[], stdin?: string): string | undefined {
2434
+ if (commandTokens[0] !== "batch" || stdin === undefined) return undefined;
2435
+ const parsed = parseUserBatchStdin(stdin);
2436
+ if (parsed.error || parsed.steps === undefined) return undefined;
2437
+ let priorStepInvalidatesRefs = false;
2438
+ for (const step of parsed.steps) {
2439
+ if ((step[0] ?? "") === "snapshot") {
2440
+ priorStepInvalidatesRefs = false;
2441
+ }
2442
+ const refIds = collectRefsFromTokens(step);
2443
+ if (refIds.length > 0 && REF_GUARDED_COMMANDS.has(step[0] ?? "") && priorStepInvalidatesRefs) {
2444
+ return `Batch step ${step[0]} uses page-scoped ref ${refIds.map((refId) => `@${refId}`).join(", ")} after an earlier batch step can navigate or mutate the page. Split the batch, run snapshot -i after the page-changing step, then retry with current refs.`;
2445
+ }
2446
+ if (REF_INVALIDATING_BATCH_COMMANDS.has(step[0] ?? "")) {
2447
+ priorStepInvalidatesRefs = true;
2448
+ }
2449
+ }
2450
+ return undefined;
2451
+ }
2452
+
2453
+ function buildStaleRefPreflight(options: {
2454
+ commandTokens: string[];
2455
+ currentTarget?: SessionTabTarget;
2456
+ refSnapshot?: SessionRefSnapshot;
2457
+ stdin?: string;
2458
+ }): StaleRefPreflight | undefined {
2459
+ const usedRefIds = [...new Set(getGuardedRefUsage(options.commandTokens, options.stdin))];
2460
+ const batchInvalidationMessage = getBatchRefInvalidationMessage(options.commandTokens, options.stdin);
2461
+ if (batchInvalidationMessage && usedRefIds.length > 0) {
2462
+ return {
2463
+ message: batchInvalidationMessage,
2464
+ refIds: usedRefIds,
2465
+ snapshot: options.refSnapshot,
2466
+ };
2467
+ }
2468
+ if (usedRefIds.length === 0 || !options.refSnapshot) return undefined;
2469
+ if (!targetsMatch(options.refSnapshot.target, options.currentTarget)) {
2470
+ return {
2471
+ message: `Ref ${usedRefIds.map((refId) => `@${refId}`).join(", ")} came from a snapshot for ${options.refSnapshot.target?.url ?? "a prior page"}, but the current session target is ${options.currentTarget?.url ?? "unknown"}. Run snapshot -i again before using page-scoped refs.`,
2472
+ refIds: usedRefIds,
2473
+ snapshot: options.refSnapshot,
2474
+ };
2475
+ }
2476
+ const knownRefs = new Set(options.refSnapshot.refIds);
2477
+ const missingRefs = usedRefIds.filter((refId) => !knownRefs.has(refId));
2478
+ if (missingRefs.length > 0) {
2479
+ return {
2480
+ message: `Ref ${missingRefs.map((refId) => `@${refId}`).join(", ")} was not present in the latest snapshot for this session. Run snapshot -i again before using page-scoped refs.`,
2481
+ refIds: missingRefs,
2482
+ snapshot: options.refSnapshot,
2483
+ };
2484
+ }
2485
+ return undefined;
2486
+ }
2487
+
2488
+ function sessionPrefixArgs(sessionName: string | undefined, args: string[]): string[] {
2489
+ return sessionName && args[0] !== "--session" ? ["--session", sessionName, ...args] : args;
2490
+ }
2491
+
2492
+ function sessionAwareStaleRefNextActions(sessionName: string | undefined): AgentBrowserNextAction[] {
2493
+ return (buildAgentBrowserNextActions({ failureCategory: "stale-ref", resultCategory: "failure" }) ?? []).map((action) => {
2494
+ const actionArgs = action.params?.args;
2495
+ return {
2496
+ ...action,
2497
+ params: action.params && actionArgs ? { ...action.params, args: sessionPrefixArgs(sessionName, actionArgs) } : action.params,
2498
+ };
2499
+ });
2500
+ }
2501
+
2066
2502
  function buildPinnedBatchPlan(options: {
2067
2503
  command?: string;
2068
2504
  commandTokens: string[];
@@ -2196,14 +2632,16 @@ async function runSessionCommandData(options: {
2196
2632
  cwd: string;
2197
2633
  sessionName?: string;
2198
2634
  signal?: AbortSignal;
2635
+ stdin?: string;
2199
2636
  }): Promise<unknown | undefined> {
2200
- const { args, cwd, sessionName, signal } = options;
2637
+ const { args, cwd, sessionName, signal, stdin } = options;
2201
2638
  if (!sessionName) return undefined;
2202
2639
 
2203
2640
  const processResult = await runAgentBrowserProcess({
2204
2641
  args: ["--json", "--session", sessionName, ...args],
2205
2642
  cwd,
2206
2643
  signal,
2644
+ stdin,
2207
2645
  });
2208
2646
  try {
2209
2647
  if (processResult.aborted || processResult.spawnError || processResult.exitCode !== 0) {
@@ -2249,6 +2687,410 @@ function mergeNavigationSummaryIntoData(data: unknown, navigationSummary: Naviga
2249
2687
  return { navigationSummary, result: data };
2250
2688
  }
2251
2689
 
2690
+ function getSnapshotRefRecord(data: unknown): Record<string, unknown> | undefined {
2691
+ return isRecord(data) && isRecord(data.refs) ? data.refs : undefined;
2692
+ }
2693
+
2694
+ const OVERLAY_CLOSE_NAME_PATTERN = /(?:\b(?:close|dismiss|no thanks|not now|maybe later|hide|skip|continue without|x)\b|^\s*×\s*$)/i;
2695
+ const OVERLAY_CONTEXT_ROLES = new Set(["alertdialog", "dialog"]);
2696
+ const OVERLAY_ACTION_ROLES = new Set(["button", "link", "menuitem"]);
2697
+ const OVERLAY_BLOCKER_CANDIDATE_LIMIT = 3;
2698
+
2699
+ function getOverlayBlockerCandidates(snapshotData: unknown): OverlayBlockerCandidate[] {
2700
+ const refs = getSnapshotRefRecord(snapshotData);
2701
+ if (!refs) return [];
2702
+ const hasOverlayContext = Object.values(refs).some((entry) => {
2703
+ if (!isRecord(entry)) return false;
2704
+ const role = typeof entry.role === "string" ? entry.role : "";
2705
+ return OVERLAY_CONTEXT_ROLES.has(role.toLowerCase());
2706
+ });
2707
+ if (!hasOverlayContext) return [];
2708
+ const candidates: OverlayBlockerCandidate[] = [];
2709
+ for (const [ref, entry] of Object.entries(refs)) {
2710
+ if (!/^e\d+$/.test(ref) || !isRecord(entry)) continue;
2711
+ const role = typeof entry.role === "string" ? entry.role : undefined;
2712
+ const name = typeof entry.name === "string" ? entry.name : undefined;
2713
+ if (!role || !OVERLAY_ACTION_ROLES.has(role.toLowerCase()) || !name || !OVERLAY_CLOSE_NAME_PATTERN.test(name)) continue;
2714
+ candidates.push({
2715
+ args: ["click", `@${ref}`],
2716
+ name,
2717
+ reason: `Visible ${role} ${JSON.stringify(name)} appears in a snapshot that also contains overlay/banner/dialog context.`,
2718
+ ref: `@${ref}`,
2719
+ role,
2720
+ });
2721
+ if (candidates.length >= OVERLAY_BLOCKER_CANDIDATE_LIMIT) break;
2722
+ }
2723
+ return candidates;
2724
+ }
2725
+
2726
+ function formatOverlayBlockerText(diagnostic: OverlayBlockerDiagnostic): string {
2727
+ return [
2728
+ "Possible overlay blockers:",
2729
+ ...diagnostic.candidates.map((candidate) => `- ${candidate.ref}${candidate.role ? ` ${candidate.role}` : ""}${candidate.name ? ` ${JSON.stringify(candidate.name)}` : ""}: ${candidate.reason}`),
2730
+ ].join("\n");
2731
+ }
2732
+
2733
+ function buildOverlayBlockerNextActions(options: { diagnostic: OverlayBlockerDiagnostic; sessionName?: string }): AgentBrowserNextAction[] {
2734
+ return [
2735
+ {
2736
+ id: "inspect-overlay-state",
2737
+ params: { args: sessionPrefixArgs(options.sessionName, ["snapshot", "-i"]) },
2738
+ reason: "Refresh interactive refs and inspect whether an overlay, banner, modal, or dialog is blocking the intended click.",
2739
+ safety: "Read-only inspection; use current refs from this snapshot before interacting.",
2740
+ tool: "agent_browser" as const,
2741
+ },
2742
+ ...options.diagnostic.candidates.map((candidate, index) => ({
2743
+ id: `try-overlay-blocker-candidate-${index + 1}`,
2744
+ params: { args: sessionPrefixArgs(options.sessionName, candidate.args) },
2745
+ reason: candidate.reason,
2746
+ safety: "Only click this if the candidate is clearly a close/dismiss control for an overlay that blocks the intended workflow.",
2747
+ tool: "agent_browser" as const,
2748
+ })),
2749
+ ];
2750
+ }
2751
+
2752
+ function buildVisibleTextProbeScript(selector: string): string {
2753
+ return `(() => {\n const selector = ${JSON.stringify(selector)};\n const isVisible = (element) => {\n const style = window.getComputedStyle(element);\n if (!style || style.display === 'none' || style.visibility === 'hidden' || style.visibility === 'collapse' || Number(style.opacity) === 0) return false;\n return Array.from(element.getClientRects()).some((rect) => rect.width > 0 && rect.height > 0);\n };\n let matches = [];\n try {\n matches = Array.from(document.querySelectorAll(selector));\n } catch (error) {\n return JSON.stringify({ selector, error: error instanceof Error ? error.message : String(error) });\n }\n const visible = matches.filter(isVisible);\n const trim = (value) => typeof value === 'string' ? value.trim().replace(/\\s+/g, ' ').slice(0, 200) : undefined;\n return JSON.stringify({\n selector,\n matchCount: matches.length,\n visibleCount: visible.length,\n firstMatchVisible: matches[0] ? isVisible(matches[0]) : undefined,\n firstTextPreview: trim(matches[0]?.textContent),\n firstVisibleTextPreview: trim(visible[0]?.textContent),\n });\n})()`;
2754
+ }
2755
+
2756
+ function parseSelectorTextVisibilityProbe(data: unknown, selector: string): Omit<SelectorTextVisibilityDiagnostic, "summary"> | undefined {
2757
+ const result = extractStringResultField(data, "result");
2758
+ if (!result) return undefined;
2759
+ let parsed: unknown;
2760
+ try {
2761
+ parsed = JSON.parse(result);
2762
+ } catch {
2763
+ return undefined;
2764
+ }
2765
+ if (!isRecord(parsed) || typeof parsed.error === "string") return undefined;
2766
+ const matchCount = typeof parsed.matchCount === "number" ? parsed.matchCount : undefined;
2767
+ const visibleCount = typeof parsed.visibleCount === "number" ? parsed.visibleCount : undefined;
2768
+ if (matchCount === undefined || visibleCount === undefined) return undefined;
2769
+ return {
2770
+ firstMatchVisible: typeof parsed.firstMatchVisible === "boolean" ? parsed.firstMatchVisible : undefined,
2771
+ firstVisibleTextPreview: typeof parsed.firstVisibleTextPreview === "string" && parsed.firstVisibleTextPreview.length > 0 ? redactSensitiveText(parsed.firstVisibleTextPreview) : undefined,
2772
+ matchCount,
2773
+ selector,
2774
+ visibleCount,
2775
+ };
2776
+ }
2777
+
2778
+ function selectorMayExposeSensitiveLiteral(selector: string): boolean {
2779
+ return redactSensitiveText(selector) !== selector || /\[[^\]]*[~|^$*]?=\s*(?:"[^"]*"|'[^']*'|[^\]\s]+)\s*(?:[is]\s*)?\]/.test(selector);
2780
+ }
2781
+
2782
+ async function collectSelectorTextVisibilityDiagnosticForSelector(options: {
2783
+ cwd: string;
2784
+ selector: string | undefined;
2785
+ sessionName?: string;
2786
+ signal?: AbortSignal;
2787
+ }): Promise<SelectorTextVisibilityDiagnostic | undefined> {
2788
+ const { selector } = options;
2789
+ if (!selector || /^@e\d+$/.test(selector) || selectorMayExposeSensitiveLiteral(selector)) return undefined;
2790
+ const probe = await runSessionCommandData({
2791
+ args: ["eval", "--stdin"],
2792
+ cwd: options.cwd,
2793
+ sessionName: options.sessionName,
2794
+ signal: options.signal,
2795
+ stdin: buildVisibleTextProbeScript(selector),
2796
+ });
2797
+ const parsed = parseSelectorTextVisibilityProbe(probe, selector);
2798
+ if (!parsed || parsed.matchCount <= 1 && parsed.firstMatchVisible !== false) return undefined;
2799
+ if (parsed.visibleCount === 0) return undefined;
2800
+ const visibleMatchNoun = `visible match${parsed.visibleCount === 1 ? "" : "es"}`;
2801
+ const visibleMatchVerb = parsed.visibleCount === 1 ? "exists" : "exist";
2802
+ const summary = parsed.firstMatchVisible === false
2803
+ ? `Selector ${JSON.stringify(selector)} matched ${parsed.matchCount} elements; the first match is hidden while ${parsed.visibleCount} ${visibleMatchNoun} ${visibleMatchVerb}.`
2804
+ : `Selector ${JSON.stringify(selector)} matched ${parsed.matchCount} elements; get text reads the first upstream match, which may not be the intended visible tab/panel.`;
2805
+ return { ...parsed, summary };
2806
+ }
2807
+
2808
+ function getBatchGetTextSelectors(data: unknown): string[] {
2809
+ if (!Array.isArray(data)) return [];
2810
+ return data.flatMap((item) => {
2811
+ if (!isRecord(item) || item.success === false) return [];
2812
+ const [command, subcommand, selector] = extractBatchResultCommand(item);
2813
+ return command === "get" && subcommand === "text" && selector ? [selector] : [];
2814
+ });
2815
+ }
2816
+
2817
+ async function collectSelectorTextVisibilityDiagnostics(options: {
2818
+ commandInfo: CommandInfo;
2819
+ commandTokens: string[];
2820
+ cwd: string;
2821
+ data: unknown;
2822
+ sessionName?: string;
2823
+ signal?: AbortSignal;
2824
+ }): Promise<SelectorTextVisibilityDiagnostic[]> {
2825
+ const selectors = options.commandInfo.command === "get" && options.commandInfo.subcommand === "text"
2826
+ ? [options.commandTokens[2]]
2827
+ : options.commandInfo.command === "batch"
2828
+ ? getBatchGetTextSelectors(options.data)
2829
+ : [];
2830
+ const diagnostics: SelectorTextVisibilityDiagnostic[] = [];
2831
+ for (const selector of selectors) {
2832
+ const diagnostic = await collectSelectorTextVisibilityDiagnosticForSelector({
2833
+ cwd: options.cwd,
2834
+ selector,
2835
+ sessionName: options.sessionName,
2836
+ signal: options.signal,
2837
+ });
2838
+ if (diagnostic) diagnostics.push(diagnostic);
2839
+ }
2840
+ return diagnostics.sort((left, right) => Number(right.firstMatchVisible === false) - Number(left.firstMatchVisible === false));
2841
+ }
2842
+
2843
+ function formatSelectorTextVisibilityText(diagnostics: SelectorTextVisibilityDiagnostic[]): string | undefined {
2844
+ if (diagnostics.length === 0) return undefined;
2845
+ return diagnostics.flatMap((diagnostic) => {
2846
+ const lines = [`Selector text visibility warning: ${diagnostic.summary}`];
2847
+ if (diagnostic.firstVisibleTextPreview) lines.push(`First visible text preview: ${JSON.stringify(diagnostic.firstVisibleTextPreview)}`);
2848
+ return lines;
2849
+ }).join("\n");
2850
+ }
2851
+
2852
+ function looksLikeFunctionEvalStdin(stdin: string | undefined): boolean {
2853
+ const trimmed = stdin?.trim();
2854
+ if (!trimmed) return false;
2855
+ return /^(?:async\s+)?function\b/.test(trimmed) || /^(?:async\s*)?\([^)]*\)\s*=>/.test(trimmed) || /^(?:async\s+)?[A-Za-z_$][\w$]*\s*=>/.test(trimmed);
2856
+ }
2857
+
2858
+ function isEmptyRecord(value: unknown): boolean {
2859
+ return isRecord(value) && Object.keys(value).length === 0;
2860
+ }
2861
+
2862
+ function getEvalStdinHint(options: { command?: string; data: unknown; stdin?: string }): EvalStdinHint | undefined {
2863
+ if (options.command !== "eval" || !looksLikeFunctionEvalStdin(options.stdin) || !isRecord(options.data)) return undefined;
2864
+ const result = options.data.result;
2865
+ if (!isEmptyRecord(result)) return undefined;
2866
+ return {
2867
+ reason: "eval --stdin received a function-shaped snippet and the upstream JSON result was an empty object, which often means the function itself was returned or serialized instead of invoked.",
2868
+ suggestion: "Pass a plain expression such as `({ title: document.title })`, or invoke the function explicitly, for example `(() => ({ title: document.title }))()`.",
2869
+ };
2870
+ }
2871
+
2872
+ function formatEvalStdinHintText(hint: EvalStdinHint | undefined): string | undefined {
2873
+ return hint ? `Eval stdin hint: ${hint.reason} ${hint.suggestion}` : undefined;
2874
+ }
2875
+
2876
+ async function getArtifactCleanupGuidance(options: { command?: string; cwd: string; manifest?: SessionArtifactManifest; succeeded: boolean }): Promise<ArtifactCleanupGuidance | undefined> {
2877
+ if (!options.succeeded || options.command !== "close" || !options.manifest || options.manifest.entries.length === 0) return undefined;
2878
+ const explicitEntries = options.manifest.entries.filter((entry) => entry.storageScope === "explicit-path");
2879
+ const explicitArtifactPaths: string[] = [];
2880
+ const seenPaths = new Set<string>();
2881
+ for (const entry of explicitEntries) {
2882
+ if (explicitArtifactPaths.length >= 10) break;
2883
+ const displayPath = entry.path;
2884
+ if (seenPaths.has(displayPath)) continue;
2885
+ const absolutePath = entry.absolutePath ?? (isAbsolute(entry.path) ? entry.path : resolve(options.cwd, entry.path));
2886
+ try {
2887
+ await stat(absolutePath);
2888
+ } catch {
2889
+ continue;
2890
+ }
2891
+ seenPaths.add(displayPath);
2892
+ explicitArtifactPaths.push(displayPath);
2893
+ }
2894
+ return {
2895
+ explicitArtifactPaths,
2896
+ note: "Closing the browser session does not delete explicit screenshots, downloads, PDFs, traces, HAR files, or recordings; clean existing paths with host file tools when no longer needed.",
2897
+ owner: "host-file-tools",
2898
+ summary: formatSessionArtifactRetentionSummary(options.manifest),
2899
+ };
2900
+ }
2901
+
2902
+ function formatArtifactCleanupGuidanceText(guidance: ArtifactCleanupGuidance | undefined): string | undefined {
2903
+ if (!guidance) return undefined;
2904
+ const lines = [
2905
+ "Artifact lifecycle:",
2906
+ `- ${guidance.summary}`,
2907
+ `- ${guidance.note}`,
2908
+ ];
2909
+ if (guidance.explicitArtifactPaths.length > 0) {
2910
+ lines.push(`- Explicit artifact paths to review: ${guidance.explicitArtifactPaths.join(", ")}`);
2911
+ }
2912
+ return lines.join("\n");
2913
+ }
2914
+
2915
+ function buildSelectorTextVisibilityNextActions(options: { diagnostics: SelectorTextVisibilityDiagnostic[]; sessionName?: string }): AgentBrowserNextAction[] {
2916
+ return options.diagnostics.map((diagnostic, index) => ({
2917
+ id: index === 0 ? "inspect-visible-text-candidates" : `inspect-visible-text-candidates-${index + 1}`,
2918
+ params: {
2919
+ args: sessionPrefixArgs(options.sessionName, ["eval", "--stdin"]),
2920
+ stdin: buildVisibleTextProbeScript(diagnostic.selector),
2921
+ },
2922
+ reason: "Inspect selector match count and visible text before trusting get text on tabbed or hidden DOM content.",
2923
+ safety: "Read-only DOM inspection; use a more specific visible selector or current @ref before acting on hidden-tab text.",
2924
+ tool: "agent_browser" as const,
2925
+ }));
2926
+ }
2927
+
2928
+ function getTimeoutProgressSteps(compiledJob: CompiledAgentBrowserJob | undefined, command: string | undefined, stdin: string | undefined): Array<{ args: string[]; index: number }> {
2929
+ if (compiledJob) return compiledJob.steps.map((step, index) => ({ args: step.args, index: index + 1 }));
2930
+ if (command !== "batch" || !stdin) return [];
2931
+ try {
2932
+ const parsed = JSON.parse(stdin) as unknown;
2933
+ if (!Array.isArray(parsed)) return [];
2934
+ return parsed.flatMap((step, index) => Array.isArray(step) && step.every((token) => typeof token === "string") ? [{ args: step as string[], index: index + 1 }] : []);
2935
+ } catch {
2936
+ return [];
2937
+ }
2938
+ }
2939
+
2940
+ function getLastPositionalToken(args: string[], startIndex = 1): string | undefined {
2941
+ for (let index = args.length - 1; index >= startIndex; index -= 1) {
2942
+ const token = args[index];
2943
+ if (token && !token.startsWith("-")) return token;
2944
+ }
2945
+ return undefined;
2946
+ }
2947
+
2948
+ function getTimeoutStepArtifactPath(args: string[]): string | undefined {
2949
+ const [command] = args;
2950
+ if (command === "screenshot") {
2951
+ const index = getScreenshotPathTokenIndex(args);
2952
+ return index === undefined ? undefined : args[index];
2953
+ }
2954
+ if (command === "pdf") return getLastPositionalToken(args);
2955
+ if (command === "download") return getLastPositionalToken(args, 2);
2956
+ if (command === "wait") {
2957
+ const inlineDownload = args.find((token) => token.startsWith("--download="));
2958
+ if (inlineDownload) return inlineDownload.slice("--download=".length) || undefined;
2959
+ const downloadIndex = args.indexOf("--download");
2960
+ const downloadPath = downloadIndex >= 0 ? args[downloadIndex + 1] : undefined;
2961
+ if (downloadPath && !downloadPath.startsWith("-")) return downloadPath;
2962
+ }
2963
+ return undefined;
2964
+ }
2965
+
2966
+ async function collectTimeoutArtifactEvidence(cwd: string, steps: Array<{ args: string[]; index: number }>): Promise<TimeoutArtifactEvidence[]> {
2967
+ const evidence: TimeoutArtifactEvidence[] = [];
2968
+ for (const step of steps) {
2969
+ const path = getTimeoutStepArtifactPath(step.args);
2970
+ if (!path) continue;
2971
+ const absolutePath = isAbsolute(path) ? path : resolve(cwd, path);
2972
+ try {
2973
+ const stats = await stat(absolutePath);
2974
+ evidence.push({ absolutePath, exists: true, path, sizeBytes: stats.size, stepIndex: step.index });
2975
+ } catch {
2976
+ evidence.push({ absolutePath, exists: false, path, stepIndex: step.index });
2977
+ }
2978
+ }
2979
+ return evidence;
2980
+ }
2981
+
2982
+ function getPlannedCurrentPageUrl(steps: Array<{ args: string[]; index: number }>): string | undefined {
2983
+ for (let index = steps.length - 1; index >= 0; index -= 1) {
2984
+ const args = steps[index]?.args ?? [];
2985
+ if (args[0] === "open" || args[0] === "navigate" || args[0] === "pushstate") {
2986
+ return getLastPositionalToken(args);
2987
+ }
2988
+ }
2989
+ return undefined;
2990
+ }
2991
+
2992
+ async function collectTimeoutPartialProgress(options: {
2993
+ command?: string;
2994
+ compiledJob?: CompiledAgentBrowserJob;
2995
+ cwd: string;
2996
+ sessionName?: string;
2997
+ stdin?: string;
2998
+ }): Promise<TimeoutPartialProgress | undefined> {
2999
+ const steps = getTimeoutProgressSteps(options.compiledJob, options.command, options.stdin);
3000
+ const artifacts = await collectTimeoutArtifactEvidence(options.cwd, steps);
3001
+ const [urlData, titleData] = await Promise.all([
3002
+ runSessionCommandData({ args: ["get", "url"], cwd: options.cwd, sessionName: options.sessionName }),
3003
+ runSessionCommandData({ args: ["get", "title"], cwd: options.cwd, sessionName: options.sessionName }),
3004
+ ]);
3005
+ const recoveredUrl = extractStringResultField(urlData, "result") ?? extractStringResultField(urlData, "url");
3006
+ const title = extractStringResultField(titleData, "result") ?? extractStringResultField(titleData, "title");
3007
+ const plannedUrl = recoveredUrl ? undefined : getPlannedCurrentPageUrl(steps);
3008
+ const url = recoveredUrl ?? plannedUrl;
3009
+ if (steps.length === 0 && artifacts.length === 0 && !url && !title) return undefined;
3010
+ const foundArtifacts = artifacts.filter((artifact) => artifact.exists).length;
3011
+ const pageStateSummary = recoveredUrl || title ? " and current page state" : plannedUrl ? " and planned page URL" : "";
3012
+ return {
3013
+ artifacts,
3014
+ currentPage: url || title ? { title, url } : undefined,
3015
+ steps: steps.length > 0 ? steps : undefined,
3016
+ summary: `Timed out before upstream returned final results; recovered ${foundArtifacts}/${artifacts.length} declared artifact path${artifacts.length === 1 ? "" : "s"}${pageStateSummary}.`,
3017
+ };
3018
+ }
3019
+
3020
+ function redactSensitivePathSegmentsForDiagnostic(path: string): string {
3021
+ return path.split(/([/\\]+)/).map((segment) => {
3022
+ if (segment === "/" || segment === "\\" || /^[/\\]+$/.test(segment)) return segment;
3023
+ return redactSensitiveText(segment) !== segment || /(?:secret|token|password|passwd|credential|auth|api[-_]?key|bearer)/i.test(segment) ? "[REDACTED]" : segment;
3024
+ }).join("");
3025
+ }
3026
+
3027
+ function sanitizeCurrentPageUrlForTimeoutDiagnostic(url: string): string {
3028
+ try {
3029
+ const parsedUrl = new URL(url);
3030
+ parsedUrl.pathname = parsedUrl.pathname.split("/").map((segment) => redactSensitivePathSegmentsForDiagnostic(segment)).join("/");
3031
+ for (const [key, value] of parsedUrl.searchParams.entries()) {
3032
+ if (redactSensitiveText(key) !== key || redactSensitiveText(value) !== value || /(?:secret|token|password|passwd|credential|auth|api[-_]?key|bearer)/i.test(`${key} ${value}`)) {
3033
+ parsedUrl.searchParams.set(key, "[REDACTED]");
3034
+ }
3035
+ }
3036
+ if (parsedUrl.hash) {
3037
+ parsedUrl.hash = redactSensitivePathSegmentsForDiagnostic(redactSensitiveText(parsedUrl.hash));
3038
+ }
3039
+ return redactSensitiveText(parsedUrl.toString());
3040
+ } catch {
3041
+ return redactSensitivePathSegmentsForDiagnostic(redactSensitiveText(url));
3042
+ }
3043
+ }
3044
+
3045
+ function formatTimeoutPartialProgressText(progress: TimeoutPartialProgress): string {
3046
+ const lines = [`Timeout partial progress: ${progress.summary}`];
3047
+ const currentPageTitle = progress.currentPage?.title ? redactSensitivePathSegmentsForDiagnostic(redactSensitiveText(progress.currentPage.title)) : undefined;
3048
+ const currentPageUrl = progress.currentPage?.url ? sanitizeCurrentPageUrlForTimeoutDiagnostic(progress.currentPage.url) : undefined;
3049
+ if (currentPageTitle || currentPageUrl) {
3050
+ lines.push(`Current page: ${[currentPageTitle, currentPageUrl].filter(Boolean).join(" — ")}`);
3051
+ }
3052
+ if (progress.steps && progress.steps.length > 0) {
3053
+ const shownSteps = progress.steps.slice(0, 6);
3054
+ lines.push("Planned steps:");
3055
+ for (const step of shownSteps) {
3056
+ const command = redactSensitivePathSegmentsForDiagnostic(redactInvocationArgs(step.args).join(" "));
3057
+ lines.push(`- Step ${step.index}: ${command}`);
3058
+ }
3059
+ if (progress.steps.length > shownSteps.length) {
3060
+ lines.push(`- ... ${progress.steps.length - shownSteps.length} more step${progress.steps.length - shownSteps.length === 1 ? "" : "s"} omitted`);
3061
+ }
3062
+ }
3063
+ for (const artifact of progress.artifacts) {
3064
+ const path = redactSensitivePathSegmentsForDiagnostic(artifact.path);
3065
+ lines.push(`Artifact from step ${artifact.stepIndex}: ${path} (${artifact.exists ? `exists${typeof artifact.sizeBytes === "number" ? `, ${artifact.sizeBytes} bytes` : ""}` : "missing"})`);
3066
+ }
3067
+ return lines.join("\n");
3068
+ }
3069
+
3070
+ async function collectOverlayBlockerDiagnostic(options: {
3071
+ command?: string;
3072
+ cwd: string;
3073
+ data: unknown;
3074
+ navigationSummary?: NavigationSummary;
3075
+ priorTarget?: SessionTabTarget;
3076
+ sessionName?: string;
3077
+ signal?: AbortSignal;
3078
+ }): Promise<OverlayBlockerDiagnostic | undefined> {
3079
+ if (options.command !== "click" || !isRecord(options.data) || typeof options.data.clicked !== "string") return undefined;
3080
+ const priorUrl = normalizeComparableUrl(options.priorTarget?.url);
3081
+ const currentUrl = normalizeComparableUrl(options.navigationSummary?.url);
3082
+ if (!priorUrl || !currentUrl || priorUrl !== currentUrl) return undefined;
3083
+ const snapshotData = await runSessionCommandData({ args: ["snapshot", "-i"], cwd: options.cwd, sessionName: options.sessionName, signal: options.signal });
3084
+ const candidates = getOverlayBlockerCandidates(snapshotData);
3085
+ const snapshot = extractRefSnapshotFromData(snapshotData);
3086
+ if (candidates.length === 0 || !snapshot) return undefined;
3087
+ return {
3088
+ candidates,
3089
+ snapshot,
3090
+ summary: `Click completed but the page stayed on ${currentUrl}; a fresh snapshot contains likely overlay close/dismiss controls.`,
3091
+ };
3092
+ }
3093
+
2252
3094
  async function collectOpenResultTabCorrection(options: {
2253
3095
  cwd: string;
2254
3096
  sessionName?: string;
@@ -2314,6 +3156,68 @@ function buildSessionDetailFields(sessionName: string | undefined, usedImplicitS
2314
3156
  return sessionName ? { sessionName, usedImplicitSession } : {};
2315
3157
  }
2316
3158
 
3159
+ function buildManagedSessionOutcome(options: {
3160
+ activeAfter: boolean;
3161
+ activeBefore: boolean;
3162
+ attemptedSessionName?: string;
3163
+ command?: string;
3164
+ currentSessionName: string;
3165
+ previousSessionName: string;
3166
+ replacedSessionName?: string;
3167
+ sessionMode: "auto" | "fresh";
3168
+ succeeded: boolean;
3169
+ }): ManagedSessionOutcome | undefined {
3170
+ const { activeAfter, activeBefore, attemptedSessionName, command, currentSessionName, previousSessionName, replacedSessionName, sessionMode, succeeded } = options;
3171
+ if (!attemptedSessionName) return undefined;
3172
+ let status: ManagedSessionOutcome["status"];
3173
+ let summary: string;
3174
+ if (command === "close") {
3175
+ status = succeeded ? "closed" : activeBefore ? "preserved" : "abandoned";
3176
+ summary = succeeded
3177
+ ? `Managed session ${attemptedSessionName} was closed.`
3178
+ : activeBefore
3179
+ ? `Managed session close failed; previous managed session ${previousSessionName} remains current.`
3180
+ : `Managed session close failed; no managed session is active.`;
3181
+ } else if (succeeded) {
3182
+ if (replacedSessionName) {
3183
+ status = "replaced";
3184
+ summary = `Managed session ${replacedSessionName} was replaced by ${currentSessionName}.`;
3185
+ } else if (!activeBefore && activeAfter) {
3186
+ status = "created";
3187
+ summary = `Managed session ${currentSessionName} is now current.`;
3188
+ } else {
3189
+ status = "unchanged";
3190
+ summary = `Managed session ${currentSessionName} remains current.`;
3191
+ }
3192
+ } else if (activeBefore) {
3193
+ status = "preserved";
3194
+ summary = sessionMode === "fresh" && attemptedSessionName !== previousSessionName
3195
+ ? `Fresh managed session ${attemptedSessionName} failed before becoming current; previous managed session ${previousSessionName} was preserved.`
3196
+ : `Managed session call failed; previous managed session ${previousSessionName} was preserved.`;
3197
+ } else {
3198
+ status = "abandoned";
3199
+ summary = sessionMode === "fresh"
3200
+ ? `Fresh managed session ${attemptedSessionName} failed before becoming current; no previous managed session was active, so no managed session is current.`
3201
+ : `Managed session call failed before any managed session became current.`;
3202
+ }
3203
+ return {
3204
+ activeAfter,
3205
+ activeBefore,
3206
+ attemptedSessionName,
3207
+ currentSessionName,
3208
+ previousSessionName,
3209
+ replacedSessionName,
3210
+ sessionMode,
3211
+ status,
3212
+ succeeded,
3213
+ summary,
3214
+ };
3215
+ }
3216
+
3217
+ function formatManagedSessionOutcomeText(outcome: ManagedSessionOutcome | undefined): string | undefined {
3218
+ return outcome && !outcome.succeeded && outcome.sessionMode === "fresh" ? `Managed session outcome: ${outcome.summary}` : undefined;
3219
+ }
3220
+
2317
3221
  function getPersistentSessionArtifactStore(ctx: {
2318
3222
  sessionManager: {
2319
3223
  getSessionDir?: () => string;
@@ -2465,6 +3369,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
2465
3369
  let managedSessionCwd = process.cwd();
2466
3370
  let freshSessionOrdinal = 0;
2467
3371
  let sessionTabTargets = new Map<string, OrderedSessionTabTarget>();
3372
+ let sessionRefSnapshots = new Map<string, OrderedSessionRefSnapshot>();
2468
3373
  let sessionTabTargetUpdateOrder = 0;
2469
3374
  let traceOwners = new Map<string, TraceOwner>();
2470
3375
  let artifactManifest: SessionArtifactManifest | undefined;
@@ -2478,7 +3383,8 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
2478
3383
  managedSessionCwd = ctx.cwd;
2479
3384
  freshSessionOrdinal = restoredState.freshSessionOrdinal;
2480
3385
  sessionTabTargets = restoreSessionTabTargetsFromBranch(ctx.sessionManager.getBranch());
2481
- sessionTabTargetUpdateOrder = getLatestSessionTabTargetOrder(sessionTabTargets);
3386
+ sessionRefSnapshots = restoreSessionRefSnapshotsFromBranch(ctx.sessionManager.getBranch());
3387
+ sessionTabTargetUpdateOrder = Math.max(getLatestSessionTabTargetOrder(sessionTabTargets), getLatestSessionTabTargetOrder(sessionRefSnapshots));
2482
3388
  artifactManifest = restoreArtifactManifestFromBranch(ctx.sessionManager.getBranch());
2483
3389
  });
2484
3390
 
@@ -2495,6 +3401,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
2495
3401
  }
2496
3402
  managedSessionActive = false;
2497
3403
  sessionTabTargets = new Map<string, OrderedSessionTabTarget>();
3404
+ sessionRefSnapshots = new Map<string, OrderedSessionRefSnapshot>();
2498
3405
  sessionTabTargetUpdateOrder = 0;
2499
3406
  traceOwners = new Map<string, TraceOwner>();
2500
3407
  artifactManifest = undefined;
@@ -2624,12 +3531,29 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
2624
3531
  const runTool = async (): Promise<AgentBrowserToolResult> => {
2625
3532
  const sessionMode = params.sessionMode ?? DEFAULT_SESSION_MODE;
2626
3533
  const freshSessionName = createFreshSessionName(managedSessionBaseName, ephemeralSessionSeed, freshSessionOrdinal + 1);
2627
- const executionPlan = buildExecutionPlan(preparedArgs.args, {
3534
+ let executionPlan = buildExecutionPlan(preparedArgs.args, {
2628
3535
  freshSessionName,
2629
3536
  managedSessionActive,
2630
3537
  managedSessionName,
2631
3538
  sessionMode,
2632
3539
  });
3540
+ let semanticActionVisibleRefResolution: SemanticActionVisibleRefResolution | undefined;
3541
+ if (!executionPlan.validationError && executionPlan.managedSessionName !== freshSessionName) {
3542
+ semanticActionVisibleRefResolution = await resolveSemanticActionVisibleRefArgs({
3543
+ compiled: compiledSemanticAction,
3544
+ cwd: ctx.cwd,
3545
+ sessionName: executionPlan.sessionName,
3546
+ signal,
3547
+ });
3548
+ if (semanticActionVisibleRefResolution) {
3549
+ executionPlan = buildExecutionPlan(semanticActionVisibleRefResolution.args, {
3550
+ freshSessionName,
3551
+ managedSessionActive,
3552
+ managedSessionName,
3553
+ sessionMode,
3554
+ });
3555
+ }
3556
+ }
2633
3557
  const redactedEffectiveArgs = redactInvocationArgs(executionPlan.effectiveArgs);
2634
3558
  const redactedRecoveryHint = redactRecoveryHint(executionPlan.recoveryHint);
2635
3559
  const compatibilityWorkaround: CompatibilityWorkaround | undefined = executionPlan.compatibilityWorkaround;
@@ -2657,7 +3581,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
2657
3581
  };
2658
3582
  }
2659
3583
 
2660
- const commandTokens = extractCommandTokens(preparedArgs.args);
3584
+ const commandTokens = semanticActionVisibleRefResolution ? extractCommandTokens(semanticActionVisibleRefResolution.args) : extractCommandTokens(preparedArgs.args);
2661
3585
  const exactSensitiveValues = getExactSensitiveStdinValues({
2662
3586
  command: executionPlan.commandInfo.command,
2663
3587
  commandTokens,
@@ -2726,6 +3650,34 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
2726
3650
 
2727
3651
  const priorSessionTabTargetState = executionPlan.sessionName ? sessionTabTargets.get(executionPlan.sessionName) : undefined;
2728
3652
  const priorSessionTabTarget = priorSessionTabTargetState?.target;
3653
+ const priorRefSnapshotState = executionPlan.sessionName ? sessionRefSnapshots.get(executionPlan.sessionName) : undefined;
3654
+ const resolvedSemanticActionRefSnapshot = semanticActionVisibleRefResolution?.snapshot
3655
+ ? { ...semanticActionVisibleRefResolution.snapshot, target: semanticActionVisibleRefResolution.snapshot.target ?? priorSessionTabTarget }
3656
+ : undefined;
3657
+ const staleRefPreflight = buildStaleRefPreflight({
3658
+ commandTokens,
3659
+ currentTarget: priorSessionTabTarget,
3660
+ refSnapshot: resolvedSemanticActionRefSnapshot ?? priorRefSnapshotState,
3661
+ stdin: toolStdin,
3662
+ });
3663
+ if (staleRefPreflight) {
3664
+ return {
3665
+ content: [{ type: "text", text: staleRefPreflight.message }],
3666
+ details: {
3667
+ args: redactedArgs,
3668
+ command: executionPlan.commandInfo.command,
3669
+ compatibilityWorkaround,
3670
+ effectiveArgs: redactedEffectiveArgs,
3671
+ nextActions: sessionAwareStaleRefNextActions(executionPlan.sessionName),
3672
+ refIds: staleRefPreflight.refIds,
3673
+ refSnapshot: staleRefPreflight.snapshot,
3674
+ sessionMode,
3675
+ ...buildAgentBrowserResultCategoryDetails({ args: redactedEffectiveArgs, command: executionPlan.commandInfo.command, errorText: staleRefPreflight.message, failureCategory: "stale-ref", succeeded: false }),
3676
+ ...buildSessionDetailFields(executionPlan.sessionName, executionPlan.usedImplicitSession),
3677
+ },
3678
+ isError: true,
3679
+ };
3680
+ }
2729
3681
  let pinnedBatchUnwrapMode: PinnedBatchUnwrapMode | undefined;
2730
3682
  let includePinnedNavigationSummary = false;
2731
3683
  let sessionTabCorrection: OpenResultTabCorrection | undefined;
@@ -2832,12 +3784,24 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
2832
3784
 
2833
3785
  if (processResult.spawnError?.message.includes("ENOENT")) {
2834
3786
  const errorText = buildMissingBinaryMessage();
3787
+ const managedSessionOutcome = buildManagedSessionOutcome({
3788
+ activeAfter: managedSessionActive,
3789
+ activeBefore: managedSessionActive,
3790
+ attemptedSessionName: executionPlan.managedSessionName,
3791
+ command: executionPlan.commandInfo.command,
3792
+ currentSessionName: managedSessionName,
3793
+ previousSessionName: managedSessionName,
3794
+ sessionMode,
3795
+ succeeded: false,
3796
+ });
3797
+ const managedSessionOutcomeText = formatManagedSessionOutcomeText(managedSessionOutcome);
2835
3798
  return {
2836
- content: [{ type: "text", text: errorText }],
3799
+ content: [{ type: "text", text: managedSessionOutcomeText ? `${errorText}\n\n${managedSessionOutcomeText}` : errorText }],
2837
3800
  details: {
2838
3801
  args: redactedArgs,
2839
3802
  compatibilityWorkaround,
2840
3803
  effectiveArgs: redactedProcessArgs,
3804
+ managedSessionOutcome,
2841
3805
  sessionMode,
2842
3806
  sessionTabCorrection,
2843
3807
  ...buildAgentBrowserResultCategoryDetails({ args: redactedProcessArgs, command: executionPlan.commandInfo.command, errorText, failureCategory: "missing-binary", spawnError: processResult.spawnError.message, succeeded: false }),
@@ -2918,6 +3882,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
2918
3882
  data: mergeNavigationSummaryIntoData(presentationEnvelope.data, navigationSummary),
2919
3883
  };
2920
3884
  }
3885
+ let overlayBlockerDiagnostic: OverlayBlockerDiagnostic | undefined;
2921
3886
 
2922
3887
  let openResultTabCorrection: OpenResultTabCorrection | undefined;
2923
3888
  if (
@@ -3021,33 +3986,91 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
3021
3986
  }
3022
3987
  }
3023
3988
  }
3989
+ let selectorTextVisibilityDiagnostics: SelectorTextVisibilityDiagnostic[] = [];
3990
+ const timeoutPartialProgress = processResult.timedOut ? await collectTimeoutPartialProgress({
3991
+ command: executionPlan.commandInfo.command,
3992
+ compiledJob,
3993
+ cwd: ctx.cwd,
3994
+ sessionName: executionPlan.sessionName,
3995
+ stdin: toolStdin,
3996
+ }) : undefined;
3997
+ if (succeeded && !sessionTabCorrection && !aboutBlankSessionMismatch) {
3998
+ overlayBlockerDiagnostic = await collectOverlayBlockerDiagnostic({
3999
+ command: executionPlan.commandInfo.command,
4000
+ cwd: ctx.cwd,
4001
+ data: presentationEnvelope?.data,
4002
+ navigationSummary,
4003
+ priorTarget: priorSessionTabTarget,
4004
+ sessionName: executionPlan.sessionName,
4005
+ signal,
4006
+ });
4007
+ }
4008
+ if (succeeded) {
4009
+ selectorTextVisibilityDiagnostics = await collectSelectorTextVisibilityDiagnostics({
4010
+ commandInfo: executionPlan.commandInfo,
4011
+ commandTokens,
4012
+ cwd: ctx.cwd,
4013
+ data: presentationEnvelope?.data,
4014
+ sessionName: executionPlan.sessionName,
4015
+ signal,
4016
+ });
4017
+ }
4018
+ let currentRefSnapshot: SessionRefSnapshot | undefined;
3024
4019
  if (executionPlan.sessionName) {
3025
4020
  const activeSessionTabTargetState = sessionTabTargets.get(executionPlan.sessionName);
3026
4021
  if (shouldApplySessionTabTargetUpdate({ current: activeSessionTabTargetState, updateOrder: tabTargetUpdateOrder })) {
3027
4022
  if (executionPlan.commandInfo.command === "close" && succeeded) {
3028
4023
  sessionTabTargets.delete(executionPlan.sessionName);
4024
+ sessionRefSnapshots.delete(executionPlan.sessionName);
3029
4025
  } else if (currentSessionTabTarget) {
3030
4026
  sessionTabTargets.set(executionPlan.sessionName, { order: tabTargetUpdateOrder, target: currentSessionTabTarget });
3031
4027
  }
3032
4028
  }
4029
+ const refSnapshot = succeeded
4030
+ ? executionPlan.commandInfo.command === "snapshot"
4031
+ ? extractRefSnapshotFromData(presentationEnvelope?.data)
4032
+ : executionPlan.commandInfo.command === "batch"
4033
+ ? extractRefSnapshotFromBatchResults(presentationEnvelope?.data)
4034
+ : resolvedSemanticActionRefSnapshot ?? overlayBlockerDiagnostic?.snapshot
4035
+ : undefined;
4036
+ if (refSnapshot && shouldApplySessionTabTargetUpdate({ current: sessionRefSnapshots.get(executionPlan.sessionName), updateOrder: tabTargetUpdateOrder })) {
4037
+ currentRefSnapshot = { ...refSnapshot, target: refSnapshot.target ?? currentSessionTabTarget };
4038
+ sessionRefSnapshots.set(executionPlan.sessionName, { ...currentRefSnapshot, order: tabTargetUpdateOrder });
4039
+ } else {
4040
+ currentRefSnapshot = sessionRefSnapshots.get(executionPlan.sessionName);
4041
+ }
3033
4042
  }
3034
4043
 
4044
+ const priorManagedSessionActive = managedSessionActive;
3035
4045
  const priorManagedSessionCwd = managedSessionCwd;
4046
+ const priorManagedSessionName = managedSessionName;
3036
4047
  const managedSessionState = resolveManagedSessionState({
3037
4048
  command: executionPlan.commandInfo.command,
3038
4049
  managedSessionName: executionPlan.managedSessionName,
3039
- priorActive: managedSessionActive,
3040
- priorSessionName: managedSessionName,
4050
+ priorActive: priorManagedSessionActive,
4051
+ priorSessionName: priorManagedSessionName,
3041
4052
  succeeded,
3042
4053
  });
3043
4054
  const replacedManagedSessionName = managedSessionState.replacedSessionName;
3044
4055
  managedSessionActive = managedSessionState.active;
3045
4056
  managedSessionName = managedSessionState.sessionName;
4057
+ let managedSessionOutcome = buildManagedSessionOutcome({
4058
+ activeAfter: managedSessionActive,
4059
+ activeBefore: priorManagedSessionActive,
4060
+ attemptedSessionName: executionPlan.managedSessionName,
4061
+ command: executionPlan.commandInfo.command,
4062
+ currentSessionName: managedSessionName,
4063
+ previousSessionName: priorManagedSessionName,
4064
+ replacedSessionName: replacedManagedSessionName,
4065
+ sessionMode,
4066
+ succeeded,
4067
+ });
3046
4068
  if (executionPlan.managedSessionName && succeeded) {
3047
4069
  managedSessionCwd = ctx.cwd;
3048
4070
  }
3049
4071
  if (replacedManagedSessionName) {
3050
4072
  sessionTabTargets.delete(replacedManagedSessionName);
4073
+ sessionRefSnapshots.delete(replacedManagedSessionName);
3051
4074
  await closeManagedSession({
3052
4075
  cwd: priorManagedSessionCwd,
3053
4076
  sessionName: replacedManagedSessionName,
@@ -3132,9 +4155,11 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
3132
4155
  } else if (sourceLookup) {
3133
4156
  presentation.content.unshift({ type: "text", text: sourceLookup.summary });
3134
4157
  }
3135
- if (qaPreset && !qaPreset.passed) {
3136
- succeeded = false;
3137
- presentation.failureCategory = "qa-failure";
4158
+ if (qaPreset && (!qaPreset.passed || qaPreset.warnings.length > 0)) {
4159
+ if (!qaPreset.passed) {
4160
+ succeeded = false;
4161
+ presentation.failureCategory = "qa-failure";
4162
+ }
3138
4163
  presentation.summary = qaPreset.summary;
3139
4164
  if (presentation.content[0]?.type === "text") {
3140
4165
  presentation.content[0] = { ...presentation.content[0], text: `${qaPreset.summary}\n\n${presentation.content[0].text}` };
@@ -3142,6 +4167,21 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
3142
4167
  presentation.content.unshift({ type: "text", text: qaPreset.summary });
3143
4168
  }
3144
4169
  }
4170
+ if (managedSessionOutcome && managedSessionOutcome.succeeded !== succeeded) {
4171
+ managedSessionOutcome = { ...managedSessionOutcome, succeeded };
4172
+ }
4173
+ const evalStdinHint = getEvalStdinHint({
4174
+ command: executionPlan.commandInfo.command,
4175
+ data: presentationEnvelope?.data,
4176
+ stdin: toolStdin,
4177
+ });
4178
+ const resultArtifactManifest = presentation.artifactManifest ?? artifactManifest;
4179
+ const artifactCleanup = await getArtifactCleanupGuidance({
4180
+ command: executionPlan.commandInfo.command,
4181
+ cwd: ctx.cwd,
4182
+ manifest: resultArtifactManifest,
4183
+ succeeded,
4184
+ });
3145
4185
  const warningText = aboutBlankSessionMismatch ? buildAboutBlankWarning(aboutBlankSessionMismatch) : undefined;
3146
4186
  const contentWithSessionWarnings = userRequestedJson && !plainTextInspection
3147
4187
  ? buildJsonVisibleContent({
@@ -3187,6 +4227,21 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
3187
4227
  validationError: undefined,
3188
4228
  });
3189
4229
  let nextActions = presentation.nextActions ? [...presentation.nextActions] : undefined;
4230
+ if (categoryDetails.failureCategory === "stale-ref") {
4231
+ nextActions = sessionAwareStaleRefNextActions(executionPlan.sessionName);
4232
+ }
4233
+ if (categoryDetails.failureCategory === "selector-not-found" && redactedCompiledSemanticAction) {
4234
+ const candidateActions = buildSemanticActionCandidateActions(redactedCompiledSemanticAction);
4235
+ if (candidateActions.length > 0) {
4236
+ (nextActions ??= []).push(...candidateActions);
4237
+ }
4238
+ }
4239
+ if (overlayBlockerDiagnostic) {
4240
+ (nextActions ??= []).push(...buildOverlayBlockerNextActions({ diagnostic: overlayBlockerDiagnostic, sessionName: executionPlan.sessionName }));
4241
+ }
4242
+ if (selectorTextVisibilityDiagnostics.length > 0) {
4243
+ (nextActions ??= []).push(...buildSelectorTextVisibilityNextActions({ diagnostics: selectorTextVisibilityDiagnostics, sessionName: executionPlan.sessionName }));
4244
+ }
3190
4245
  if (categoryDetails.failureCategory === "stale-ref" && redactedCompiledSemanticAction) {
3191
4246
  (nextActions ??= []).push({
3192
4247
  id: "retry-semantic-action-after-stale-ref",
@@ -3202,8 +4257,9 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
3202
4257
  compiledQaPreset: redactedCompiledQaPreset,
3203
4258
  compiledSourceLookup: redactedCompiledSourceLookup,
3204
4259
  compiledNetworkSourceLookup: redactedCompiledNetworkSourceLookup,
3205
- artifactManifest: presentation.artifactManifest,
3206
- artifactRetentionSummary: presentation.artifactRetentionSummary,
4260
+ artifactManifest: resultArtifactManifest,
4261
+ artifactRetentionSummary: presentation.artifactRetentionSummary ?? (resultArtifactManifest ? formatSessionArtifactRetentionSummary(resultArtifactManifest) : undefined),
4262
+ artifactCleanup,
3207
4263
  artifactVerification: presentation.artifactVerification,
3208
4264
  artifacts: presentation.artifacts,
3209
4265
  batchFailure: presentation.batchFailure,
@@ -3224,11 +4280,17 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
3224
4280
  fullOutputPath: parseFailureOutput.fullOutputPath ?? presentation.fullOutputPath,
3225
4281
  fullOutputPaths: presentation.fullOutputPaths,
3226
4282
  fullOutputUnavailable: parseFailureOutput.fullOutputUnavailable,
4283
+ managedSessionOutcome,
3227
4284
  imagePath: presentation.imagePath,
3228
4285
  imagePaths: presentation.imagePaths,
3229
4286
  nextActions,
3230
4287
  pageChangeSummary: presentation.pageChangeSummary,
4288
+ overlayBlockers: overlayBlockerDiagnostic,
3231
4289
  qaPreset,
4290
+ selectorTextVisibility: selectorTextVisibilityDiagnostics[0],
4291
+ selectorTextVisibilityAll: selectorTextVisibilityDiagnostics.length > 1 ? selectorTextVisibilityDiagnostics : undefined,
4292
+ evalStdinHint,
4293
+ timeoutPartialProgress,
3232
4294
  parseError: plainTextInspection ? undefined : parseError,
3233
4295
  savedFile: presentation.savedFile,
3234
4296
  savedFilePath: presentation.savedFilePath,
@@ -3237,6 +4299,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
3237
4299
  sessionMode,
3238
4300
  sessionTabCorrection,
3239
4301
  sessionTabTarget: currentSessionTabTarget,
4302
+ refSnapshot: currentRefSnapshot,
3240
4303
  ...buildSessionDetailFields(executionPlan.sessionName, executionPlan.usedImplicitSession),
3241
4304
  sessionRecoveryHint: redactedRecoveryHint,
3242
4305
  startupScopedFlags: executionPlan.startupScopedFlags,
@@ -3247,8 +4310,24 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
3247
4310
  timeoutMs: processResult.timeoutMs,
3248
4311
  };
3249
4312
 
4313
+ const semanticActionCandidateText = nextActions ? formatSemanticActionCandidateText(nextActions) : undefined;
4314
+ const overlayBlockerText = overlayBlockerDiagnostic ? formatOverlayBlockerText(overlayBlockerDiagnostic) : undefined;
4315
+ const selectorTextVisibilityText = formatSelectorTextVisibilityText(selectorTextVisibilityDiagnostics);
4316
+ const evalStdinHintText = formatEvalStdinHintText(evalStdinHint);
4317
+ const artifactCleanupText = formatArtifactCleanupGuidanceText(artifactCleanup);
4318
+ const timeoutPartialProgressText = timeoutPartialProgress ? formatTimeoutPartialProgressText(timeoutPartialProgress) : undefined;
4319
+ const managedSessionOutcomeText = formatManagedSessionOutcomeText(managedSessionOutcome);
4320
+ const rawAppendedDiagnosticText = [semanticActionCandidateText, overlayBlockerText, selectorTextVisibilityText, evalStdinHintText, artifactCleanupText, timeoutPartialProgressText, managedSessionOutcomeText].filter((item): item is string => item !== undefined).join("\n\n");
4321
+ const appendedDiagnosticText = redactSensitiveText(redactExactSensitiveText(rawAppendedDiagnosticText, exactSensitiveValues));
4322
+ const shouldAppendDiagnosticText = appendedDiagnosticText.length > 0 && (!userRequestedJson || plainTextInspection);
4323
+ const content = shouldAppendDiagnosticText && redactedContent[0]?.type === "text"
4324
+ ? [
4325
+ { ...redactedContent[0], text: `${redactedContent[0].text}\n\n${appendedDiagnosticText}` },
4326
+ ...redactedContent.slice(1),
4327
+ ]
4328
+ : redactedContent;
3250
4329
  const result = {
3251
- content: redactedContent,
4330
+ content,
3252
4331
  details: redactToolDetails(details, exactSensitiveValues),
3253
4332
  isError: !succeeded,
3254
4333
  };