pi-agent-browser-native 0.3.0 → 0.6.5
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 +265 -0
- package/README.md +130 -54
- package/dist/extensions/agent-browser/index.js +781 -169
- package/dist/extensions/agent-browser/lib/argv-descriptor.js +35 -3
- package/dist/extensions/agent-browser/lib/argv-grammar.js +50 -2
- package/dist/extensions/agent-browser/lib/batch-lifecycle.js +71 -0
- package/dist/extensions/agent-browser/lib/command-policy.js +5 -8
- package/dist/extensions/agent-browser/lib/command-taxonomy.js +53 -12
- package/dist/extensions/agent-browser/lib/config-policy.js +25 -1
- package/dist/extensions/agent-browser/lib/config.js +1 -1
- package/dist/extensions/agent-browser/lib/input-modes/job.js +61 -13
- package/dist/extensions/agent-browser/lib/input-modes/lookups.js +2 -2
- package/dist/extensions/agent-browser/lib/input-modes/params.js +23 -24
- package/dist/extensions/agent-browser/lib/input-modes/script.js +462 -0
- package/dist/extensions/agent-browser/lib/input-modes/semantic-action.js +51 -12
- package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +26 -4
- package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +6 -139
- package/dist/extensions/agent-browser/lib/managed-session-restore.js +26 -116
- package/dist/extensions/agent-browser/lib/managed-session-snapshots.js +2 -4
- package/dist/extensions/agent-browser/lib/managed-session-storage.js +54 -25
- package/dist/extensions/agent-browser/lib/orchestration/batch-stdin.js +26 -5
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.js +110 -30
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.js +2 -1
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +54 -48
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +71 -5
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +2 -1
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +6 -7
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/snapshot-filter.js +119 -2
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/wait-timeouts.js +7 -6
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +152 -64
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +244 -102
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +63 -37
- package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +20 -21
- package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +36 -18
- package/dist/extensions/agent-browser/lib/orchestration/output-file.js +41 -21
- package/dist/extensions/agent-browser/lib/orchestration/script-mode.js +299 -0
- package/dist/extensions/agent-browser/lib/page-target-validation.js +270 -0
- package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +32 -10
- package/dist/extensions/agent-browser/lib/playbook.js +29 -25
- package/dist/extensions/agent-browser/lib/process-environment.js +14 -0
- package/dist/extensions/agent-browser/lib/process-identity.js +5 -12
- package/dist/extensions/agent-browser/lib/process.js +130 -104
- package/dist/extensions/agent-browser/lib/recording-reservations.js +116 -0
- package/dist/extensions/agent-browser/lib/results/action-recommendations.js +63 -6
- package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +62 -4
- package/dist/extensions/agent-browser/lib/results/categories.js +6 -1
- package/dist/extensions/agent-browser/lib/results/next-actions.js +19 -5
- package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +85 -38
- package/dist/extensions/agent-browser/lib/results/presentation/batch.js +86 -18
- package/dist/extensions/agent-browser/lib/results/presentation/common.js +38 -2
- package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +18 -17
- package/dist/extensions/agent-browser/lib/results/presentation/errors.js +2 -1
- package/dist/extensions/agent-browser/lib/results/presentation/navigation.js +38 -20
- package/dist/extensions/agent-browser/lib/results/presentation/registry.js +60 -15
- package/dist/extensions/agent-browser/lib/results/presentation/semantic-action.js +1 -10
- package/dist/extensions/agent-browser/lib/results/presentation.js +36 -6
- package/dist/extensions/agent-browser/lib/results/recovery-actions.js +3 -1
- package/dist/extensions/agent-browser/lib/results/recovery-next-actions.js +9 -0
- package/dist/extensions/agent-browser/lib/results/selector-recovery.js +54 -11
- package/dist/extensions/agent-browser/lib/results/snapshot-high-value-controls.js +13 -7
- package/dist/extensions/agent-browser/lib/results/snapshot-spill.js +2 -1
- package/dist/extensions/agent-browser/lib/results/snapshot.js +4 -4
- package/dist/extensions/agent-browser/lib/runtime.js +186 -108
- package/dist/extensions/agent-browser/lib/session-page-state.js +71 -10
- package/dist/extensions/agent-browser/lib/temp.js +1 -2
- package/dist/extensions/agent-browser/lib/upstream-version.js +14 -0
- package/dist/extensions/agent-browser/lib/web-search.js +108 -24
- package/dist/extensions/agent-browser/script-worker.js +169 -0
- package/dist/scripts/agent-browser-target.mjs +21 -0
- package/docs/ARCHITECTURE.md +57 -34
- package/docs/COMMAND_REFERENCE.md +255 -68
- package/docs/ELECTRON.md +2 -2
- package/docs/RELEASE.md +12 -10
- package/docs/REQUIREMENTS.md +11 -8
- package/docs/SUPPORT_MATRIX.md +36 -24
- package/docs/TOOL_CONTRACT.md +169 -95
- package/package.json +3 -1
- package/platform-smoke.config.mjs +2 -2
- package/scripts/agent-browser-capability-baseline.mjs +87 -9
- package/scripts/agent-browser-target.mjs +21 -0
- package/scripts/build.mjs +41 -0
- package/scripts/config.mjs +1 -0
- package/scripts/doctor.mjs +16 -9
- package/scripts/platform-smoke/browser-dogfood-windows.ps1 +9 -3
- package/scripts/platform-smoke/targets.mjs +12 -6
- package/dist/extensions/agent-browser/lib/managed-session-capabilities.js +0 -20
- package/dist/extensions/agent-browser/lib/managed-session-state-policy.js +0 -583
- package/dist/extensions/agent-browser/lib/navigation-policy.js +0 -78
- package/dist/extensions/agent-browser/lib/results/presentation/managed-list-filter.js +0 -37
|
@@ -3,8 +3,12 @@ import { linkSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync
|
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { dirname, isAbsolute, join, parse, resolve, win32 } from "node:path";
|
|
5
5
|
import { canonicalizeAgentBrowserNamespace } from "./argv-grammar.js";
|
|
6
|
-
|
|
6
|
+
const MANAGED_SESSION_RESTORE_KEY_PATTERN = /^piab-r2-[a-f\d]{32}$/i;
|
|
7
7
|
const MANAGED_SESSION_NAME_PREFIX = "piab-r2-";
|
|
8
|
+
export function isManagedSessionRestoreKey(value) {
|
|
9
|
+
return typeof value === "string" && MANAGED_SESSION_RESTORE_KEY_PATTERN.test(value);
|
|
10
|
+
}
|
|
11
|
+
const MANAGED_SESSION_FRESH_SUFFIX_PATTERN = /-fresh-[a-f\d]{10}$/i;
|
|
8
12
|
const MANAGED_SESSION_RESTORE_KEY_HASH_LENGTH = 32;
|
|
9
13
|
const PROJECT_GENERATION_MARKER_NAME = "pi-agent-browser-project-generation-v1.json";
|
|
10
14
|
const PROJECT_GENERATION_MARKER_MAX_BYTES = 1_024;
|
|
@@ -15,7 +19,7 @@ function isAbsoluteHome(path, platform) {
|
|
|
15
19
|
function currentUid() {
|
|
16
20
|
return typeof process.getuid === "function" ? process.getuid() : undefined;
|
|
17
21
|
}
|
|
18
|
-
function isTrustedPosixDirectory(path, requireCurrentOwner) {
|
|
22
|
+
function isTrustedPosixDirectory(path, requireCurrentOwner, platform = process.platform) {
|
|
19
23
|
const uid = currentUid();
|
|
20
24
|
if (uid === undefined)
|
|
21
25
|
return false;
|
|
@@ -32,11 +36,13 @@ function isTrustedPosixDirectory(path, requireCurrentOwner) {
|
|
|
32
36
|
}
|
|
33
37
|
if (entry.isSymbolicLink() || !entry.isDirectory())
|
|
34
38
|
return false;
|
|
35
|
-
|
|
39
|
+
const androidSystemAncestor = platform === "android" && (cursor === "/data" || cursor === "/data/data") && entry.uid === 1000 && (entry.mode & 0o002) === 0;
|
|
40
|
+
const androidAppDirectory = platform === "android" && entry.uid === uid && entry.gid === uid && (entry.mode & 0o002) === 0;
|
|
41
|
+
if (!androidSystemAncestor && entry.uid !== 0 && entry.uid !== uid)
|
|
36
42
|
return false;
|
|
37
43
|
const writableByOthers = (entry.mode & 0o022) !== 0;
|
|
38
44
|
const rootOwnedStickyDirectory = entry.uid === 0 && (entry.mode & 0o1000) !== 0;
|
|
39
|
-
if (writableByOthers && !rootOwnedStickyDirectory)
|
|
45
|
+
if (writableByOthers && !rootOwnedStickyDirectory && !androidSystemAncestor && !androidAppDirectory)
|
|
40
46
|
return false;
|
|
41
47
|
}
|
|
42
48
|
try {
|
|
@@ -56,7 +62,7 @@ export function resolveManagedSessionRestoreHome(parentEnv, platform = process.p
|
|
|
56
62
|
return candidate;
|
|
57
63
|
try {
|
|
58
64
|
const canonical = realpathSync(candidate);
|
|
59
|
-
return isTrustedPosixDirectory(canonical, true) ? canonical : undefined;
|
|
65
|
+
return isTrustedPosixDirectory(canonical, true, platform) ? canonical : undefined;
|
|
60
66
|
}
|
|
61
67
|
catch {
|
|
62
68
|
return undefined;
|
|
@@ -138,12 +144,15 @@ function readProjectGenerationMarker(path, platform) {
|
|
|
138
144
|
return undefined;
|
|
139
145
|
}
|
|
140
146
|
}
|
|
141
|
-
function getDirectoryFilesystemIdentity(path) {
|
|
147
|
+
function getDirectoryFilesystemIdentity(path, platform) {
|
|
142
148
|
try {
|
|
143
149
|
const entry = statSync(path, { bigint: true });
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
150
|
+
if (!entry.isDirectory() || entry.dev <= 0n || entry.ino <= 0n)
|
|
151
|
+
return undefined;
|
|
152
|
+
// ponytail: Android reports mutable ctime as birthtime; use statx birthtime/inode generation when Node exposes either reliably.
|
|
153
|
+
return platform === "android"
|
|
154
|
+
? `${entry.dev}:${entry.ino}`
|
|
155
|
+
: entry.birthtimeNs > 0n ? `${entry.dev}:${entry.ino}:${entry.birthtimeNs}` : undefined;
|
|
147
156
|
}
|
|
148
157
|
catch {
|
|
149
158
|
return undefined;
|
|
@@ -157,13 +166,13 @@ function resolveManagedSessionRestoreProjectCheckout(cwd, platform) {
|
|
|
157
166
|
catch {
|
|
158
167
|
return undefined;
|
|
159
168
|
}
|
|
160
|
-
if (platform !== "win32" && !isTrustedPosixDirectory(canonicalCwd, false))
|
|
169
|
+
if (platform !== "win32" && !isTrustedPosixDirectory(canonicalCwd, false, platform))
|
|
161
170
|
return undefined;
|
|
162
171
|
const checkout = resolveGitCheckout(canonicalCwd, platform);
|
|
163
172
|
if (!checkout)
|
|
164
173
|
return undefined;
|
|
165
|
-
if (platform !== "win32" && (!isTrustedPosixDirectory(checkout.worktreeDirectory, true)
|
|
166
|
-
|| !isTrustedPosixDirectory(checkout.gitDirectory, true)))
|
|
174
|
+
if (platform !== "win32" && (!isTrustedPosixDirectory(checkout.worktreeDirectory, true, platform)
|
|
175
|
+
|| !isTrustedPosixDirectory(checkout.gitDirectory, true, platform)))
|
|
167
176
|
return undefined;
|
|
168
177
|
return { canonicalCwd, ...checkout };
|
|
169
178
|
}
|
|
@@ -175,8 +184,8 @@ function resolveProjectGenerationIdentity(cwd, platform = process.platform) {
|
|
|
175
184
|
if (!checkout)
|
|
176
185
|
return undefined;
|
|
177
186
|
const { canonicalCwd } = checkout;
|
|
178
|
-
const gitFilesystemIdentity = getDirectoryFilesystemIdentity(checkout.gitDirectory);
|
|
179
|
-
const worktreeFilesystemIdentity = getDirectoryFilesystemIdentity(checkout.worktreeDirectory);
|
|
187
|
+
const gitFilesystemIdentity = getDirectoryFilesystemIdentity(checkout.gitDirectory, platform);
|
|
188
|
+
const worktreeFilesystemIdentity = getDirectoryFilesystemIdentity(checkout.worktreeDirectory, platform);
|
|
180
189
|
if (!gitFilesystemIdentity || !worktreeFilesystemIdentity)
|
|
181
190
|
return undefined;
|
|
182
191
|
const markerPath = join(checkout.gitDirectory, PROJECT_GENERATION_MARKER_NAME);
|
|
@@ -192,22 +201,35 @@ function resolveProjectGenerationIdentity(cwd, platform = process.platform) {
|
|
|
192
201
|
projectGenerationCache.delete(canonicalCwd);
|
|
193
202
|
try {
|
|
194
203
|
if (!marker) {
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
204
|
+
const content = JSON.stringify({ id: randomUUID(), version: 1 });
|
|
205
|
+
if (platform === "android") {
|
|
206
|
+
// ponytail: Android denies hard links in app storage; use renameat2(RENAME_NOREPLACE) if Node exposes it.
|
|
198
207
|
try {
|
|
199
|
-
|
|
208
|
+
writeFileSync(markerPath, content, { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
200
209
|
}
|
|
201
210
|
catch (error) {
|
|
202
211
|
if (error.code !== "EEXIST")
|
|
203
212
|
return undefined;
|
|
204
213
|
}
|
|
205
214
|
}
|
|
206
|
-
|
|
215
|
+
else {
|
|
216
|
+
const candidatePath = `${markerPath}.candidate-${process.pid}-${randomUUID()}`;
|
|
207
217
|
try {
|
|
208
|
-
|
|
218
|
+
writeFileSync(candidatePath, content, { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
219
|
+
try {
|
|
220
|
+
linkSync(candidatePath, markerPath);
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
if (error.code !== "EEXIST")
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
finally {
|
|
228
|
+
try {
|
|
229
|
+
unlinkSync(candidatePath);
|
|
230
|
+
}
|
|
231
|
+
catch { }
|
|
209
232
|
}
|
|
210
|
-
catch { }
|
|
211
233
|
}
|
|
212
234
|
marker = readProjectGenerationMarker(markerPath, platform);
|
|
213
235
|
}
|
|
@@ -231,16 +253,23 @@ function resolveProjectGenerationIdentity(cwd, platform = process.platform) {
|
|
|
231
253
|
export function hasManagedSessionRestoreProjectIdentity(cwd) {
|
|
232
254
|
return resolveProjectGenerationIdentity(cwd) !== undefined;
|
|
233
255
|
}
|
|
234
|
-
/**
|
|
235
|
-
export function
|
|
256
|
+
/** Keep fresh rotations from one Pi transcript in one private upstream restore pool. */
|
|
257
|
+
export function getManagedSessionRestoreScope(sessionName) {
|
|
258
|
+
return sessionName.replace(MANAGED_SESSION_FRESH_SUFFIX_PATTERN, "");
|
|
259
|
+
}
|
|
260
|
+
/** Stable for one Pi transcript and checkout generation; isolated from other concurrent transcripts. */
|
|
261
|
+
export function createManagedSessionRestoreKey(cwd, restoreScope = "", platform = process.platform) {
|
|
236
262
|
let canonicalCwd = resolve(cwd);
|
|
237
263
|
try {
|
|
238
264
|
canonicalCwd = realpathSync(canonicalCwd);
|
|
239
265
|
}
|
|
240
266
|
catch { }
|
|
241
|
-
const identity = resolveProjectGenerationIdentity(canonicalCwd);
|
|
267
|
+
const identity = resolveProjectGenerationIdentity(canonicalCwd, platform);
|
|
242
268
|
const material = identity ?? `unavailable:${canonicalCwd}`;
|
|
243
|
-
const digest = createHash("sha256")
|
|
269
|
+
const digest = createHash("sha256")
|
|
270
|
+
.update(`restore-v3:${material}:scope:${restoreScope}`)
|
|
271
|
+
.digest("hex")
|
|
272
|
+
.slice(0, MANAGED_SESSION_RESTORE_KEY_HASH_LENGTH);
|
|
244
273
|
return `${MANAGED_SESSION_NAME_PREFIX}${digest}`;
|
|
245
274
|
}
|
|
246
275
|
function hasValidEncryptionKey(parentEnv) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const BATCH_STDIN_EXAMPLE = ' Example: { "args": ["batch"], "stdin": "[[\\"get\\",\\"title\\"],[\\"get\\",\\"url\\"]]" }';
|
|
1
2
|
// Mirror upstream commands::shell_words_split so policy inspection sees the same argv.
|
|
2
3
|
export function parseBatchCommandArgument(command) {
|
|
3
4
|
const tokens = [];
|
|
@@ -36,20 +37,20 @@ export function parseBatchCommandArgument(command) {
|
|
|
36
37
|
function validateUserBatchStep(step, index) {
|
|
37
38
|
if (!Array.isArray(step)) {
|
|
38
39
|
return {
|
|
39
|
-
error: `agent_browser batch stdin step ${index} must be a non-empty array of string command tokens
|
|
40
|
+
error: `agent_browser batch stdin step ${index} must be a non-empty array of string command tokens.${BATCH_STDIN_EXAMPLE}`,
|
|
40
41
|
ok: false,
|
|
41
42
|
};
|
|
42
43
|
}
|
|
43
44
|
if (step.length === 0) {
|
|
44
45
|
return {
|
|
45
|
-
error: `agent_browser batch stdin step ${index} must not be empty
|
|
46
|
+
error: `agent_browser batch stdin step ${index} must not be empty.${BATCH_STDIN_EXAMPLE}`,
|
|
46
47
|
ok: false,
|
|
47
48
|
};
|
|
48
49
|
}
|
|
49
50
|
const invalidTokenIndex = step.findIndex((token) => typeof token !== "string");
|
|
50
51
|
if (invalidTokenIndex !== -1) {
|
|
51
52
|
return {
|
|
52
|
-
error: `agent_browser batch stdin step ${index} token ${invalidTokenIndex} must be a string
|
|
53
|
+
error: `agent_browser batch stdin step ${index} token ${invalidTokenIndex} must be a string.${BATCH_STDIN_EXAMPLE}`,
|
|
53
54
|
ok: false,
|
|
54
55
|
};
|
|
55
56
|
}
|
|
@@ -62,13 +63,13 @@ export function parseBatchStdinJsonArray(stdin) {
|
|
|
62
63
|
try {
|
|
63
64
|
const parsed = JSON.parse(stdin);
|
|
64
65
|
if (!Array.isArray(parsed)) {
|
|
65
|
-
return { error:
|
|
66
|
+
return { error: `agent_browser batch stdin must be a JSON array of command steps.${BATCH_STDIN_EXAMPLE}` };
|
|
66
67
|
}
|
|
67
68
|
return { steps: parsed };
|
|
68
69
|
}
|
|
69
70
|
catch (error) {
|
|
70
71
|
const message = error instanceof Error ? error.message : String(error);
|
|
71
|
-
return { error: `agent_browser batch stdin could not be parsed as JSON: ${message}` };
|
|
72
|
+
return { error: `agent_browser batch stdin could not be parsed as JSON: ${message}.${BATCH_STDIN_EXAMPLE}` };
|
|
72
73
|
}
|
|
73
74
|
}
|
|
74
75
|
export function parseUserBatchStdin(stdin) {
|
|
@@ -86,6 +87,26 @@ export function parseUserBatchStdin(stdin) {
|
|
|
86
87
|
}
|
|
87
88
|
return { steps };
|
|
88
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* The batch steps upstream will actually execute: run_batch uses raw batch
|
|
92
|
+
* arguments exclusively when any exist and reads stdin only otherwise.
|
|
93
|
+
* Upstream filters only the exact `--bail` token, so an equals form such as
|
|
94
|
+
* `--bail=true` stays a raw command (an unknown-command row) and keeps stdin
|
|
95
|
+
* ignored.
|
|
96
|
+
*/
|
|
97
|
+
export function getUpstreamEffectiveBatchSteps(commandTokens, stdin) {
|
|
98
|
+
if (commandTokens[0] !== "batch")
|
|
99
|
+
return [];
|
|
100
|
+
const argumentSteps = commandTokens.slice(1).flatMap((command) => {
|
|
101
|
+
if (command === "--bail")
|
|
102
|
+
return [];
|
|
103
|
+
const step = parseBatchCommandArgument(command).step;
|
|
104
|
+
return step ? [step] : [];
|
|
105
|
+
});
|
|
106
|
+
if (argumentSteps.length > 0)
|
|
107
|
+
return argumentSteps;
|
|
108
|
+
return parseUserBatchStdin(stdin).steps ?? [];
|
|
109
|
+
}
|
|
89
110
|
export function parseValidBatchStepEntries(stdin) {
|
|
90
111
|
const parsed = parseBatchStdinJsonArray(stdin);
|
|
91
112
|
if (parsed.error || parsed.steps === undefined)
|
|
@@ -1,46 +1,126 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { lstatSync, readlinkSync, realpathSync, statSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
3
|
+
import { foldAgentBrowserFilesystemIdentity } from "../../argv-grammar.js";
|
|
4
|
+
import { parseWaitCommandTokens } from "../../argv-descriptor.js";
|
|
3
5
|
const SCREENSHOT_BOOLEAN_FLAGS = new Set(["--annotate", "--full", "-f"]);
|
|
4
6
|
const SCREENSHOT_VALUE_FLAGS = new Set(["--screenshot-dir", "--screenshot-format", "--screenshot-quality"]);
|
|
5
|
-
const SCREENSHOT_IMAGE_EXTENSIONS =
|
|
6
|
-
function
|
|
7
|
-
const
|
|
8
|
-
|
|
7
|
+
const SCREENSHOT_IMAGE_EXTENSIONS = [".jpeg", ".jpg", ".png", ".webp"];
|
|
8
|
+
function isSingleScreenshotPathToken(token) {
|
|
9
|
+
const explicitlyRelative = token.startsWith("./") || token.startsWith("../");
|
|
10
|
+
if (token.startsWith("#") || token.startsWith("@") || (token.startsWith(".") && !explicitlyRelative && !token.includes("/")))
|
|
11
|
+
return false;
|
|
12
|
+
return explicitlyRelative || token.includes("/") || SCREENSHOT_IMAGE_EXTENSIONS.some((extension) => token.endsWith(extension));
|
|
13
|
+
}
|
|
14
|
+
function getScreenshotPositionalIndices(commandTokens) {
|
|
15
|
+
if (commandTokens[0] !== "screenshot")
|
|
16
|
+
return [];
|
|
17
|
+
const positionalIndices = [];
|
|
18
|
+
for (let index = 1; index < commandTokens.length; index += 1) {
|
|
19
|
+
const token = commandTokens[index];
|
|
20
|
+
if (SCREENSHOT_VALUE_FLAGS.has(token)) {
|
|
21
|
+
index += 1;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (SCREENSHOT_BOOLEAN_FLAGS.has(token))
|
|
25
|
+
continue;
|
|
26
|
+
positionalIndices.push(index);
|
|
27
|
+
}
|
|
28
|
+
return positionalIndices;
|
|
9
29
|
}
|
|
10
30
|
export function getScreenshotPathTokenIndex(commandTokens) {
|
|
11
|
-
|
|
31
|
+
const positionalIndices = getScreenshotPositionalIndices(commandTokens);
|
|
32
|
+
if (positionalIndices.length === 0)
|
|
12
33
|
return undefined;
|
|
34
|
+
const candidateIndex = positionalIndices.length >= 2 ? positionalIndices[1] : positionalIndices[0];
|
|
35
|
+
const candidate = commandTokens[candidateIndex];
|
|
36
|
+
if (positionalIndices.length >= 2 || isSingleScreenshotPathToken(candidate)) {
|
|
37
|
+
return candidateIndex;
|
|
13
38
|
}
|
|
14
|
-
|
|
15
|
-
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
const DIFF_SCREENSHOT_VALUE_FLAGS = new Set(["-b", "--baseline", "-o", "--output", "-s", "--selector", "-t", "--threshold"]);
|
|
42
|
+
function getDiffScreenshotOutputPath(commandTokens) {
|
|
43
|
+
let outputPath;
|
|
44
|
+
for (let index = 2; index < commandTokens.length; index += 1) {
|
|
16
45
|
const token = commandTokens[index];
|
|
17
|
-
if (token
|
|
18
|
-
|
|
19
|
-
|
|
46
|
+
if (!DIFF_SCREENSHOT_VALUE_FLAGS.has(token))
|
|
47
|
+
continue;
|
|
48
|
+
const value = commandTokens[index + 1];
|
|
49
|
+
if (value === undefined)
|
|
50
|
+
return undefined;
|
|
51
|
+
if (token === "-o" || token === "--output")
|
|
52
|
+
outputPath = value;
|
|
53
|
+
index += 1;
|
|
54
|
+
}
|
|
55
|
+
return outputPath;
|
|
56
|
+
}
|
|
57
|
+
function foldArtifactPath(path, platform) {
|
|
58
|
+
return foldAgentBrowserFilesystemIdentity(path, platform);
|
|
59
|
+
}
|
|
60
|
+
function canonicalizeArtifactPath(absolutePath, platform, seenSymlinks) {
|
|
61
|
+
let cursor = absolutePath;
|
|
62
|
+
const suffix = [];
|
|
63
|
+
while (true) {
|
|
64
|
+
try {
|
|
65
|
+
const canonicalPath = join(realpathSync.native(cursor), ...suffix);
|
|
66
|
+
try {
|
|
67
|
+
const stats = statSync(canonicalPath, { bigint: true });
|
|
68
|
+
if (stats.ino > 0n)
|
|
69
|
+
return `inode:${stats.dev}:${stats.ino}`;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// The destination does not exist yet; canonical ancestry still catches aliases.
|
|
20
73
|
}
|
|
21
|
-
|
|
74
|
+
return foldArtifactPath(canonicalPath, platform);
|
|
22
75
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
76
|
+
catch {
|
|
77
|
+
let symlinkTarget;
|
|
78
|
+
try {
|
|
79
|
+
if (lstatSync(cursor).isSymbolicLink())
|
|
80
|
+
symlinkTarget = resolve(dirname(cursor), readlinkSync(cursor));
|
|
28
81
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
82
|
+
catch { }
|
|
83
|
+
if (symlinkTarget) {
|
|
84
|
+
if (seenSymlinks.has(cursor))
|
|
85
|
+
throw new Error(`Artifact destination contains a symlink loop: ${absolutePath}`);
|
|
86
|
+
if (seenSymlinks.size >= 32)
|
|
87
|
+
throw new Error(`Artifact destination has too many symlink hops: ${absolutePath}`);
|
|
88
|
+
seenSymlinks.add(cursor);
|
|
89
|
+
return canonicalizeArtifactPath(join(symlinkTarget, ...suffix), platform, seenSymlinks);
|
|
33
90
|
}
|
|
91
|
+
const parent = dirname(cursor);
|
|
92
|
+
if (parent === cursor)
|
|
93
|
+
return foldArtifactPath(absolutePath, platform);
|
|
94
|
+
suffix.unshift(basename(cursor));
|
|
95
|
+
cursor = parent;
|
|
34
96
|
}
|
|
35
|
-
positionalIndices.push(index);
|
|
36
97
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
98
|
+
}
|
|
99
|
+
export function canonicalizeExplicitArtifactDestination(cwd, destination, platform = process.platform) {
|
|
100
|
+
return canonicalizeArtifactPath(resolve(cwd, destination), platform, new Set());
|
|
101
|
+
}
|
|
102
|
+
export function getExplicitArtifactDestination(commandTokens) {
|
|
103
|
+
const command = commandTokens[0];
|
|
104
|
+
const subcommand = commandTokens[1];
|
|
105
|
+
if (command === "screenshot") {
|
|
106
|
+
const index = getScreenshotPathTokenIndex(commandTokens);
|
|
107
|
+
return index === undefined ? undefined : commandTokens[index];
|
|
44
108
|
}
|
|
109
|
+
if (command === "download")
|
|
110
|
+
return commandTokens[2];
|
|
111
|
+
if (command === "pdf")
|
|
112
|
+
return commandTokens[1];
|
|
113
|
+
if (command === "wait")
|
|
114
|
+
return parseWaitCommandTokens(commandTokens).downloadPath;
|
|
115
|
+
if (command === "state" && subcommand === "save")
|
|
116
|
+
return commandTokens[2];
|
|
117
|
+
if (command === "diff" && subcommand === "screenshot")
|
|
118
|
+
return getDiffScreenshotOutputPath(commandTokens);
|
|
119
|
+
if (command === "network" && subcommand === "har" && commandTokens[2] === "stop")
|
|
120
|
+
return commandTokens[3];
|
|
121
|
+
if ((command === "trace" || command === "profiler") && subcommand === "stop")
|
|
122
|
+
return commandTokens[2];
|
|
123
|
+
if (command === "record" && (subcommand === "start" || subcommand === "restart"))
|
|
124
|
+
return commandTokens[2];
|
|
45
125
|
return undefined;
|
|
46
126
|
}
|
|
@@ -245,6 +245,7 @@ export async function collectClickDispatchDiagnostic(options) {
|
|
|
245
245
|
const result = getEvalResultRecord(data);
|
|
246
246
|
if (!result)
|
|
247
247
|
return undefined;
|
|
248
|
+
options.probe.cleaned = true;
|
|
248
249
|
const status = typeof result.status === "string" ? result.status : undefined;
|
|
249
250
|
if (status !== "no-native-event-observed")
|
|
250
251
|
return undefined;
|
|
@@ -264,7 +265,7 @@ export async function collectClickDispatchDiagnostic(options) {
|
|
|
264
265
|
};
|
|
265
266
|
}
|
|
266
267
|
export async function cleanupClickDispatchProbe(options) {
|
|
267
|
-
if (!options.probe || !options.sessionName)
|
|
268
|
+
if (!options.probe || options.probe.cleaned || !options.sessionName)
|
|
268
269
|
return;
|
|
269
270
|
await runSessionCommandData({
|
|
270
271
|
args: ["eval", "--stdin"],
|
|
@@ -3,14 +3,12 @@ import { isAbsolute, resolve } from "node:path";
|
|
|
3
3
|
import { isCloseCommand, isOpenNavigationCommand } from "../../command-taxonomy.js";
|
|
4
4
|
import { boundElectronProbeString } from "../../electron/cdp.js";
|
|
5
5
|
import { executableExistsOnPath } from "../../executable-path.js";
|
|
6
|
-
import { isHttpOrHttpsUrl } from "../../input-modes/job.js";
|
|
7
6
|
import { formatSessionArtifactRetentionSummary } from "../../results/artifact-manifest.js";
|
|
8
7
|
import { buildNextToolAction, withOptionalSessionArgs } from "../../results/next-actions.js";
|
|
9
8
|
import { buildVisibleRefFallbackDiagnosticFromSnapshot, getVisibleRefFallbackTarget } from "../../results/selector-recovery.js";
|
|
10
|
-
import { extractRefSnapshotFromData, normalizeComparableUrl } from "../../session-page-state.js";
|
|
11
|
-
import { redactInvocationArgs, redactSensitiveText } from "../../runtime.js";
|
|
9
|
+
import { extractRefSnapshotFromData, isAboutBlankUrl, normalizeComparableUrl } from "../../session-page-state.js";
|
|
10
|
+
import { extractUpstreamCommandTokens, parseWaitCommandTokens, redactInvocationArgs, redactSensitiveText } from "../../runtime.js";
|
|
12
11
|
import { isRecord } from "../../parsing.js";
|
|
13
|
-
import { getManagedSessionStateAccessValidationError, isFileUrl } from "../../managed-session-state-policy.js";
|
|
14
12
|
import { extractBatchResultCommand, extractNavigationSummaryFromData, extractStringResultField, findElectronLaunchRecordForSession, runSessionCommandData, } from "./session-state.js";
|
|
15
13
|
import { parseValidBatchStepEntries } from "../batch-stdin.js";
|
|
16
14
|
import { getScreenshotPathTokenIndex } from "./artifact-paths.js";
|
|
@@ -22,10 +20,17 @@ export async function collectNavigationSummary(options) {
|
|
|
22
20
|
const url = extractStringResultField(await runSessionCommandData({ args: ["get", "url"], cwd: options.cwd, namespace: options.namespace, sessionName: options.sessionName, signal: options.signal }), "url");
|
|
23
21
|
if (!url || !/^[a-z][a-z0-9+.-]*:/i.test(url))
|
|
24
22
|
return undefined;
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
const urlChanged = options.priorTarget?.url ? normalizeComparableUrl(options.priorTarget.url) !== normalizeComparableUrl(url) : undefined;
|
|
24
|
+
if (isAboutBlankUrl(url))
|
|
25
|
+
return { url, ...(urlChanged !== undefined ? { urlChanged } : {}) };
|
|
26
|
+
// Reuse the title already observed for this exact URL instead of spending a second probe. Titles can
|
|
27
|
+
// change without a URL change on SPAs, but this summary is only a "last observed" page label; the URL
|
|
28
|
+
// stays live-probed on every call.
|
|
29
|
+
if (options.reusePriorTitle !== false && options.priorTarget?.title && normalizeComparableUrl(options.priorTarget.url) === normalizeComparableUrl(url)) {
|
|
30
|
+
return { title: options.priorTarget.title, url, urlChanged: false };
|
|
31
|
+
}
|
|
27
32
|
const title = extractStringResultField(await runSessionCommandData({ args: ["get", "title"], cwd: options.cwd, namespace: options.namespace, sessionName: options.sessionName, signal: options.signal }), "title");
|
|
28
|
-
return { title, url };
|
|
33
|
+
return { title, url, ...(urlChanged !== undefined ? { urlChanged } : {}) };
|
|
29
34
|
}
|
|
30
35
|
function extractScrollPositionSnapshot(data) {
|
|
31
36
|
const result = isRecord(data) && isRecord(data.result) ? data.result : data;
|
|
@@ -83,6 +88,21 @@ function sameScrollPositionSnapshot(left, right) {
|
|
|
83
88
|
return other?.id === container.id && other.scrollTop === container.scrollTop && other.scrollLeft === container.scrollLeft;
|
|
84
89
|
});
|
|
85
90
|
}
|
|
91
|
+
export function buildUnsupportedScrollIntoViewRecovery(options) {
|
|
92
|
+
if (!["scrollintoview", "scrollinto"].includes(options.commandTokens[0] ?? "") || options.commandTokens.some((token) => token === "--help" || token === "-h"))
|
|
93
|
+
return undefined;
|
|
94
|
+
const match = /^text=(.+)$/s.exec(options.commandTokens[1] ?? "");
|
|
95
|
+
if (!match)
|
|
96
|
+
return undefined;
|
|
97
|
+
const text = redactSensitiveText(match[1]);
|
|
98
|
+
return {
|
|
99
|
+
error: "scrollintoview accepts a CSS selector, xpath=..., or a current @e… ref; text=... is not supported and can falsely report success without moving the page.",
|
|
100
|
+
nextActions: [
|
|
101
|
+
{ id: "scroll-semantic-text-target", params: { args: withOptionalSessionArgs(options.sessionName, ["find", "text", text, "hover"]) }, reason: "Use the upstream semantic text locator; hover resolves and scrolls the matched element into view.", safety: "Hover may open a tooltip or menu. Use the snapshot/ref recovery instead when hover state could affect the workflow.", tool: "agent_browser" },
|
|
102
|
+
{ id: "refresh-refs-for-scroll-target", params: { args: withOptionalSessionArgs(options.sessionName, ["snapshot", "-i"]) }, reason: "Capture a current element ref, then retry scrollintoview with that @e… ref.", safety: "Read-only snapshot; choose the intended current ref before retrying the scroll.", tool: "agent_browser" },
|
|
103
|
+
],
|
|
104
|
+
};
|
|
105
|
+
}
|
|
86
106
|
export function buildScrollNoopDiagnostic(before, after) {
|
|
87
107
|
if (!before || !after || !sameScrollPositionSnapshot(before, after))
|
|
88
108
|
return undefined;
|
|
@@ -390,14 +410,14 @@ function isBroadGetTextSelector(selector) {
|
|
|
390
410
|
return normalized === "body" || normalized === "html" || normalized === ":root" || normalized === "*" || normalized === "main" || normalized === "div" || normalized === "section" || normalized === "article" || /^\[role=(?:"application"|'application'|application)\]$/i.test(normalized);
|
|
391
411
|
}
|
|
392
412
|
function getElectronTextScopeContext(options) {
|
|
393
|
-
const record = findElectronLaunchRecordForSession(options.sessionName, options.electronLaunchRecords);
|
|
413
|
+
const record = findElectronLaunchRecordForSession(options.sessionName, options.electronLaunchRecords, options.namespace);
|
|
394
414
|
if (!record)
|
|
395
415
|
return undefined;
|
|
396
416
|
const url = options.currentTarget?.url ?? options.priorTarget?.url;
|
|
397
417
|
return { launchId: record.launchId, sessionName: record.sessionName ?? options.sessionName, url };
|
|
398
418
|
}
|
|
399
419
|
export function getSourceLookupElectronContext(options) {
|
|
400
|
-
const record = findElectronLaunchRecordForSession(options.sessionName, options.electronLaunchRecords);
|
|
420
|
+
const record = findElectronLaunchRecordForSession(options.sessionName, options.electronLaunchRecords, options.namespace);
|
|
401
421
|
if (!record)
|
|
402
422
|
return undefined;
|
|
403
423
|
const url = options.currentTarget?.url ?? options.priorTarget?.url;
|
|
@@ -469,9 +489,11 @@ export function formatEvalResultWarningText(warning) {
|
|
|
469
489
|
return warning ? `Eval result warning: ${warning.reason} ${warning.suggestion}` : undefined;
|
|
470
490
|
}
|
|
471
491
|
export async function getArtifactCleanupGuidance(options) {
|
|
472
|
-
if (!options.succeeded || !isCloseCommand(options.command) || !options.manifest
|
|
492
|
+
if (!options.succeeded || !isCloseCommand(options.command) || !options.manifest)
|
|
473
493
|
return undefined;
|
|
474
494
|
const explicitEntries = options.manifest.entries.filter((entry) => entry.storageScope === "explicit-path");
|
|
495
|
+
if (explicitEntries.length === 0)
|
|
496
|
+
return undefined;
|
|
475
497
|
const explicitArtifactPaths = [];
|
|
476
498
|
const seenPaths = new Set();
|
|
477
499
|
for (const entry of explicitEntries) {
|
|
@@ -490,16 +512,15 @@ export async function getArtifactCleanupGuidance(options) {
|
|
|
490
512
|
seenPaths.add(displayPath);
|
|
491
513
|
explicitArtifactPaths.push(displayPath);
|
|
492
514
|
}
|
|
515
|
+
if (explicitArtifactPaths.length === 0)
|
|
516
|
+
return undefined;
|
|
493
517
|
return { explicitArtifactPaths, 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.", owner: "host-file-tools", summary: formatSessionArtifactRetentionSummary(options.manifest) };
|
|
494
518
|
}
|
|
495
519
|
export function formatArtifactCleanupGuidanceText(guidance) {
|
|
496
|
-
if (!guidance)
|
|
520
|
+
if (!guidance || guidance.explicitArtifactPaths.length === 0)
|
|
497
521
|
return undefined;
|
|
498
522
|
const explicitCount = guidance.explicitArtifactPaths.length;
|
|
499
|
-
|
|
500
|
-
? "No existing explicit artifact paths were found in the recent manifest."
|
|
501
|
-
: `${explicitCount} explicit artifact${explicitCount === 1 ? "" : "s"} remain${explicitCount === 1 ? "s" : ""}; expand or inspect details.artifactCleanup.explicitArtifactPaths for paths.`;
|
|
502
|
-
return `Artifact lifecycle: ${explicitSummary} Browser close does not delete explicit screenshots, downloads, PDFs, traces, HAR files, or recordings; use host file tools for cleanup.`;
|
|
523
|
+
return `Artifact lifecycle: ${explicitCount} explicit artifact${explicitCount === 1 ? "" : "s"} remain${explicitCount === 1 ? "s" : ""}; expand or inspect details.artifactCleanup.explicitArtifactPaths for paths. Browser close does not delete explicit screenshots, downloads, PDFs, traces, HAR files, or recordings; use host file tools for cleanup.`;
|
|
503
524
|
}
|
|
504
525
|
async function collectManagedSessionCommandData(options) {
|
|
505
526
|
try {
|
|
@@ -524,14 +545,11 @@ async function collectElectronManagedSessionUrl(options) {
|
|
|
524
545
|
export async function collectElectronManagedSessionTarget(options) {
|
|
525
546
|
if (!options.sessionName)
|
|
526
547
|
return undefined;
|
|
527
|
-
const urlResult = await collectManagedSessionCommandData({
|
|
548
|
+
const urlResult = await collectManagedSessionCommandData({ args: ["get", "url"], cwd: options.cwd, namespace: options.namespace, sessionName: options.sessionName, signal: options.signal, timeoutMs: options.timeoutMs });
|
|
528
549
|
const url = boundElectronProbeString(extractStringResultField(urlResult.data, "result") ?? extractStringResultField(urlResult.data, "url"), 300);
|
|
529
550
|
if (urlResult.error || !url)
|
|
530
551
|
return { error: urlResult.error ?? "get url returned no active page URL.", sessionName: options.sessionName };
|
|
531
|
-
const
|
|
532
|
-
if (fileAccessError)
|
|
533
|
-
return { error: fileAccessError, sessionName: options.sessionName, url };
|
|
534
|
-
const titleResult = await collectManagedSessionCommandData({ allowManagedSessionTarget: options.allowManagedSessionTarget, args: ["get", "title"], cwd: options.cwd, namespace: options.namespace, sessionName: options.sessionName, signal: options.signal, timeoutMs: options.timeoutMs });
|
|
552
|
+
const titleResult = await collectManagedSessionCommandData({ args: ["get", "title"], cwd: options.cwd, namespace: options.namespace, sessionName: options.sessionName, signal: options.signal, timeoutMs: options.timeoutMs });
|
|
535
553
|
const title = boundElectronProbeString(extractStringResultField(titleResult.data, "result") ?? extractStringResultField(titleResult.data, "title"), 160);
|
|
536
554
|
return { sessionName: options.sessionName, title, url, ...(titleResult.error ? { error: titleResult.error } : {}) };
|
|
537
555
|
}
|
|
@@ -559,7 +577,7 @@ export function buildQaAttachedRecoveryNextActions(sessionName) {
|
|
|
559
577
|
buildNextToolAction({
|
|
560
578
|
args: sessionArgs(["snapshot", "-i"]),
|
|
561
579
|
id: "snapshot-before-qa-attached",
|
|
562
|
-
reason: "Capture interactive refs on the active
|
|
580
|
+
reason: "Capture interactive refs on the active page before retrying qa.attached.",
|
|
563
581
|
safety: "Read-only snapshot; confirms a renderable page is selected.",
|
|
564
582
|
}),
|
|
565
583
|
];
|
|
@@ -581,13 +599,7 @@ export async function validateQaAttachedPrecondition(options) {
|
|
|
581
599
|
const url = urlProbe.url?.trim();
|
|
582
600
|
if (!url) {
|
|
583
601
|
return {
|
|
584
|
-
error: "qa.attached requires an attached session with a readable
|
|
585
|
-
nextActions: buildQaAttachedRecoveryNextActions(options.sessionName),
|
|
586
|
-
};
|
|
587
|
-
}
|
|
588
|
-
if (!isHttpOrHttpsUrl(url)) {
|
|
589
|
-
return {
|
|
590
|
-
error: `qa.attached requires an http(s) page URL; the current attached URL is "${url}". Use tab list and snapshot -i to recover a web surface before retrying.`,
|
|
602
|
+
error: "qa.attached requires an attached session with a readable page URL. Run tab list, select a stable tab, then snapshot -i before retrying.",
|
|
591
603
|
nextActions: buildQaAttachedRecoveryNextActions(options.sessionName),
|
|
592
604
|
};
|
|
593
605
|
}
|
|
@@ -679,9 +691,6 @@ export async function collectElectronHandoff(options) {
|
|
|
679
691
|
const url = extractStringResultField(urlData, "result") ?? extractStringResultField(urlData, "url");
|
|
680
692
|
if (!url)
|
|
681
693
|
throw new Error("Electron handoff get url returned no active page URL.");
|
|
682
|
-
const fileAccessError = getManagedSessionStateAccessValidationError({ args: ["snapshot", "-i"], currentPageUrl: url, cwd: options.cwd });
|
|
683
|
-
if (fileAccessError)
|
|
684
|
-
return { error: fileAccessError, failureCategory: "validation-error", handoff: options.handoff };
|
|
685
694
|
const tabs = await runSessionCommandData({ args: ["tab", "list"], cwd: options.cwd, namespace: options.namespace, sessionName: options.sessionName, signal: options.signal, throwOnFailure: true });
|
|
686
695
|
if (options.signal?.aborted)
|
|
687
696
|
throw new Error("Electron handoff was aborted.");
|
|
@@ -718,24 +727,18 @@ function getLastPositionalToken(args, startIndex = 1) {
|
|
|
718
727
|
return undefined;
|
|
719
728
|
}
|
|
720
729
|
function getTimeoutStepArtifactPath(args) {
|
|
721
|
-
const
|
|
730
|
+
const commandArgs = extractUpstreamCommandTokens(args);
|
|
731
|
+
const [command] = commandArgs;
|
|
722
732
|
if (command === "screenshot") {
|
|
723
|
-
const index = getScreenshotPathTokenIndex(
|
|
724
|
-
return index === undefined ? undefined :
|
|
733
|
+
const index = getScreenshotPathTokenIndex(commandArgs);
|
|
734
|
+
return index === undefined ? undefined : commandArgs[index];
|
|
725
735
|
}
|
|
726
736
|
if (command === "pdf")
|
|
727
|
-
return getLastPositionalToken(
|
|
737
|
+
return getLastPositionalToken(commandArgs);
|
|
728
738
|
if (command === "download")
|
|
729
|
-
return getLastPositionalToken(
|
|
730
|
-
if (command === "wait")
|
|
731
|
-
|
|
732
|
-
if (inlineDownload)
|
|
733
|
-
return inlineDownload.slice("--download=".length) || undefined;
|
|
734
|
-
const downloadIndex = args.indexOf("--download");
|
|
735
|
-
const downloadPath = downloadIndex >= 0 ? args[downloadIndex + 1] : undefined;
|
|
736
|
-
if (downloadPath && !downloadPath.startsWith("-"))
|
|
737
|
-
return downloadPath;
|
|
738
|
-
}
|
|
739
|
+
return getLastPositionalToken(commandArgs, 2);
|
|
740
|
+
if (command === "wait")
|
|
741
|
+
return parseWaitCommandTokens(commandArgs).downloadPath;
|
|
739
742
|
return undefined;
|
|
740
743
|
}
|
|
741
744
|
async function statTimeoutArtifactPath(absolutePath) {
|
|
@@ -874,7 +877,7 @@ export async function collectTimeoutPartialProgress(options) {
|
|
|
874
877
|
const artifacts = await collectTimeoutArtifactEvidence(options.cwd, rawSteps);
|
|
875
878
|
const urlData = await runSessionCommandData({ args: ["get", "url"], cwd: options.cwd, namespace: options.namespace, sessionName: options.sessionName });
|
|
876
879
|
const recoveredUrl = extractStringResultField(urlData, "result") ?? extractStringResultField(urlData, "url");
|
|
877
|
-
const titleData = recoveredUrl
|
|
880
|
+
const titleData = recoveredUrl
|
|
878
881
|
? await runSessionCommandData({ args: ["get", "title"], cwd: options.cwd, namespace: options.namespace, sessionName: options.sessionName })
|
|
879
882
|
: undefined;
|
|
880
883
|
const title = extractStringResultField(titleData, "result") ?? extractStringResultField(titleData, "title");
|
|
@@ -909,7 +912,7 @@ function sanitizeCurrentPageUrlForTimeoutDiagnostic(url) {
|
|
|
909
912
|
return redactSensitivePathSegmentsForDiagnostic(redactSensitiveText(url));
|
|
910
913
|
}
|
|
911
914
|
}
|
|
912
|
-
export function formatTimeoutPartialProgressText(progress) {
|
|
915
|
+
export function formatTimeoutPartialProgressText(progress, pageTargetUnknown = false) {
|
|
913
916
|
const lines = [`Timeout partial progress: ${progress.summary}`];
|
|
914
917
|
const currentPageTitle = progress.currentPage?.title ? redactSensitivePathSegmentsForDiagnostic(redactSensitiveText(progress.currentPage.title)) : undefined;
|
|
915
918
|
const currentPageUrl = progress.currentPage?.url ? sanitizeCurrentPageUrlForTimeoutDiagnostic(progress.currentPage.url) : undefined;
|
|
@@ -927,7 +930,10 @@ export function formatTimeoutPartialProgressText(progress) {
|
|
|
927
930
|
lines.push(`- ... ${progress.steps.length - shownSteps.length} more step${progress.steps.length - shownSteps.length === 1 ? "" : "s"} omitted`);
|
|
928
931
|
}
|
|
929
932
|
if (progress.retryStep?.retry?.args) {
|
|
930
|
-
|
|
933
|
+
const payload = JSON.stringify({ args: redactInvocationArgs(progress.retryStep.retry.args) });
|
|
934
|
+
lines.push(pageTargetUnknown
|
|
935
|
+
? `Retry candidate for step ${progress.retryStep.index}: ${payload}. Verify the current URL before running it.`
|
|
936
|
+
: `Retry failed step: ${payload}`);
|
|
931
937
|
}
|
|
932
938
|
for (const artifact of progress.artifacts)
|
|
933
939
|
lines.push(`Artifact from step ${artifact.stepIndex}: ${redactSensitivePathSegmentsForDiagnostic(artifact.path)} (${artifact.exists ? `exists${typeof artifact.sizeBytes === "number" ? `, ${artifact.sizeBytes} bytes` : ""}` : "missing"})`);
|