pi-agent-browser-native 0.2.25 → 0.2.26

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.
@@ -34,6 +34,7 @@ import {
34
34
  parseAgentBrowserEnvelope,
35
35
  type AgentBrowserBatchResult,
36
36
  type AgentBrowserEnvelope,
37
+ type AgentBrowserNextAction,
37
38
  } from "./lib/results.js";
38
39
  import {
39
40
  buildExecutionPlan,
@@ -56,6 +57,7 @@ import {
56
57
  resolveManagedSessionState,
57
58
  shouldAppendBrowserSystemPrompt,
58
59
  validateToolArgs,
60
+ type CommandInfo,
59
61
  type CompatibilityWorkaround,
60
62
  type OpenResultTabCorrection,
61
63
  } from "./lib/runtime.js";
@@ -72,6 +74,7 @@ import {
72
74
  formatSessionArtifactRetentionSummary,
73
75
  isSessionArtifactManifest,
74
76
  mergeSessionArtifactManifest,
77
+ summarizeNetworkFailures,
75
78
  } from "./lib/results/shared.js";
76
79
 
77
80
  const DEFAULT_SESSION_MODE = "auto" as const;
@@ -99,6 +102,7 @@ interface AgentBrowserSemanticActionInput {
99
102
  text?: string;
100
103
  role?: string;
101
104
  name?: string;
105
+ session?: string;
102
106
  }
103
107
 
104
108
  interface CompiledAgentBrowserSemanticAction {
@@ -222,6 +226,7 @@ const AGENT_BROWSER_PARAMS = Type.Object({
222
226
  text: Type.Optional(Type.String({ description: "Text/value argument for fill or select actions." })),
223
227
  role: Type.Optional(Type.String({ description: "Role locator value; when set it must match value for locator=role." })),
224
228
  name: Type.Optional(Type.String({ description: "Accessible name filter for locator=role; compiles to --name <name>." })),
229
+ session: Type.Optional(Type.String({ description: "Optional upstream session name; prepends --session <name> before the compiled find command." })),
225
230
  }),
226
231
  ),
227
232
  qa: Type.Optional(
@@ -232,7 +237,7 @@ const AGENT_BROWSER_PARAMS = Type.Object({
232
237
  screenshotPath: Type.Optional(Type.String({ description: "Optional evidence screenshot path captured at the end of the QA preset." })),
233
238
  checkConsole: Type.Optional(Type.Boolean({ description: "Whether to fail on console error messages. Defaults to true." })),
234
239
  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." })),
240
+ checkNetwork: Type.Optional(Type.Boolean({ description: "Whether to inspect network requests and fail on actionable request failures; benign icon misses warn. Defaults to true." })),
236
241
  }),
237
242
  ),
238
243
  sourceLookup: Type.Optional(
@@ -366,6 +371,7 @@ interface AgentBrowserQaPresetAnalysis {
366
371
  failedChecks: string[];
367
372
  passed: boolean;
368
373
  summary: string;
374
+ warnings: string[];
369
375
  }
370
376
 
371
377
  function getBatchResultItems(data: unknown): Array<Record<string, unknown>> {
@@ -381,6 +387,7 @@ function analyzeQaPresetResults(data: unknown): AgentBrowserQaPresetAnalysis | u
381
387
  const items = getBatchResultItems(data);
382
388
  if (items.length === 0) return undefined;
383
389
  const failedChecks: string[] = [];
390
+ const warnings: string[] = [];
384
391
  for (const item of items) {
385
392
  if (item.success === false) {
386
393
  failedChecks.push(`${getCommandNameFromBatchItem(item) ?? "step"} failed`);
@@ -395,15 +402,20 @@ function analyzeQaPresetResults(data: unknown): AgentBrowserQaPresetAnalysis | u
395
402
  if (errorCount > 0) failedChecks.push(`${errorCount} console error message(s)`);
396
403
  }
397
404
  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)`);
405
+ const networkFailures = summarizeNetworkFailures(result.requests);
406
+ if (networkFailures.actionableCount > 0) failedChecks.push(`${networkFailures.actionableCount} actionable failed network request(s)`);
407
+ if (networkFailures.benignCount > 0) warnings.push(`${networkFailures.benignCount} benign network request failure(s) ignored`);
400
408
  }
401
409
  }
402
410
  const uniqueFailures = [...new Set(failedChecks)];
411
+ const uniqueWarnings = [...new Set(warnings)];
403
412
  return {
404
413
  failedChecks: uniqueFailures,
405
414
  passed: uniqueFailures.length === 0,
406
- summary: uniqueFailures.length === 0 ? "QA preset passed." : `QA preset failed: ${uniqueFailures.join("; ")}.`,
415
+ summary: uniqueFailures.length === 0
416
+ ? uniqueWarnings.length === 0 ? "QA preset passed." : `QA preset passed with warnings: ${uniqueWarnings.join("; ")}.`
417
+ : `QA preset failed: ${uniqueFailures.join("; ")}.`,
418
+ warnings: uniqueWarnings,
407
419
  };
408
420
  }
409
421
 
@@ -878,6 +890,85 @@ async function analyzeNetworkSourceLookupResults(data: unknown, compiled: Compil
878
890
  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
891
  }
880
892
 
893
+ function appendSemanticActionTextArg(args: string[], action: string, text: string | undefined): void {
894
+ if ((action === "fill" || action === "select") && text) {
895
+ args.push(text);
896
+ }
897
+ }
898
+
899
+ function getCompiledSemanticActionCommandIndex(compiled: CompiledAgentBrowserSemanticAction): number {
900
+ return compiled.args[0] === "--session" ? 2 : 0;
901
+ }
902
+
903
+ function getCompiledSemanticActionTextArg(compiled: CompiledAgentBrowserSemanticAction): string | undefined {
904
+ if (compiled.action !== "fill" && compiled.action !== "select") return undefined;
905
+ const commandIndex = getCompiledSemanticActionCommandIndex(compiled);
906
+ if (commandIndex < 0) return undefined;
907
+ const markerIndex = compiled.args.indexOf("--name");
908
+ return markerIndex >= 0 ? compiled.args[markerIndex - 1] : compiled.args[commandIndex + 4];
909
+ }
910
+
911
+ function getCompiledSemanticActionSessionPrefix(compiled: CompiledAgentBrowserSemanticAction): string[] {
912
+ const commandIndex = getCompiledSemanticActionCommandIndex(compiled);
913
+ return commandIndex > 0 ? compiled.args.slice(0, commandIndex) : [];
914
+ }
915
+
916
+ const SEMANTIC_ACTION_CANDIDATE_ACTION_IDS = new Set([
917
+ "try-searchbox-name-candidate",
918
+ "try-textbox-name-candidate",
919
+ "try-button-name-candidate",
920
+ "try-link-name-candidate",
921
+ "try-labeled-textbox-candidate",
922
+ ]);
923
+
924
+ function formatSemanticActionCandidateText(actions: AgentBrowserNextAction[]): string | undefined {
925
+ const candidateActions = actions.filter((action) => SEMANTIC_ACTION_CANDIDATE_ACTION_IDS.has(action.id) && action.params?.args);
926
+ if (candidateActions.length === 0) return undefined;
927
+ return [
928
+ "Agent-browser candidate fallbacks:",
929
+ ...candidateActions.map((action) => `- ${action.id}: agent_browser ${JSON.stringify({ args: action.params?.args })} — ${action.reason}`),
930
+ ].join("\n");
931
+ }
932
+
933
+ function buildSemanticActionCandidateActions(compiled: CompiledAgentBrowserSemanticAction): AgentBrowserNextAction[] {
934
+ const commandIndex = getCompiledSemanticActionCommandIndex(compiled);
935
+ if (commandIndex < 0) return [];
936
+ const locator = compiled.args[commandIndex + 1];
937
+ const value = compiled.args[commandIndex + 2];
938
+ if (!locator || !value) return [];
939
+ const text = getCompiledSemanticActionTextArg(compiled);
940
+ const sessionPrefix = getCompiledSemanticActionSessionPrefix(compiled);
941
+ const buildRoleCandidate = (role: string, id: string, reason: string): AgentBrowserNextAction => {
942
+ const args = [...sessionPrefix, "find", "role", role, compiled.action];
943
+ appendSemanticActionTextArg(args, compiled.action, text);
944
+ args.push("--name", value);
945
+ return {
946
+ id,
947
+ params: { args: redactInvocationArgs(args) },
948
+ reason,
949
+ safety: "Candidate locator fallback only; inspect the page if multiple elements could match the same accessible name.",
950
+ tool: "agent_browser" as const,
951
+ };
952
+ };
953
+
954
+ if (locator === "placeholder" && compiled.action === "fill") {
955
+ return [
956
+ buildRoleCandidate("searchbox", "try-searchbox-name-candidate", "Retry against a searchbox with the same accessible name; many search inputs expose names instead of placeholders."),
957
+ buildRoleCandidate("textbox", "try-textbox-name-candidate", "Retry against a textbox with the same accessible name when placeholder lookup misses."),
958
+ ];
959
+ }
960
+ if (locator === "text" && compiled.action === "click") {
961
+ return [
962
+ buildRoleCandidate("button", "try-button-name-candidate", "Retry against a button with the same accessible name when text lookup misses."),
963
+ buildRoleCandidate("link", "try-link-name-candidate", "Retry against a link with the same accessible name when text lookup misses."),
964
+ ];
965
+ }
966
+ if (locator === "label" && compiled.action === "fill") {
967
+ return [buildRoleCandidate("textbox", "try-labeled-textbox-candidate", "Retry against a textbox with the same accessible name when label lookup misses.")];
968
+ }
969
+ return [];
970
+ }
971
+
881
972
  function compileAgentBrowserSemanticAction(input: unknown): { compiled?: CompiledAgentBrowserSemanticAction; error?: string } {
882
973
  if (!isRecord(input)) {
883
974
  return { error: "semanticAction must be an object." };
@@ -888,6 +979,7 @@ function compileAgentBrowserSemanticAction(input: unknown): { compiled?: Compile
888
979
  const text = input.text;
889
980
  const role = input.role;
890
981
  const name = input.name;
982
+ const session = input.session;
891
983
  if (typeof action !== "string" || !AGENT_BROWSER_SEMANTIC_ACTIONS.includes(action as AgentBrowserSemanticActionName)) {
892
984
  return { error: `semanticAction.action must be one of: ${AGENT_BROWSER_SEMANTIC_ACTIONS.join(", ")}.` };
893
985
  }
@@ -912,7 +1004,10 @@ function compileAgentBrowserSemanticAction(input: unknown): { compiled?: Compile
912
1004
  if (name !== undefined && (locator !== "role" || typeof name !== "string" || name.length === 0)) {
913
1005
  return { error: "semanticAction.name is only supported as a non-empty string for locator=role." };
914
1006
  }
915
- const args = ["find", locator, value, action];
1007
+ if (session !== undefined && (typeof session !== "string" || session.trim().length === 0)) {
1008
+ return { error: "semanticAction.session must be a non-empty string when provided." };
1009
+ }
1010
+ const args = typeof session === "string" ? ["--session", session, "find", locator, value, action] : ["find", locator, value, action];
916
1011
  if (action === "fill" || action === "select") {
917
1012
  args.push(text as string);
918
1013
  }
@@ -1300,6 +1395,72 @@ interface NavigationSummary {
1300
1395
  url?: string;
1301
1396
  }
1302
1397
 
1398
+ interface OverlayBlockerCandidate {
1399
+ args: string[];
1400
+ name?: string;
1401
+ reason: string;
1402
+ ref: string;
1403
+ role?: string;
1404
+ }
1405
+
1406
+ interface OverlayBlockerDiagnostic {
1407
+ candidates: OverlayBlockerCandidate[];
1408
+ snapshot: SessionRefSnapshot;
1409
+ summary: string;
1410
+ }
1411
+
1412
+ interface SelectorTextVisibilityDiagnostic {
1413
+ firstMatchVisible?: boolean;
1414
+ firstVisibleTextPreview?: string;
1415
+ matchCount: number;
1416
+ selector: string;
1417
+ summary: string;
1418
+ visibleCount: number;
1419
+ }
1420
+
1421
+ interface TimeoutArtifactEvidence {
1422
+ absolutePath: string;
1423
+ exists: boolean;
1424
+ path: string;
1425
+ sizeBytes?: number;
1426
+ stepIndex: number;
1427
+ }
1428
+
1429
+ interface TimeoutPartialProgress {
1430
+ artifacts: TimeoutArtifactEvidence[];
1431
+ currentPage?: {
1432
+ title?: string;
1433
+ url?: string;
1434
+ };
1435
+ steps?: Array<{ args: string[]; index: number }>;
1436
+ summary: string;
1437
+ }
1438
+
1439
+ interface EvalStdinHint {
1440
+ reason: string;
1441
+ suggestion: string;
1442
+ }
1443
+
1444
+ interface ArtifactCleanupGuidance {
1445
+ explicitArtifactPaths: string[];
1446
+ note: string;
1447
+ owner: "host-file-tools";
1448
+ summary: string;
1449
+ }
1450
+
1451
+ interface ManagedSessionOutcome {
1452
+ activeAfter: boolean;
1453
+ activeBefore: boolean;
1454
+ attemptedSessionName?: string;
1455
+ currentSessionName: string;
1456
+ previousSessionName: string;
1457
+ replacedSessionName?: string;
1458
+ sessionMode: "auto" | "fresh";
1459
+ status: "abandoned" | "closed" | "created" | "preserved" | "replaced" | "unchanged";
1460
+ succeeded: boolean;
1461
+ summary: string;
1462
+ }
1463
+
1303
1464
  function isRecord(value: unknown): value is Record<string, unknown> {
1304
1465
  return typeof value === "object" && value !== null;
1305
1466
  }
@@ -1703,7 +1864,7 @@ function shouldCaptureNavigationSummary(command: string | undefined, data: unkno
1703
1864
  );
1704
1865
  }
1705
1866
 
1706
- function extractStringResultField(data: unknown, fieldName: "title" | "url"): string | undefined {
1867
+ function extractStringResultField(data: unknown, fieldName: "result" | "title" | "url"): string | undefined {
1707
1868
  if (typeof data === "string") {
1708
1869
  const text = data.trim();
1709
1870
  return text.length > 0 ? text : undefined;
@@ -1740,6 +1901,21 @@ interface OrderedSessionTabTarget {
1740
1901
  target: SessionTabTarget;
1741
1902
  }
1742
1903
 
1904
+ interface SessionRefSnapshot {
1905
+ refIds: string[];
1906
+ target?: SessionTabTarget;
1907
+ }
1908
+
1909
+ interface OrderedSessionRefSnapshot extends SessionRefSnapshot {
1910
+ order: number;
1911
+ }
1912
+
1913
+ interface StaleRefPreflight {
1914
+ message: string;
1915
+ refIds: string[];
1916
+ snapshot?: SessionRefSnapshot;
1917
+ }
1918
+
1743
1919
  interface AboutBlankSessionMismatch {
1744
1920
  activeUrl: "about:blank";
1745
1921
  recoveryApplied: boolean;
@@ -1748,7 +1924,7 @@ interface AboutBlankSessionMismatch {
1748
1924
  targetUrl: string;
1749
1925
  }
1750
1926
 
1751
- function getLatestSessionTabTargetOrder(targets: Map<string, OrderedSessionTabTarget>): number {
1927
+ function getLatestSessionTabTargetOrder(targets: Map<string, { order: number }>): number {
1752
1928
  let latestOrder = 0;
1753
1929
  for (const target of targets.values()) {
1754
1930
  latestOrder = Math.max(latestOrder, target.order);
@@ -1757,7 +1933,7 @@ function getLatestSessionTabTargetOrder(targets: Map<string, OrderedSessionTabTa
1757
1933
  }
1758
1934
 
1759
1935
  function shouldApplySessionTabTargetUpdate(options: {
1760
- current?: OrderedSessionTabTarget;
1936
+ current?: { order: number };
1761
1937
  updateOrder: number;
1762
1938
  }): boolean {
1763
1939
  return !options.current || options.updateOrder >= options.current.order;
@@ -1903,6 +2079,66 @@ function restoreSessionTabTargetsFromBranch(branch: unknown[]): Map<string, Orde
1903
2079
  return restoredTargets;
1904
2080
  }
1905
2081
 
2082
+ function extractRefSnapshotFromData(data: unknown): SessionRefSnapshot | undefined {
2083
+ if (!isRecord(data)) return undefined;
2084
+ const refIds = isRecord(data.refs) ? Object.keys(data.refs).filter((refId) => /^e\d+$/.test(refId)) : [];
2085
+ if (refIds.length === 0) return undefined;
2086
+ return {
2087
+ refIds,
2088
+ target: extractSessionTabTargetFromData(data),
2089
+ };
2090
+ }
2091
+
2092
+ function extractRefSnapshotFromBatchResults(data: unknown): SessionRefSnapshot | undefined {
2093
+ if (!Array.isArray(data)) return undefined;
2094
+ let latestSnapshot: SessionRefSnapshot | undefined;
2095
+ for (const item of data) {
2096
+ if (!isRecord(item) || item.success === false) continue;
2097
+ const [name] = extractBatchResultCommand(item);
2098
+ if (name !== "snapshot") continue;
2099
+ latestSnapshot = extractRefSnapshotFromData(item.result) ?? latestSnapshot;
2100
+ }
2101
+ return latestSnapshot;
2102
+ }
2103
+
2104
+ function restoreSessionRefSnapshotsFromBranch(branch: unknown[]): Map<string, OrderedSessionRefSnapshot> {
2105
+ const restoredSnapshots = new Map<string, OrderedSessionRefSnapshot>();
2106
+ let restoredOrder = 0;
2107
+ for (const entry of branch) {
2108
+ if (!isRecord(entry) || entry.type !== "message") continue;
2109
+ const message = isRecord(entry.message) ? entry.message : undefined;
2110
+ if (!message || message.toolName !== "agent_browser") continue;
2111
+ const details = isRecord(message.details) ? message.details : undefined;
2112
+ if (!details) continue;
2113
+ const sessionName = typeof details.sessionName === "string" ? details.sessionName : undefined;
2114
+ if (!sessionName) continue;
2115
+ const command = typeof details.command === "string" ? details.command : undefined;
2116
+ if (command === "close" && message.isError !== true) {
2117
+ restoredOrder += 1;
2118
+ restoredSnapshots.delete(sessionName);
2119
+ continue;
2120
+ }
2121
+ const refSnapshot = isRecord(details.refSnapshot)
2122
+ ? {
2123
+ refIds: Array.isArray(details.refSnapshot.refIds)
2124
+ ? details.refSnapshot.refIds.filter((refId): refId is string => typeof refId === "string" && /^e\d+$/.test(refId))
2125
+ : [],
2126
+ target: isRecord(details.refSnapshot.target)
2127
+ ? normalizeSessionTabTarget({
2128
+ title: typeof details.refSnapshot.target.title === "string" ? details.refSnapshot.target.title : undefined,
2129
+ url: typeof details.refSnapshot.target.url === "string" ? details.refSnapshot.target.url : undefined,
2130
+ })
2131
+ : undefined,
2132
+ }
2133
+ : undefined;
2134
+ if (refSnapshot && refSnapshot.refIds.length > 0) {
2135
+ restoredOrder += 1;
2136
+ restoredSnapshots.set(sessionName, { ...refSnapshot, order: restoredOrder });
2137
+ }
2138
+ }
2139
+ return restoredSnapshots;
2140
+ }
2141
+
1906
2142
  function restoreArtifactManifestFromBranch(branch: unknown[]): SessionArtifactManifest | undefined {
1907
2143
  let restoredManifest: SessionArtifactManifest | undefined;
1908
2144
  for (const entry of branch) {
@@ -2052,6 +2288,46 @@ function parseUserBatchStdin(stdin: string | undefined): { error?: string; steps
2052
2288
  }
2053
2289
  }
2054
2290
 
2291
+ const REF_INVALIDATING_BATCH_COMMANDS = new Set([
2292
+ "back",
2293
+ "check",
2294
+ "click",
2295
+ "dblclick",
2296
+ "drag",
2297
+ "fill",
2298
+ "forward",
2299
+ "goto",
2300
+ "keyboard",
2301
+ "mouse",
2302
+ "navigate",
2303
+ "open",
2304
+ "press",
2305
+ "reload",
2306
+ "select",
2307
+ "type",
2308
+ "uncheck",
2309
+ "upload",
2310
+ ]);
2311
+
2312
+ const REF_GUARDED_COMMANDS = new Set([
2313
+ "check",
2314
+ "click",
2315
+ "dblclick",
2316
+ "download",
2317
+ "drag",
2318
+ "fill",
2319
+ "focus",
2320
+ "hover",
2321
+ "keyboard",
2322
+ "mouse",
2323
+ "press",
2324
+ "scrollintoview",
2325
+ "select",
2326
+ "type",
2327
+ "uncheck",
2328
+ "upload",
2329
+ ]);
2330
+
2055
2331
  function getStaleRefArgs(commandTokens: string[], stdin?: string): string[] {
2056
2332
  if (commandTokens[0] !== "batch" || stdin === undefined) {
2057
2333
  return commandTokens;
@@ -2063,6 +2339,101 @@ function getStaleRefArgs(commandTokens: string[], stdin?: string): string[] {
2063
2339
  return parsed.steps.flatMap((step) => step);
2064
2340
  }
2065
2341
 
2342
+ function collectRefsFromTokens(tokens: string[]): string[] {
2343
+ return tokens.filter((token) => /^@e\d+\b/.test(token)).map((token) => token.slice(1));
2344
+ }
2345
+
2346
+ function getGuardedRefUsage(commandTokens: string[], stdin?: string): string[] {
2347
+ const collectFromStep = (step: string[]) => REF_GUARDED_COMMANDS.has(step[0] ?? "") ? collectRefsFromTokens(step) : [];
2348
+ if (commandTokens[0] !== "batch" || stdin === undefined) {
2349
+ return collectFromStep(commandTokens);
2350
+ }
2351
+ const parsed = parseUserBatchStdin(stdin);
2352
+ if (parsed.error || parsed.steps === undefined) {
2353
+ return collectFromStep(commandTokens);
2354
+ }
2355
+ const refsBeforeInBatchSnapshot: string[] = [];
2356
+ for (const step of parsed.steps) {
2357
+ if ((step[0] ?? "") === "snapshot") break;
2358
+ refsBeforeInBatchSnapshot.push(...collectFromStep(step));
2359
+ }
2360
+ return refsBeforeInBatchSnapshot;
2361
+ }
2362
+
2363
+ function targetsMatch(left: SessionTabTarget | undefined, right: SessionTabTarget | undefined): boolean {
2364
+ if (!left || !right) return true;
2365
+ return normalizeComparableUrl(left.url) === normalizeComparableUrl(right.url);
2366
+ }
2367
+
2368
+ function getBatchRefInvalidationMessage(commandTokens: string[], stdin?: string): string | undefined {
2369
+ if (commandTokens[0] !== "batch" || stdin === undefined) return undefined;
2370
+ const parsed = parseUserBatchStdin(stdin);
2371
+ if (parsed.error || parsed.steps === undefined) return undefined;
2372
+ let priorStepInvalidatesRefs = false;
2373
+ for (const step of parsed.steps) {
2374
+ if ((step[0] ?? "") === "snapshot") {
2375
+ priorStepInvalidatesRefs = false;
2376
+ }
2377
+ const refIds = collectRefsFromTokens(step);
2378
+ if (refIds.length > 0 && REF_GUARDED_COMMANDS.has(step[0] ?? "") && priorStepInvalidatesRefs) {
2379
+ 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.`;
2380
+ }
2381
+ if (REF_INVALIDATING_BATCH_COMMANDS.has(step[0] ?? "")) {
2382
+ priorStepInvalidatesRefs = true;
2383
+ }
2384
+ }
2385
+ return undefined;
2386
+ }
2387
+
2388
+ function buildStaleRefPreflight(options: {
2389
+ commandTokens: string[];
2390
+ currentTarget?: SessionTabTarget;
2391
+ refSnapshot?: SessionRefSnapshot;
2392
+ stdin?: string;
2393
+ }): StaleRefPreflight | undefined {
2394
+ const usedRefIds = [...new Set(getGuardedRefUsage(options.commandTokens, options.stdin))];
2395
+ const batchInvalidationMessage = getBatchRefInvalidationMessage(options.commandTokens, options.stdin);
2396
+ if (batchInvalidationMessage && usedRefIds.length > 0) {
2397
+ return {
2398
+ message: batchInvalidationMessage,
2399
+ refIds: usedRefIds,
2400
+ snapshot: options.refSnapshot,
2401
+ };
2402
+ }
2403
+ if (usedRefIds.length === 0 || !options.refSnapshot) return undefined;
2404
+ if (!targetsMatch(options.refSnapshot.target, options.currentTarget)) {
2405
+ return {
2406
+ 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.`,
2407
+ refIds: usedRefIds,
2408
+ snapshot: options.refSnapshot,
2409
+ };
2410
+ }
2411
+ const knownRefs = new Set(options.refSnapshot.refIds);
2412
+ const missingRefs = usedRefIds.filter((refId) => !knownRefs.has(refId));
2413
+ if (missingRefs.length > 0) {
2414
+ return {
2415
+ 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.`,
2416
+ refIds: missingRefs,
2417
+ snapshot: options.refSnapshot,
2418
+ };
2419
+ }
2420
+ return undefined;
2421
+ }
2422
+
2423
+ function sessionPrefixArgs(sessionName: string | undefined, args: string[]): string[] {
2424
+ return sessionName && args[0] !== "--session" ? ["--session", sessionName, ...args] : args;
2425
+ }
2426
+
2427
+ function sessionAwareStaleRefNextActions(sessionName: string | undefined): AgentBrowserNextAction[] {
2428
+ return (buildAgentBrowserNextActions({ failureCategory: "stale-ref", resultCategory: "failure" }) ?? []).map((action) => {
2429
+ const actionArgs = action.params?.args;
2430
+ return {
2431
+ ...action,
2432
+ params: action.params && actionArgs ? { ...action.params, args: sessionPrefixArgs(sessionName, actionArgs) } : action.params,
2433
+ };
2434
+ });
2435
+ }
2436
+
2066
2437
  function buildPinnedBatchPlan(options: {
2067
2438
  command?: string;
2068
2439
  commandTokens: string[];
@@ -2196,14 +2567,16 @@ async function runSessionCommandData(options: {
2196
2567
  cwd: string;
2197
2568
  sessionName?: string;
2198
2569
  signal?: AbortSignal;
2570
+ stdin?: string;
2199
2571
  }): Promise<unknown | undefined> {
2200
- const { args, cwd, sessionName, signal } = options;
2572
+ const { args, cwd, sessionName, signal, stdin } = options;
2201
2573
  if (!sessionName) return undefined;
2202
2574
 
2203
2575
  const processResult = await runAgentBrowserProcess({
2204
2576
  args: ["--json", "--session", sessionName, ...args],
2205
2577
  cwd,
2206
2578
  signal,
2579
+ stdin,
2207
2580
  });
2208
2581
  try {
2209
2582
  if (processResult.aborted || processResult.spawnError || processResult.exitCode !== 0) {
@@ -2249,6 +2622,401 @@ function mergeNavigationSummaryIntoData(data: unknown, navigationSummary: Naviga
2249
2622
  return { navigationSummary, result: data };
2250
2623
  }
2251
2624
 
2625
+ function getSnapshotRefRecord(data: unknown): Record<string, unknown> | undefined {
2626
+ return isRecord(data) && isRecord(data.refs) ? data.refs : undefined;
2627
+ }
2628
+
2629
+ const OVERLAY_CLOSE_NAME_PATTERN = /(?:\b(?:close|dismiss|no thanks|not now|maybe later|hide|skip|continue without|x)\b|^\s*×\s*$)/i;
2630
+ const OVERLAY_CONTEXT_NAME_PATTERN = /\b(?:banner|modal|dialog|popup|pop-up|overlay|donat(?:e|ion)|subscribe|sign in|login|cookie|privacy|consent)\b/i;
2631
+ const OVERLAY_CONTEXT_ROLES = new Set(["alertdialog", "dialog"]);
2632
+ const OVERLAY_ACTION_ROLES = new Set(["button", "link", "menuitem"]);
2633
+ const OVERLAY_BLOCKER_CANDIDATE_LIMIT = 3;
2634
+
2635
+ function getOverlayBlockerCandidates(snapshotData: unknown): OverlayBlockerCandidate[] {
2636
+ const refs = getSnapshotRefRecord(snapshotData);
2637
+ if (!refs) return [];
2638
+ const hasOverlayContext = Object.values(refs).some((entry) => {
2639
+ if (!isRecord(entry)) return false;
2640
+ const role = typeof entry.role === "string" ? entry.role : "";
2641
+ const name = typeof entry.name === "string" ? entry.name : "";
2642
+ return OVERLAY_CONTEXT_ROLES.has(role.toLowerCase()) || OVERLAY_CONTEXT_NAME_PATTERN.test(name);
2643
+ });
2644
+ if (!hasOverlayContext) return [];
2645
+ const candidates: OverlayBlockerCandidate[] = [];
2646
+ for (const [ref, entry] of Object.entries(refs)) {
2647
+ if (!/^e\d+$/.test(ref) || !isRecord(entry)) continue;
2648
+ const role = typeof entry.role === "string" ? entry.role : undefined;
2649
+ const name = typeof entry.name === "string" ? entry.name : undefined;
2650
+ if (!role || !OVERLAY_ACTION_ROLES.has(role.toLowerCase()) || !name || !OVERLAY_CLOSE_NAME_PATTERN.test(name)) continue;
2651
+ candidates.push({
2652
+ args: ["click", `@${ref}`],
2653
+ name,
2654
+ reason: `Visible ${role} ${JSON.stringify(name)} appears in a snapshot that also contains overlay/banner/dialog context.`,
2655
+ ref: `@${ref}`,
2656
+ role,
2657
+ });
2658
+ if (candidates.length >= OVERLAY_BLOCKER_CANDIDATE_LIMIT) break;
2659
+ }
2660
+ return candidates;
2661
+ }
2662
+
2663
+ function formatOverlayBlockerText(diagnostic: OverlayBlockerDiagnostic): string {
2664
+ return [
2665
+ "Possible overlay blockers:",
2666
+ ...diagnostic.candidates.map((candidate) => `- ${candidate.ref}${candidate.role ? ` ${candidate.role}` : ""}${candidate.name ? ` ${JSON.stringify(candidate.name)}` : ""}: ${candidate.reason}`),
2667
+ ].join("\n");
2668
+ }
2669
+
2670
+ function buildOverlayBlockerNextActions(options: { diagnostic: OverlayBlockerDiagnostic; sessionName?: string }): AgentBrowserNextAction[] {
2671
+ return [
2672
+ {
2673
+ id: "inspect-overlay-state",
2674
+ params: { args: sessionPrefixArgs(options.sessionName, ["snapshot", "-i"]) },
2675
+ reason: "Refresh interactive refs and inspect whether an overlay, banner, modal, or dialog is blocking the intended click.",
2676
+ safety: "Read-only inspection; use current refs from this snapshot before interacting.",
2677
+ tool: "agent_browser" as const,
2678
+ },
2679
+ ...options.diagnostic.candidates.map((candidate, index) => ({
2680
+ id: `try-overlay-blocker-candidate-${index + 1}`,
2681
+ params: { args: sessionPrefixArgs(options.sessionName, candidate.args) },
2682
+ reason: candidate.reason,
2683
+ safety: "Only click this if the candidate is clearly a close/dismiss control for an overlay that blocks the intended workflow.",
2684
+ tool: "agent_browser" as const,
2685
+ })),
2686
+ ];
2687
+ }
2688
+
2689
+ function buildVisibleTextProbeScript(selector: string): string {
2690
+ 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})()`;
2691
+ }
2692
+
2693
+ function parseSelectorTextVisibilityProbe(data: unknown, selector: string): Omit<SelectorTextVisibilityDiagnostic, "summary"> | undefined {
2694
+ const result = extractStringResultField(data, "result");
2695
+ if (!result) return undefined;
2696
+ let parsed: unknown;
2697
+ try {
2698
+ parsed = JSON.parse(result);
2699
+ } catch {
2700
+ return undefined;
2701
+ }
2702
+ if (!isRecord(parsed) || typeof parsed.error === "string") return undefined;
2703
+ const matchCount = typeof parsed.matchCount === "number" ? parsed.matchCount : undefined;
2704
+ const visibleCount = typeof parsed.visibleCount === "number" ? parsed.visibleCount : undefined;
2705
+ if (matchCount === undefined || visibleCount === undefined) return undefined;
2706
+ return {
2707
+ firstMatchVisible: typeof parsed.firstMatchVisible === "boolean" ? parsed.firstMatchVisible : undefined,
2708
+ firstVisibleTextPreview: typeof parsed.firstVisibleTextPreview === "string" && parsed.firstVisibleTextPreview.length > 0 ? redactSensitiveText(parsed.firstVisibleTextPreview) : undefined,
2709
+ matchCount,
2710
+ selector,
2711
+ visibleCount,
2712
+ };
2713
+ }
2714
+
2715
+ function selectorMayExposeSensitiveLiteral(selector: string): boolean {
2716
+ return redactSensitiveText(selector) !== selector || /\[[^\]]*[~|^$*]?=\s*(?:"[^"]*"|'[^']*'|[^\]\s]+)\s*(?:[is]\s*)?\]/.test(selector);
2717
+ }
2718
+
2719
+ async function collectSelectorTextVisibilityDiagnosticForSelector(options: {
2720
+ cwd: string;
2721
+ selector: string | undefined;
2722
+ sessionName?: string;
2723
+ signal?: AbortSignal;
2724
+ }): Promise<SelectorTextVisibilityDiagnostic | undefined> {
2725
+ const { selector } = options;
2726
+ if (!selector || /^@e\d+$/.test(selector) || selectorMayExposeSensitiveLiteral(selector)) return undefined;
2727
+ const probe = await runSessionCommandData({
2728
+ args: ["eval", "--stdin"],
2729
+ cwd: options.cwd,
2730
+ sessionName: options.sessionName,
2731
+ signal: options.signal,
2732
+ stdin: buildVisibleTextProbeScript(selector),
2733
+ });
2734
+ const parsed = parseSelectorTextVisibilityProbe(probe, selector);
2735
+ if (!parsed || parsed.matchCount <= 1 && parsed.firstMatchVisible !== false) return undefined;
2736
+ if (parsed.visibleCount === 0) return undefined;
2737
+ const visibleMatchNoun = `visible match${parsed.visibleCount === 1 ? "" : "es"}`;
2738
+ const visibleMatchVerb = parsed.visibleCount === 1 ? "exists" : "exist";
2739
+ const summary = parsed.firstMatchVisible === false
2740
+ ? `Selector ${JSON.stringify(selector)} matched ${parsed.matchCount} elements; the first match is hidden while ${parsed.visibleCount} ${visibleMatchNoun} ${visibleMatchVerb}.`
2741
+ : `Selector ${JSON.stringify(selector)} matched ${parsed.matchCount} elements; get text reads the first upstream match, which may not be the intended visible tab/panel.`;
2742
+ return { ...parsed, summary };
2743
+ }
2744
+
2745
+ function getBatchGetTextSelectors(data: unknown): string[] {
2746
+ if (!Array.isArray(data)) return [];
2747
+ return data.flatMap((item) => {
2748
+ if (!isRecord(item) || item.success === false) return [];
2749
+ const [command, subcommand, selector] = extractBatchResultCommand(item);
2750
+ return command === "get" && subcommand === "text" && selector ? [selector] : [];
2751
+ });
2752
+ }
2753
+
2754
+ async function collectSelectorTextVisibilityDiagnostics(options: {
2755
+ commandInfo: CommandInfo;
2756
+ commandTokens: string[];
2757
+ cwd: string;
2758
+ data: unknown;
2759
+ sessionName?: string;
2760
+ signal?: AbortSignal;
2761
+ }): Promise<SelectorTextVisibilityDiagnostic[]> {
2762
+ const selectors = options.commandInfo.command === "get" && options.commandInfo.subcommand === "text"
2763
+ ? [options.commandTokens[2]]
2764
+ : options.commandInfo.command === "batch"
2765
+ ? getBatchGetTextSelectors(options.data)
2766
+ : [];
2767
+ const diagnostics: SelectorTextVisibilityDiagnostic[] = [];
2768
+ for (const selector of selectors) {
2769
+ const diagnostic = await collectSelectorTextVisibilityDiagnosticForSelector({
2770
+ cwd: options.cwd,
2771
+ selector,
2772
+ sessionName: options.sessionName,
2773
+ signal: options.signal,
2774
+ });
2775
+ if (diagnostic) diagnostics.push(diagnostic);
2776
+ }
2777
+ return diagnostics.sort((left, right) => Number(right.firstMatchVisible === false) - Number(left.firstMatchVisible === false));
2778
+ }
2779
+
2780
+ function formatSelectorTextVisibilityText(diagnostics: SelectorTextVisibilityDiagnostic[]): string | undefined {
2781
+ if (diagnostics.length === 0) return undefined;
2782
+ return diagnostics.flatMap((diagnostic) => {
2783
+ const lines = [`Selector text visibility warning: ${diagnostic.summary}`];
2784
+ if (diagnostic.firstVisibleTextPreview) lines.push(`First visible text preview: ${JSON.stringify(diagnostic.firstVisibleTextPreview)}`);
2785
+ return lines;
2786
+ }).join("\n");
2787
+ }
2788
+
2789
+ function looksLikeFunctionEvalStdin(stdin: string | undefined): boolean {
2790
+ const trimmed = stdin?.trim();
2791
+ if (!trimmed) return false;
2792
+ return /^(?:async\s+)?function\b/.test(trimmed) || /^(?:async\s*)?\([^)]*\)\s*=>/.test(trimmed) || /^(?:async\s+)?[A-Za-z_$][\w$]*\s*=>/.test(trimmed);
2793
+ }
2794
+
2795
+ function isEmptyRecord(value: unknown): boolean {
2796
+ return isRecord(value) && Object.keys(value).length === 0;
2797
+ }
2798
+
2799
+ function getEvalStdinHint(options: { command?: string; data: unknown; stdin?: string }): EvalStdinHint | undefined {
2800
+ if (options.command !== "eval" || !looksLikeFunctionEvalStdin(options.stdin) || !isRecord(options.data)) return undefined;
2801
+ const result = options.data.result;
2802
+ if (!isEmptyRecord(result)) return undefined;
2803
+ return {
2804
+ 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.",
2805
+ suggestion: "Pass a plain expression such as `({ title: document.title })`, or invoke the function explicitly, for example `(() => ({ title: document.title }))()`.",
2806
+ };
2807
+ }
2808
+
2809
+ function formatEvalStdinHintText(hint: EvalStdinHint | undefined): string | undefined {
2810
+ return hint ? `Eval stdin hint: ${hint.reason} ${hint.suggestion}` : undefined;
2811
+ }
2812
+
2813
+ function getArtifactCleanupGuidance(options: { command?: string; manifest?: SessionArtifactManifest; succeeded: boolean }): ArtifactCleanupGuidance | undefined {
2814
+ if (!options.succeeded || options.command !== "close" || !options.manifest || options.manifest.entries.length === 0) return undefined;
2815
+ const explicitArtifactPaths = options.manifest.entries
2816
+ .filter((entry) => entry.storageScope === "explicit-path")
2817
+ .map((entry) => entry.path)
2818
+ .filter((path, index, paths) => paths.indexOf(path) === index)
2819
+ .slice(0, 10);
2820
+ return {
2821
+ explicitArtifactPaths,
2822
+ note: "Closing the browser session does not delete explicit screenshots, downloads, PDFs, traces, HAR files, or recordings; clean those paths with host file tools when no longer needed.",
2823
+ owner: "host-file-tools",
2824
+ summary: formatSessionArtifactRetentionSummary(options.manifest),
2825
+ };
2826
+ }
2827
+
2828
+ function formatArtifactCleanupGuidanceText(guidance: ArtifactCleanupGuidance | undefined): string | undefined {
2829
+ if (!guidance) return undefined;
2830
+ const lines = [
2831
+ "Artifact lifecycle:",
2832
+ `- ${guidance.summary}`,
2833
+ `- ${guidance.note}`,
2834
+ ];
2835
+ if (guidance.explicitArtifactPaths.length > 0) {
2836
+ lines.push(`- Explicit artifact paths to review: ${guidance.explicitArtifactPaths.join(", ")}`);
2837
+ }
2838
+ return lines.join("\n");
2839
+ }
2840
+
2841
+ function buildSelectorTextVisibilityNextActions(options: { diagnostics: SelectorTextVisibilityDiagnostic[]; sessionName?: string }): AgentBrowserNextAction[] {
2842
+ return options.diagnostics.map((diagnostic, index) => ({
2843
+ id: index === 0 ? "inspect-visible-text-candidates" : `inspect-visible-text-candidates-${index + 1}`,
2844
+ params: {
2845
+ args: sessionPrefixArgs(options.sessionName, ["eval", "--stdin"]),
2846
+ stdin: buildVisibleTextProbeScript(diagnostic.selector),
2847
+ },
2848
+ reason: "Inspect selector match count and visible text before trusting get text on tabbed or hidden DOM content.",
2849
+ safety: "Read-only DOM inspection; use a more specific visible selector or current @ref before acting on hidden-tab text.",
2850
+ tool: "agent_browser" as const,
2851
+ }));
2852
+ }
2853
+
2854
+ function getTimeoutProgressSteps(compiledJob: CompiledAgentBrowserJob | undefined, command: string | undefined, stdin: string | undefined): Array<{ args: string[]; index: number }> {
2855
+ if (compiledJob) return compiledJob.steps.map((step, index) => ({ args: step.args, index: index + 1 }));
2856
+ if (command !== "batch" || !stdin) return [];
2857
+ try {
2858
+ const parsed = JSON.parse(stdin) as unknown;
2859
+ if (!Array.isArray(parsed)) return [];
2860
+ return parsed.flatMap((step, index) => Array.isArray(step) && step.every((token) => typeof token === "string") ? [{ args: step as string[], index: index + 1 }] : []);
2861
+ } catch {
2862
+ return [];
2863
+ }
2864
+ }
2865
+
2866
+ function getLastPositionalToken(args: string[], startIndex = 1): string | undefined {
2867
+ for (let index = args.length - 1; index >= startIndex; index -= 1) {
2868
+ const token = args[index];
2869
+ if (token && !token.startsWith("-")) return token;
2870
+ }
2871
+ return undefined;
2872
+ }
2873
+
2874
+ function getTimeoutStepArtifactPath(args: string[]): string | undefined {
2875
+ const [command] = args;
2876
+ if (command === "screenshot") {
2877
+ const index = getScreenshotPathTokenIndex(args);
2878
+ return index === undefined ? undefined : args[index];
2879
+ }
2880
+ if (command === "pdf") return getLastPositionalToken(args);
2881
+ if (command === "download") return getLastPositionalToken(args, 2);
2882
+ if (command === "wait") {
2883
+ const inlineDownload = args.find((token) => token.startsWith("--download="));
2884
+ if (inlineDownload) return inlineDownload.slice("--download=".length) || undefined;
2885
+ const downloadIndex = args.indexOf("--download");
2886
+ const downloadPath = downloadIndex >= 0 ? args[downloadIndex + 1] : undefined;
2887
+ if (downloadPath && !downloadPath.startsWith("-")) return downloadPath;
2888
+ }
2889
+ return undefined;
2890
+ }
2891
+
2892
+ async function collectTimeoutArtifactEvidence(cwd: string, steps: Array<{ args: string[]; index: number }>): Promise<TimeoutArtifactEvidence[]> {
2893
+ const evidence: TimeoutArtifactEvidence[] = [];
2894
+ for (const step of steps) {
2895
+ const path = getTimeoutStepArtifactPath(step.args);
2896
+ if (!path) continue;
2897
+ const absolutePath = isAbsolute(path) ? path : resolve(cwd, path);
2898
+ try {
2899
+ const stats = await stat(absolutePath);
2900
+ evidence.push({ absolutePath, exists: true, path, sizeBytes: stats.size, stepIndex: step.index });
2901
+ } catch {
2902
+ evidence.push({ absolutePath, exists: false, path, stepIndex: step.index });
2903
+ }
2904
+ }
2905
+ return evidence;
2906
+ }
2907
+
2908
+ function getPlannedCurrentPageUrl(steps: Array<{ args: string[]; index: number }>): string | undefined {
2909
+ for (let index = steps.length - 1; index >= 0; index -= 1) {
2910
+ const args = steps[index]?.args ?? [];
2911
+ if (args[0] === "open" || args[0] === "navigate" || args[0] === "pushstate") {
2912
+ return getLastPositionalToken(args);
2913
+ }
2914
+ }
2915
+ return undefined;
2916
+ }
2917
+
2918
+ async function collectTimeoutPartialProgress(options: {
2919
+ command?: string;
2920
+ compiledJob?: CompiledAgentBrowserJob;
2921
+ cwd: string;
2922
+ sessionName?: string;
2923
+ stdin?: string;
2924
+ }): Promise<TimeoutPartialProgress | undefined> {
2925
+ const steps = getTimeoutProgressSteps(options.compiledJob, options.command, options.stdin);
2926
+ const artifacts = await collectTimeoutArtifactEvidence(options.cwd, steps);
2927
+ const [urlData, titleData] = await Promise.all([
2928
+ runSessionCommandData({ args: ["get", "url"], cwd: options.cwd, sessionName: options.sessionName }),
2929
+ runSessionCommandData({ args: ["get", "title"], cwd: options.cwd, sessionName: options.sessionName }),
2930
+ ]);
2931
+ const recoveredUrl = extractStringResultField(urlData, "result") ?? extractStringResultField(urlData, "url");
2932
+ const title = extractStringResultField(titleData, "result") ?? extractStringResultField(titleData, "title");
2933
+ const plannedUrl = recoveredUrl ? undefined : getPlannedCurrentPageUrl(steps);
2934
+ const url = recoveredUrl ?? plannedUrl;
2935
+ if (steps.length === 0 && artifacts.length === 0 && !url && !title) return undefined;
2936
+ const foundArtifacts = artifacts.filter((artifact) => artifact.exists).length;
2937
+ const pageStateSummary = recoveredUrl || title ? " and current page state" : plannedUrl ? " and planned page URL" : "";
2938
+ return {
2939
+ artifacts,
2940
+ currentPage: url || title ? { title, url } : undefined,
2941
+ steps: steps.length > 0 ? steps : undefined,
2942
+ summary: `Timed out before upstream returned final results; recovered ${foundArtifacts}/${artifacts.length} declared artifact path${artifacts.length === 1 ? "" : "s"}${pageStateSummary}.`,
2943
+ };
2944
+ }
2945
+
2946
+ function redactSensitivePathSegmentsForDiagnostic(path: string): string {
2947
+ return path.split(/([/\\]+)/).map((segment) => {
2948
+ if (segment === "/" || segment === "\\" || /^[/\\]+$/.test(segment)) return segment;
2949
+ return redactSensitiveText(segment) !== segment || /(?:secret|token|password|passwd|credential|auth|api[-_]?key|bearer)/i.test(segment) ? "[REDACTED]" : segment;
2950
+ }).join("");
2951
+ }
2952
+
2953
+ function sanitizeCurrentPageUrlForTimeoutDiagnostic(url: string): string {
2954
+ try {
2955
+ const parsedUrl = new URL(url);
2956
+ parsedUrl.pathname = parsedUrl.pathname.split("/").map((segment) => redactSensitivePathSegmentsForDiagnostic(segment)).join("/");
2957
+ for (const [key, value] of parsedUrl.searchParams.entries()) {
2958
+ if (redactSensitiveText(key) !== key || redactSensitiveText(value) !== value || /(?:secret|token|password|passwd|credential|auth|api[-_]?key|bearer)/i.test(`${key} ${value}`)) {
2959
+ parsedUrl.searchParams.set(key, "[REDACTED]");
2960
+ }
2961
+ }
2962
+ if (parsedUrl.hash) {
2963
+ parsedUrl.hash = redactSensitivePathSegmentsForDiagnostic(redactSensitiveText(parsedUrl.hash));
2964
+ }
2965
+ return redactSensitiveText(parsedUrl.toString());
2966
+ } catch {
2967
+ return redactSensitivePathSegmentsForDiagnostic(redactSensitiveText(url));
2968
+ }
2969
+ }
2970
+
2971
+ function formatTimeoutPartialProgressText(progress: TimeoutPartialProgress): string {
2972
+ const lines = [`Timeout partial progress: ${progress.summary}`];
2973
+ const currentPageTitle = progress.currentPage?.title ? redactSensitivePathSegmentsForDiagnostic(redactSensitiveText(progress.currentPage.title)) : undefined;
2974
+ const currentPageUrl = progress.currentPage?.url ? sanitizeCurrentPageUrlForTimeoutDiagnostic(progress.currentPage.url) : undefined;
2975
+ if (currentPageTitle || currentPageUrl) {
2976
+ lines.push(`Current page: ${[currentPageTitle, currentPageUrl].filter(Boolean).join(" — ")}`);
2977
+ }
2978
+ if (progress.steps && progress.steps.length > 0) {
2979
+ const shownSteps = progress.steps.slice(0, 6);
2980
+ lines.push("Planned steps:");
2981
+ for (const step of shownSteps) {
2982
+ const command = redactSensitivePathSegmentsForDiagnostic(redactInvocationArgs(step.args).join(" "));
2983
+ lines.push(`- Step ${step.index}: ${command}`);
2984
+ }
2985
+ if (progress.steps.length > shownSteps.length) {
2986
+ lines.push(`- ... ${progress.steps.length - shownSteps.length} more step${progress.steps.length - shownSteps.length === 1 ? "" : "s"} omitted`);
2987
+ }
2988
+ }
2989
+ for (const artifact of progress.artifacts) {
2990
+ const path = redactSensitivePathSegmentsForDiagnostic(artifact.path);
2991
+ lines.push(`Artifact from step ${artifact.stepIndex}: ${path} (${artifact.exists ? `exists${typeof artifact.sizeBytes === "number" ? `, ${artifact.sizeBytes} bytes` : ""}` : "missing"})`);
2992
+ }
2993
+ return lines.join("\n");
2994
+ }
2995
+
2996
+ async function collectOverlayBlockerDiagnostic(options: {
2997
+ command?: string;
2998
+ cwd: string;
2999
+ data: unknown;
3000
+ navigationSummary?: NavigationSummary;
3001
+ priorTarget?: SessionTabTarget;
3002
+ sessionName?: string;
3003
+ signal?: AbortSignal;
3004
+ }): Promise<OverlayBlockerDiagnostic | undefined> {
3005
+ if (options.command !== "click" || !isRecord(options.data) || typeof options.data.clicked !== "string") return undefined;
3006
+ const priorUrl = normalizeComparableUrl(options.priorTarget?.url);
3007
+ const currentUrl = normalizeComparableUrl(options.navigationSummary?.url);
3008
+ if (!priorUrl || !currentUrl || priorUrl !== currentUrl) return undefined;
3009
+ const snapshotData = await runSessionCommandData({ args: ["snapshot", "-i"], cwd: options.cwd, sessionName: options.sessionName, signal: options.signal });
3010
+ const candidates = getOverlayBlockerCandidates(snapshotData);
3011
+ const snapshot = extractRefSnapshotFromData(snapshotData);
3012
+ if (candidates.length === 0 || !snapshot) return undefined;
3013
+ return {
3014
+ candidates,
3015
+ snapshot,
3016
+ summary: `Click completed but the page stayed on ${currentUrl}; a fresh snapshot contains likely overlay close/dismiss controls.`,
3017
+ };
3018
+ }
3019
+
2252
3020
  async function collectOpenResultTabCorrection(options: {
2253
3021
  cwd: string;
2254
3022
  sessionName?: string;
@@ -2314,6 +3082,68 @@ function buildSessionDetailFields(sessionName: string | undefined, usedImplicitS
2314
3082
  return sessionName ? { sessionName, usedImplicitSession } : {};
2315
3083
  }
2316
3084
 
3085
+ function buildManagedSessionOutcome(options: {
3086
+ activeAfter: boolean;
3087
+ activeBefore: boolean;
3088
+ attemptedSessionName?: string;
3089
+ command?: string;
3090
+ currentSessionName: string;
3091
+ previousSessionName: string;
3092
+ replacedSessionName?: string;
3093
+ sessionMode: "auto" | "fresh";
3094
+ succeeded: boolean;
3095
+ }): ManagedSessionOutcome | undefined {
3096
+ const { activeAfter, activeBefore, attemptedSessionName, command, currentSessionName, previousSessionName, replacedSessionName, sessionMode, succeeded } = options;
3097
+ if (!attemptedSessionName) return undefined;
3098
+ let status: ManagedSessionOutcome["status"];
3099
+ let summary: string;
3100
+ if (command === "close") {
3101
+ status = succeeded ? "closed" : activeBefore ? "preserved" : "abandoned";
3102
+ summary = succeeded
3103
+ ? `Managed session ${attemptedSessionName} was closed.`
3104
+ : activeBefore
3105
+ ? `Managed session close failed; previous managed session ${previousSessionName} remains current.`
3106
+ : `Managed session close failed; no managed session is active.`;
3107
+ } else if (succeeded) {
3108
+ if (replacedSessionName) {
3109
+ status = "replaced";
3110
+ summary = `Managed session ${replacedSessionName} was replaced by ${currentSessionName}.`;
3111
+ } else if (!activeBefore && activeAfter) {
3112
+ status = "created";
3113
+ summary = `Managed session ${currentSessionName} is now current.`;
3114
+ } else {
3115
+ status = "unchanged";
3116
+ summary = `Managed session ${currentSessionName} remains current.`;
3117
+ }
3118
+ } else if (activeBefore) {
3119
+ status = "preserved";
3120
+ summary = sessionMode === "fresh" && attemptedSessionName !== previousSessionName
3121
+ ? `Fresh managed session ${attemptedSessionName} failed before becoming current; previous managed session ${previousSessionName} was preserved.`
3122
+ : `Managed session call failed; previous managed session ${previousSessionName} was preserved.`;
3123
+ } else {
3124
+ status = "abandoned";
3125
+ summary = sessionMode === "fresh"
3126
+ ? `Fresh managed session ${attemptedSessionName} failed before becoming current; no previous managed session was active, so no managed session is current.`
3127
+ : `Managed session call failed before any managed session became current.`;
3128
+ }
3129
+ return {
3130
+ activeAfter,
3131
+ activeBefore,
3132
+ attemptedSessionName,
3133
+ currentSessionName,
3134
+ previousSessionName,
3135
+ replacedSessionName,
3136
+ sessionMode,
3137
+ status,
3138
+ succeeded,
3139
+ summary,
3140
+ };
3141
+ }
3142
+
3143
+ function formatManagedSessionOutcomeText(outcome: ManagedSessionOutcome | undefined): string | undefined {
3144
+ return outcome && !outcome.succeeded && outcome.sessionMode === "fresh" ? `Managed session outcome: ${outcome.summary}` : undefined;
3145
+ }
3146
+
2317
3147
  function getPersistentSessionArtifactStore(ctx: {
2318
3148
  sessionManager: {
2319
3149
  getSessionDir?: () => string;
@@ -2465,6 +3295,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
2465
3295
  let managedSessionCwd = process.cwd();
2466
3296
  let freshSessionOrdinal = 0;
2467
3297
  let sessionTabTargets = new Map<string, OrderedSessionTabTarget>();
3298
+ let sessionRefSnapshots = new Map<string, OrderedSessionRefSnapshot>();
2468
3299
  let sessionTabTargetUpdateOrder = 0;
2469
3300
  let traceOwners = new Map<string, TraceOwner>();
2470
3301
  let artifactManifest: SessionArtifactManifest | undefined;
@@ -2478,7 +3309,8 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
2478
3309
  managedSessionCwd = ctx.cwd;
2479
3310
  freshSessionOrdinal = restoredState.freshSessionOrdinal;
2480
3311
  sessionTabTargets = restoreSessionTabTargetsFromBranch(ctx.sessionManager.getBranch());
2481
- sessionTabTargetUpdateOrder = getLatestSessionTabTargetOrder(sessionTabTargets);
3312
+ sessionRefSnapshots = restoreSessionRefSnapshotsFromBranch(ctx.sessionManager.getBranch());
3313
+ sessionTabTargetUpdateOrder = Math.max(getLatestSessionTabTargetOrder(sessionTabTargets), getLatestSessionTabTargetOrder(sessionRefSnapshots));
2482
3314
  artifactManifest = restoreArtifactManifestFromBranch(ctx.sessionManager.getBranch());
2483
3315
  });
2484
3316
 
@@ -2495,6 +3327,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
2495
3327
  }
2496
3328
  managedSessionActive = false;
2497
3329
  sessionTabTargets = new Map<string, OrderedSessionTabTarget>();
3330
+ sessionRefSnapshots = new Map<string, OrderedSessionRefSnapshot>();
2498
3331
  sessionTabTargetUpdateOrder = 0;
2499
3332
  traceOwners = new Map<string, TraceOwner>();
2500
3333
  artifactManifest = undefined;
@@ -2726,6 +3559,31 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
2726
3559
 
2727
3560
  const priorSessionTabTargetState = executionPlan.sessionName ? sessionTabTargets.get(executionPlan.sessionName) : undefined;
2728
3561
  const priorSessionTabTarget = priorSessionTabTargetState?.target;
3562
+ const priorRefSnapshotState = executionPlan.sessionName ? sessionRefSnapshots.get(executionPlan.sessionName) : undefined;
3563
+ const staleRefPreflight = buildStaleRefPreflight({
3564
+ commandTokens,
3565
+ currentTarget: priorSessionTabTarget,
3566
+ refSnapshot: priorRefSnapshotState,
3567
+ stdin: toolStdin,
3568
+ });
3569
+ if (staleRefPreflight) {
3570
+ return {
3571
+ content: [{ type: "text", text: staleRefPreflight.message }],
3572
+ details: {
3573
+ args: redactedArgs,
3574
+ command: executionPlan.commandInfo.command,
3575
+ compatibilityWorkaround,
3576
+ effectiveArgs: redactedEffectiveArgs,
3577
+ nextActions: sessionAwareStaleRefNextActions(executionPlan.sessionName),
3578
+ refIds: staleRefPreflight.refIds,
3579
+ refSnapshot: staleRefPreflight.snapshot,
3580
+ sessionMode,
3581
+ ...buildAgentBrowserResultCategoryDetails({ args: redactedEffectiveArgs, command: executionPlan.commandInfo.command, errorText: staleRefPreflight.message, failureCategory: "stale-ref", succeeded: false }),
3582
+ ...buildSessionDetailFields(executionPlan.sessionName, executionPlan.usedImplicitSession),
3583
+ },
3584
+ isError: true,
3585
+ };
3586
+ }
2729
3587
  let pinnedBatchUnwrapMode: PinnedBatchUnwrapMode | undefined;
2730
3588
  let includePinnedNavigationSummary = false;
2731
3589
  let sessionTabCorrection: OpenResultTabCorrection | undefined;
@@ -2832,12 +3690,24 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
2832
3690
 
2833
3691
  if (processResult.spawnError?.message.includes("ENOENT")) {
2834
3692
  const errorText = buildMissingBinaryMessage();
3693
+ const managedSessionOutcome = buildManagedSessionOutcome({
3694
+ activeAfter: managedSessionActive,
3695
+ activeBefore: managedSessionActive,
3696
+ attemptedSessionName: executionPlan.managedSessionName,
3697
+ command: executionPlan.commandInfo.command,
3698
+ currentSessionName: managedSessionName,
3699
+ previousSessionName: managedSessionName,
3700
+ sessionMode,
3701
+ succeeded: false,
3702
+ });
3703
+ const managedSessionOutcomeText = formatManagedSessionOutcomeText(managedSessionOutcome);
2835
3704
  return {
2836
- content: [{ type: "text", text: errorText }],
3705
+ content: [{ type: "text", text: managedSessionOutcomeText ? `${errorText}\n\n${managedSessionOutcomeText}` : errorText }],
2837
3706
  details: {
2838
3707
  args: redactedArgs,
2839
3708
  compatibilityWorkaround,
2840
3709
  effectiveArgs: redactedProcessArgs,
3710
+ managedSessionOutcome,
2841
3711
  sessionMode,
2842
3712
  sessionTabCorrection,
2843
3713
  ...buildAgentBrowserResultCategoryDetails({ args: redactedProcessArgs, command: executionPlan.commandInfo.command, errorText, failureCategory: "missing-binary", spawnError: processResult.spawnError.message, succeeded: false }),
@@ -2918,6 +3788,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
2918
3788
  data: mergeNavigationSummaryIntoData(presentationEnvelope.data, navigationSummary),
2919
3789
  };
2920
3790
  }
3791
+ let overlayBlockerDiagnostic: OverlayBlockerDiagnostic | undefined;
2921
3792
 
2922
3793
  let openResultTabCorrection: OpenResultTabCorrection | undefined;
2923
3794
  if (
@@ -3021,33 +3892,91 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
3021
3892
  }
3022
3893
  }
3023
3894
  }
3895
+ let selectorTextVisibilityDiagnostics: SelectorTextVisibilityDiagnostic[] = [];
3896
+ const timeoutPartialProgress = processResult.timedOut ? await collectTimeoutPartialProgress({
3897
+ command: executionPlan.commandInfo.command,
3898
+ compiledJob,
3899
+ cwd: ctx.cwd,
3900
+ sessionName: executionPlan.sessionName,
3901
+ stdin: toolStdin,
3902
+ }) : undefined;
3903
+ if (succeeded && !sessionTabCorrection && !aboutBlankSessionMismatch) {
3904
+ overlayBlockerDiagnostic = await collectOverlayBlockerDiagnostic({
3905
+ command: executionPlan.commandInfo.command,
3906
+ cwd: ctx.cwd,
3907
+ data: presentationEnvelope?.data,
3908
+ navigationSummary,
3909
+ priorTarget: priorSessionTabTarget,
3910
+ sessionName: executionPlan.sessionName,
3911
+ signal,
3912
+ });
3913
+ }
3914
+ if (succeeded) {
3915
+ selectorTextVisibilityDiagnostics = await collectSelectorTextVisibilityDiagnostics({
3916
+ commandInfo: executionPlan.commandInfo,
3917
+ commandTokens,
3918
+ cwd: ctx.cwd,
3919
+ data: presentationEnvelope?.data,
3920
+ sessionName: executionPlan.sessionName,
3921
+ signal,
3922
+ });
3923
+ }
3924
+ let currentRefSnapshot: SessionRefSnapshot | undefined;
3024
3925
  if (executionPlan.sessionName) {
3025
3926
  const activeSessionTabTargetState = sessionTabTargets.get(executionPlan.sessionName);
3026
3927
  if (shouldApplySessionTabTargetUpdate({ current: activeSessionTabTargetState, updateOrder: tabTargetUpdateOrder })) {
3027
3928
  if (executionPlan.commandInfo.command === "close" && succeeded) {
3028
3929
  sessionTabTargets.delete(executionPlan.sessionName);
3930
+ sessionRefSnapshots.delete(executionPlan.sessionName);
3029
3931
  } else if (currentSessionTabTarget) {
3030
3932
  sessionTabTargets.set(executionPlan.sessionName, { order: tabTargetUpdateOrder, target: currentSessionTabTarget });
3031
3933
  }
3032
3934
  }
3935
+ const refSnapshot = succeeded
3936
+ ? executionPlan.commandInfo.command === "snapshot"
3937
+ ? extractRefSnapshotFromData(presentationEnvelope?.data)
3938
+ : executionPlan.commandInfo.command === "batch"
3939
+ ? extractRefSnapshotFromBatchResults(presentationEnvelope?.data)
3940
+ : overlayBlockerDiagnostic?.snapshot
3941
+ : undefined;
3942
+ if (refSnapshot && shouldApplySessionTabTargetUpdate({ current: sessionRefSnapshots.get(executionPlan.sessionName), updateOrder: tabTargetUpdateOrder })) {
3943
+ currentRefSnapshot = { ...refSnapshot, target: refSnapshot.target ?? currentSessionTabTarget };
3944
+ sessionRefSnapshots.set(executionPlan.sessionName, { ...currentRefSnapshot, order: tabTargetUpdateOrder });
3945
+ } else {
3946
+ currentRefSnapshot = sessionRefSnapshots.get(executionPlan.sessionName);
3947
+ }
3033
3948
  }
3034
3949
 
3950
+ const priorManagedSessionActive = managedSessionActive;
3035
3951
  const priorManagedSessionCwd = managedSessionCwd;
3952
+ const priorManagedSessionName = managedSessionName;
3036
3953
  const managedSessionState = resolveManagedSessionState({
3037
3954
  command: executionPlan.commandInfo.command,
3038
3955
  managedSessionName: executionPlan.managedSessionName,
3039
- priorActive: managedSessionActive,
3040
- priorSessionName: managedSessionName,
3956
+ priorActive: priorManagedSessionActive,
3957
+ priorSessionName: priorManagedSessionName,
3041
3958
  succeeded,
3042
3959
  });
3043
3960
  const replacedManagedSessionName = managedSessionState.replacedSessionName;
3044
3961
  managedSessionActive = managedSessionState.active;
3045
3962
  managedSessionName = managedSessionState.sessionName;
3963
+ let managedSessionOutcome = buildManagedSessionOutcome({
3964
+ activeAfter: managedSessionActive,
3965
+ activeBefore: priorManagedSessionActive,
3966
+ attemptedSessionName: executionPlan.managedSessionName,
3967
+ command: executionPlan.commandInfo.command,
3968
+ currentSessionName: managedSessionName,
3969
+ previousSessionName: priorManagedSessionName,
3970
+ replacedSessionName: replacedManagedSessionName,
3971
+ sessionMode,
3972
+ succeeded,
3973
+ });
3046
3974
  if (executionPlan.managedSessionName && succeeded) {
3047
3975
  managedSessionCwd = ctx.cwd;
3048
3976
  }
3049
3977
  if (replacedManagedSessionName) {
3050
3978
  sessionTabTargets.delete(replacedManagedSessionName);
3979
+ sessionRefSnapshots.delete(replacedManagedSessionName);
3051
3980
  await closeManagedSession({
3052
3981
  cwd: priorManagedSessionCwd,
3053
3982
  sessionName: replacedManagedSessionName,
@@ -3132,9 +4061,11 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
3132
4061
  } else if (sourceLookup) {
3133
4062
  presentation.content.unshift({ type: "text", text: sourceLookup.summary });
3134
4063
  }
3135
- if (qaPreset && !qaPreset.passed) {
3136
- succeeded = false;
3137
- presentation.failureCategory = "qa-failure";
4064
+ if (qaPreset && (!qaPreset.passed || qaPreset.warnings.length > 0)) {
4065
+ if (!qaPreset.passed) {
4066
+ succeeded = false;
4067
+ presentation.failureCategory = "qa-failure";
4068
+ }
3138
4069
  presentation.summary = qaPreset.summary;
3139
4070
  if (presentation.content[0]?.type === "text") {
3140
4071
  presentation.content[0] = { ...presentation.content[0], text: `${qaPreset.summary}\n\n${presentation.content[0].text}` };
@@ -3142,6 +4073,20 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
3142
4073
  presentation.content.unshift({ type: "text", text: qaPreset.summary });
3143
4074
  }
3144
4075
  }
4076
+ if (managedSessionOutcome && managedSessionOutcome.succeeded !== succeeded) {
4077
+ managedSessionOutcome = { ...managedSessionOutcome, succeeded };
4078
+ }
4079
+ const evalStdinHint = getEvalStdinHint({
4080
+ command: executionPlan.commandInfo.command,
4081
+ data: presentationEnvelope?.data,
4082
+ stdin: toolStdin,
4083
+ });
4084
+ const resultArtifactManifest = presentation.artifactManifest ?? artifactManifest;
4085
+ const artifactCleanup = getArtifactCleanupGuidance({
4086
+ command: executionPlan.commandInfo.command,
4087
+ manifest: resultArtifactManifest,
4088
+ succeeded,
4089
+ });
3145
4090
  const warningText = aboutBlankSessionMismatch ? buildAboutBlankWarning(aboutBlankSessionMismatch) : undefined;
3146
4091
  const contentWithSessionWarnings = userRequestedJson && !plainTextInspection
3147
4092
  ? buildJsonVisibleContent({
@@ -3187,6 +4132,21 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
3187
4132
  validationError: undefined,
3188
4133
  });
3189
4134
  let nextActions = presentation.nextActions ? [...presentation.nextActions] : undefined;
4135
+ if (categoryDetails.failureCategory === "stale-ref") {
4136
+ nextActions = sessionAwareStaleRefNextActions(executionPlan.sessionName);
4137
+ }
4138
+ if (categoryDetails.failureCategory === "selector-not-found" && redactedCompiledSemanticAction) {
4139
+ const candidateActions = buildSemanticActionCandidateActions(redactedCompiledSemanticAction);
4140
+ if (candidateActions.length > 0) {
4141
+ (nextActions ??= []).push(...candidateActions);
4142
+ }
4143
+ }
4144
+ if (overlayBlockerDiagnostic) {
4145
+ (nextActions ??= []).push(...buildOverlayBlockerNextActions({ diagnostic: overlayBlockerDiagnostic, sessionName: executionPlan.sessionName }));
4146
+ }
4147
+ if (selectorTextVisibilityDiagnostics.length > 0) {
4148
+ (nextActions ??= []).push(...buildSelectorTextVisibilityNextActions({ diagnostics: selectorTextVisibilityDiagnostics, sessionName: executionPlan.sessionName }));
4149
+ }
3190
4150
  if (categoryDetails.failureCategory === "stale-ref" && redactedCompiledSemanticAction) {
3191
4151
  (nextActions ??= []).push({
3192
4152
  id: "retry-semantic-action-after-stale-ref",
@@ -3202,8 +4162,9 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
3202
4162
  compiledQaPreset: redactedCompiledQaPreset,
3203
4163
  compiledSourceLookup: redactedCompiledSourceLookup,
3204
4164
  compiledNetworkSourceLookup: redactedCompiledNetworkSourceLookup,
3205
- artifactManifest: presentation.artifactManifest,
3206
- artifactRetentionSummary: presentation.artifactRetentionSummary,
4165
+ artifactManifest: resultArtifactManifest,
4166
+ artifactRetentionSummary: presentation.artifactRetentionSummary ?? (resultArtifactManifest ? formatSessionArtifactRetentionSummary(resultArtifactManifest) : undefined),
4167
+ artifactCleanup,
3207
4168
  artifactVerification: presentation.artifactVerification,
3208
4169
  artifacts: presentation.artifacts,
3209
4170
  batchFailure: presentation.batchFailure,
@@ -3224,11 +4185,17 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
3224
4185
  fullOutputPath: parseFailureOutput.fullOutputPath ?? presentation.fullOutputPath,
3225
4186
  fullOutputPaths: presentation.fullOutputPaths,
3226
4187
  fullOutputUnavailable: parseFailureOutput.fullOutputUnavailable,
4188
+ managedSessionOutcome,
3227
4189
  imagePath: presentation.imagePath,
3228
4190
  imagePaths: presentation.imagePaths,
3229
4191
  nextActions,
3230
4192
  pageChangeSummary: presentation.pageChangeSummary,
4193
+ overlayBlockers: overlayBlockerDiagnostic,
3231
4194
  qaPreset,
4195
+ selectorTextVisibility: selectorTextVisibilityDiagnostics[0],
4196
+ selectorTextVisibilityAll: selectorTextVisibilityDiagnostics.length > 1 ? selectorTextVisibilityDiagnostics : undefined,
4197
+ evalStdinHint,
4198
+ timeoutPartialProgress,
3232
4199
  parseError: plainTextInspection ? undefined : parseError,
3233
4200
  savedFile: presentation.savedFile,
3234
4201
  savedFilePath: presentation.savedFilePath,
@@ -3237,6 +4204,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
3237
4204
  sessionMode,
3238
4205
  sessionTabCorrection,
3239
4206
  sessionTabTarget: currentSessionTabTarget,
4207
+ refSnapshot: currentRefSnapshot,
3240
4208
  ...buildSessionDetailFields(executionPlan.sessionName, executionPlan.usedImplicitSession),
3241
4209
  sessionRecoveryHint: redactedRecoveryHint,
3242
4210
  startupScopedFlags: executionPlan.startupScopedFlags,
@@ -3247,8 +4215,24 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
3247
4215
  timeoutMs: processResult.timeoutMs,
3248
4216
  };
3249
4217
 
4218
+ const semanticActionCandidateText = nextActions ? formatSemanticActionCandidateText(nextActions) : undefined;
4219
+ const overlayBlockerText = overlayBlockerDiagnostic ? formatOverlayBlockerText(overlayBlockerDiagnostic) : undefined;
4220
+ const selectorTextVisibilityText = formatSelectorTextVisibilityText(selectorTextVisibilityDiagnostics);
4221
+ const evalStdinHintText = formatEvalStdinHintText(evalStdinHint);
4222
+ const artifactCleanupText = formatArtifactCleanupGuidanceText(artifactCleanup);
4223
+ const timeoutPartialProgressText = timeoutPartialProgress ? formatTimeoutPartialProgressText(timeoutPartialProgress) : undefined;
4224
+ const managedSessionOutcomeText = formatManagedSessionOutcomeText(managedSessionOutcome);
4225
+ const rawAppendedDiagnosticText = [semanticActionCandidateText, overlayBlockerText, selectorTextVisibilityText, evalStdinHintText, artifactCleanupText, timeoutPartialProgressText, managedSessionOutcomeText].filter((item): item is string => item !== undefined).join("\n\n");
4226
+ const appendedDiagnosticText = redactSensitiveText(redactExactSensitiveText(rawAppendedDiagnosticText, exactSensitiveValues));
4227
+ const shouldAppendDiagnosticText = appendedDiagnosticText.length > 0 && (!userRequestedJson || plainTextInspection);
4228
+ const content = shouldAppendDiagnosticText && redactedContent[0]?.type === "text"
4229
+ ? [
4230
+ { ...redactedContent[0], text: `${redactedContent[0].text}\n\n${appendedDiagnosticText}` },
4231
+ ...redactedContent.slice(1),
4232
+ ]
4233
+ : redactedContent;
3250
4234
  const result = {
3251
- content: redactedContent,
4235
+ content,
3252
4236
  details: redactToolDetails(details, exactSensitiveValues),
3253
4237
  isError: !succeeded,
3254
4238
  };