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.
- package/CHANGELOG.md +31 -1
- package/README.md +19 -8
- package/docs/ARCHITECTURE.md +7 -1
- package/docs/COMMAND_REFERENCE.md +23 -6
- package/docs/RELEASE.md +5 -1
- package/docs/REQUIREMENTS.md +2 -1
- package/docs/SUPPORT_MATRIX.md +20 -0
- package/docs/TOOL_CONTRACT.md +47 -15
- package/extensions/agent-browser/index.ts +1102 -23
- package/extensions/agent-browser/lib/playbook.ts +6 -5
- package/extensions/agent-browser/lib/results/presentation.ts +99 -6
- package/extensions/agent-browser/lib/results/shared.ts +72 -0
- package/extensions/agent-browser/lib/results/snapshot.ts +69 -9
- package/extensions/agent-browser/lib/results.ts +1 -0
- package/extensions/agent-browser/lib/runtime.ts +7 -2
- package/package.json +1 -1
|
@@ -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
|
|
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
|
|
399
|
-
if (
|
|
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
|
|
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",
|
|
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
|
-
|
|
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,
|
|
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?:
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
3040
|
-
priorSessionName:
|
|
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
|
-
|
|
3137
|
-
|
|
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:
|
|
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
|
|
4330
|
+
content,
|
|
3252
4331
|
details: redactToolDetails(details, exactSensitiveValues),
|
|
3253
4332
|
isError: !succeeded,
|
|
3254
4333
|
};
|