pi-agent-browser-native 0.6.6 → 0.6.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -1
- package/README.md +20 -5
- package/dist/extensions/agent-browser/index.js +138 -75
- package/dist/extensions/agent-browser/lib/command-taxonomy.js +6 -5
- package/dist/extensions/agent-browser/lib/electron/cleanup.js +10 -1
- package/dist/extensions/agent-browser/lib/input-modes/params.js +20 -7
- package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +0 -1
- package/dist/extensions/agent-browser/lib/managed-session-restore.js +13 -12
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.js +6 -25
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +2 -3
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +10 -3
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +3 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/wait-timeouts.js +1 -1
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +104 -117
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +61 -35
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +100 -135
- package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +3 -1
- package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +3 -1
- package/dist/extensions/agent-browser/lib/page-target-validation.js +10 -10
- package/dist/extensions/agent-browser/lib/parsing.js +7 -0
- package/dist/extensions/agent-browser/lib/playbook.js +5 -8
- package/dist/extensions/agent-browser/lib/process.js +23 -7
- package/dist/extensions/agent-browser/lib/recording-reservations.js +3 -1
- package/dist/extensions/agent-browser/lib/results/envelope.js +4 -1
- package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +45 -43
- package/dist/extensions/agent-browser/lib/results/recovery-actions.js +4 -4
- package/dist/extensions/agent-browser/lib/runtime.js +18 -2
- package/dist/extensions/agent-browser/lib/session-page-state.js +29 -10
- package/docs/ARCHITECTURE.md +7 -4
- package/docs/COMMAND_REFERENCE.md +22 -15
- package/docs/ELECTRON.md +6 -6
- package/docs/RELEASE.md +14 -5
- package/docs/REQUIREMENTS.md +1 -1
- package/docs/SUPPORT_MATRIX.md +13 -3
- package/docs/TOOL_CONTRACT.md +33 -22
- package/package.json +1 -1
|
@@ -1,22 +1,43 @@
|
|
|
1
|
-
import { readFile, stat } from "node:fs/promises";
|
|
1
|
+
import { open, readFile, stat } from "node:fs/promises";
|
|
2
2
|
import { extname, resolve } from "node:path";
|
|
3
3
|
import { getAgentBrowserSessionIdentityKey } from "../../argv-grammar.js";
|
|
4
|
+
import { getExplicitArtifactDestination } from "../../orchestration/browser-run/artifact-paths.js";
|
|
4
5
|
import { isRecord, parsePositiveInteger } from "../../parsing.js";
|
|
6
|
+
import { extractUpstreamCommandTokens } from "../../runtime.js";
|
|
5
7
|
import { formatSessionArtifactRetentionSummary, getSessionArtifactManifestEntryKey, isPendingRecordingArtifact, isPendingRecordingCommand, mergeSessionArtifactManifest, } from "../artifact-manifest.js";
|
|
6
8
|
import { classifyAgentBrowserSuccessCategory } from "../categories.js";
|
|
7
|
-
const
|
|
8
|
-
".gif": "image/gif",
|
|
9
|
-
".jpeg": "image/jpeg",
|
|
10
|
-
".jpg": "image/jpeg",
|
|
11
|
-
".png": "image/png",
|
|
12
|
-
".webp": "image/webp",
|
|
13
|
-
};
|
|
9
|
+
const PNG_HEADER = Buffer.from("89504e470d0a1a0a0000000d49484452", "hex");
|
|
14
10
|
const INLINE_IMAGE_MAX_BYTES_ENV = "PI_AGENT_BROWSER_INLINE_IMAGE_MAX_BYTES";
|
|
15
11
|
const DEFAULT_INLINE_IMAGE_MAX_BYTES = 5 * 1_024 * 1_024;
|
|
16
12
|
const ARTIFACT_MTIME_TOLERANCE_MS = 2_000;
|
|
17
|
-
function getImageMimeType(
|
|
18
|
-
|
|
19
|
-
|
|
13
|
+
function getImageMimeType(bytes) {
|
|
14
|
+
if (bytes.length < 16)
|
|
15
|
+
return undefined;
|
|
16
|
+
if (bytes.subarray(0, 16).equals(PNG_HEADER))
|
|
17
|
+
return "image/png";
|
|
18
|
+
if (bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff && bytes[3] !== 0xf7)
|
|
19
|
+
return "image/jpeg";
|
|
20
|
+
if (["GIF87a", "GIF89a"].includes(bytes.toString("utf8", 0, 6)))
|
|
21
|
+
return "image/gif";
|
|
22
|
+
if (bytes.toString("utf8", 0, 4) === "RIFF" && bytes.toString("utf8", 8, 12) === "WEBP")
|
|
23
|
+
return "image/webp";
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
async function getFileImageMimeType(path) {
|
|
27
|
+
try {
|
|
28
|
+
const file = await open(path, "r");
|
|
29
|
+
try {
|
|
30
|
+
const bytes = Buffer.alloc(16);
|
|
31
|
+
const { bytesRead } = await file.read(bytes, 0, bytes.length, 0);
|
|
32
|
+
return getImageMimeType(bytes.subarray(0, bytesRead));
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
await file.close();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
20
41
|
}
|
|
21
42
|
function getInlineImageMaxBytes(env = process.env) {
|
|
22
43
|
return parsePositiveInteger(env[INLINE_IMAGE_MAX_BYTES_ENV]) ?? DEFAULT_INLINE_IMAGE_MAX_BYTES;
|
|
@@ -74,17 +95,6 @@ const PATH_FIELD_CANDIDATES = [
|
|
|
74
95
|
"profilePath",
|
|
75
96
|
"videoPath",
|
|
76
97
|
];
|
|
77
|
-
const ARTIFACT_EXTENSION_TO_MEDIA_TYPE = {
|
|
78
|
-
".cpuprofile": "application/json",
|
|
79
|
-
".har": "application/json",
|
|
80
|
-
".html": "text/html",
|
|
81
|
-
".json": "application/json",
|
|
82
|
-
".pdf": "application/pdf",
|
|
83
|
-
".txt": "text/plain",
|
|
84
|
-
".webm": "video/webm",
|
|
85
|
-
".zip": "application/zip",
|
|
86
|
-
...IMAGE_EXTENSION_TO_MIME_TYPE,
|
|
87
|
-
};
|
|
88
98
|
function isDownloadWaitSubcommand(subcommand) {
|
|
89
99
|
return subcommand === "--download" || subcommand === "-d";
|
|
90
100
|
}
|
|
@@ -153,6 +163,7 @@ async function buildFileArtifactMetadata(options) {
|
|
|
153
163
|
const pendingRecording = isPendingRecordingCommand(options.commandInfo.command, options.commandInfo.subcommand, kind);
|
|
154
164
|
let exists;
|
|
155
165
|
let sizeBytes;
|
|
166
|
+
let mediaType;
|
|
156
167
|
let stale = false;
|
|
157
168
|
let updatedAtMs;
|
|
158
169
|
if (!pendingRecording) {
|
|
@@ -161,6 +172,7 @@ async function buildFileArtifactMetadata(options) {
|
|
|
161
172
|
exists = true;
|
|
162
173
|
sizeBytes = fileStats.size;
|
|
163
174
|
updatedAtMs = fileStats.mtimeMs;
|
|
175
|
+
mediaType = fileStats.isFile() ? await getFileImageMimeType(absolutePath) : undefined;
|
|
164
176
|
const commandCreatesArtifact = !(options.commandInfo.command === "wait" && isDownloadWaitSubcommand(options.commandInfo.subcommand));
|
|
165
177
|
stale = commandCreatesArtifact && artifactMtimeIsOutsideCommandWindow(updatedAtMs, options.artifactMinUpdatedAtMs, options.artifactMaxUpdatedAtMs);
|
|
166
178
|
}
|
|
@@ -176,11 +188,11 @@ async function buildFileArtifactMetadata(options) {
|
|
|
176
188
|
exists,
|
|
177
189
|
extension,
|
|
178
190
|
kind,
|
|
179
|
-
mediaType
|
|
191
|
+
mediaType,
|
|
180
192
|
namespace: options.namespace,
|
|
181
193
|
path: displayPath,
|
|
182
194
|
recordingState: pendingRecording ? "openRecording" : undefined,
|
|
183
|
-
requestedPath: options.artifactRequest?.path,
|
|
195
|
+
requestedPath: options.artifactRequest?.path ?? getExplicitArtifactDestination(extractUpstreamCommandTokens(options.commandInfo.commandTokens ?? [])),
|
|
184
196
|
session: options.sessionName,
|
|
185
197
|
sizeBytes,
|
|
186
198
|
status: pendingRecording ? "pending" : exists === false ? "missing" : stale ? "stale" : options.artifactRequest?.status ?? "saved",
|
|
@@ -212,7 +224,7 @@ async function buildPreviousRestartRecordingArtifact(options) {
|
|
|
212
224
|
exists: true,
|
|
213
225
|
extension: previousRecording.extension ?? (extname(absolutePath).toLowerCase() || undefined),
|
|
214
226
|
kind: "video",
|
|
215
|
-
mediaType:
|
|
227
|
+
mediaType: fileStats.isFile() ? await getFileImageMimeType(absolutePath) : undefined,
|
|
216
228
|
namespace: previousRecording.namespace ?? options.namespace,
|
|
217
229
|
path: previousRecording.path,
|
|
218
230
|
requestedPath: previousRecording.requestedPath,
|
|
@@ -232,7 +244,6 @@ async function buildPreviousRestartRecordingArtifact(options) {
|
|
|
232
244
|
exists: false,
|
|
233
245
|
extension: previousRecording.extension ?? (extname(absolutePath).toLowerCase() || undefined),
|
|
234
246
|
kind: "video",
|
|
235
|
-
mediaType: previousRecording.mediaType,
|
|
236
247
|
namespace: previousRecording.namespace ?? options.namespace,
|
|
237
248
|
path: previousRecording.path,
|
|
238
249
|
requestedPath: previousRecording.requestedPath,
|
|
@@ -449,13 +460,12 @@ export function formatArtifactMetadataLines(artifacts) {
|
|
|
449
460
|
return [
|
|
450
461
|
`${formatArtifactLabel(artifact)}: ${artifact.path}`,
|
|
451
462
|
`Artifact type: ${artifact.kind}`,
|
|
452
|
-
`Requested path: ${artifact.requestedPath
|
|
463
|
+
artifact.requestedPath ? `Requested path: ${artifact.requestedPath}` : undefined,
|
|
453
464
|
`Absolute path: ${artifact.absolutePath}`,
|
|
454
465
|
"Exists: pending until record stop",
|
|
455
466
|
`Status: ${artifact.status ?? "pending"}`,
|
|
456
467
|
`Recording state: ${artifact.recordingState ?? "openRecording"}`,
|
|
457
468
|
`Will exist on stop: ${artifact.willExistOnStop !== false}`,
|
|
458
|
-
artifact.subcommand === "start" ? "Page state: record start uses a fresh active page for video capture; prior in-page DOM and JavaScript state does not carry over. Take a fresh snapshot before continuing." : undefined,
|
|
459
469
|
artifact.session ? `Session: ${artifact.session}` : undefined,
|
|
460
470
|
artifact.cwd ? `CWD: ${artifact.cwd}` : undefined,
|
|
461
471
|
`Machine data: details.artifacts[${index}]`,
|
|
@@ -464,14 +474,14 @@ export function formatArtifactMetadataLines(artifacts) {
|
|
|
464
474
|
return [
|
|
465
475
|
`${formatArtifactLabel(artifact)}: ${artifact.path}`,
|
|
466
476
|
`Artifact type: ${artifact.kind}`,
|
|
467
|
-
`Requested path: ${artifact.requestedPath
|
|
477
|
+
artifact.requestedPath ? `Requested path: ${artifact.requestedPath}` : undefined,
|
|
468
478
|
`Absolute path: ${artifact.absolutePath}`,
|
|
469
479
|
`Exists: ${artifact.exists === true}`,
|
|
470
480
|
artifact.exists === false ? "not found on disk" : undefined,
|
|
471
481
|
typeof artifact.sizeBytes === "number" ? `Size: ${formatByteCount(artifact.sizeBytes)}` : undefined,
|
|
472
482
|
typeof artifact.sizeBytes === "number" ? `Size bytes: ${artifact.sizeBytes}` : undefined,
|
|
473
483
|
`Status: ${artifact.status ?? (artifact.exists === false ? "missing" : "saved")}`,
|
|
474
|
-
artifact.tempPath ? `
|
|
484
|
+
artifact.tempPath ? `Reported path: ${artifact.tempPath}` : undefined,
|
|
475
485
|
artifact.mediaType ? `Media type: ${artifact.mediaType}` : undefined,
|
|
476
486
|
artifact.session ? `Session: ${artifact.session}` : undefined,
|
|
477
487
|
artifact.cwd ? `CWD: ${artifact.cwd}` : undefined,
|
|
@@ -519,21 +529,10 @@ export function extractImagePath(commandInfo, cwd, data) {
|
|
|
519
529
|
if (!isTrustedScreenshotOutput(commandInfo)) {
|
|
520
530
|
return undefined;
|
|
521
531
|
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
return mimeType ? resolve(cwd, data) : undefined;
|
|
525
|
-
}
|
|
526
|
-
if (!isRecord(data) || typeof data.path !== "string") {
|
|
527
|
-
return undefined;
|
|
528
|
-
}
|
|
529
|
-
const mimeType = getImageMimeType(data.path);
|
|
530
|
-
return mimeType ? resolve(cwd, data.path) : undefined;
|
|
532
|
+
const path = typeof data === "string" ? data : isRecord(data) && typeof data.path === "string" ? data.path : undefined;
|
|
533
|
+
return path?.trim() && !isNonFileArtifactPathCandidate(path) ? resolve(cwd, path) : undefined;
|
|
531
534
|
}
|
|
532
535
|
export async function attachInlineImage(presentation, imagePath) {
|
|
533
|
-
const mimeType = getImageMimeType(imagePath);
|
|
534
|
-
if (!mimeType) {
|
|
535
|
-
return presentation;
|
|
536
|
-
}
|
|
537
536
|
try {
|
|
538
537
|
const fileStats = await stat(imagePath);
|
|
539
538
|
const inlineImageMaxBytes = getInlineImageMaxBytes();
|
|
@@ -543,6 +542,9 @@ export async function attachInlineImage(presentation, imagePath) {
|
|
|
543
542
|
return presentation;
|
|
544
543
|
}
|
|
545
544
|
const file = await readFile(imagePath);
|
|
545
|
+
const mimeType = getImageMimeType(file);
|
|
546
|
+
if (!mimeType)
|
|
547
|
+
return presentation;
|
|
546
548
|
presentation.content.push({ type: "image", data: file.toString("base64"), mimeType });
|
|
547
549
|
presentation.imagePath = imagePath;
|
|
548
550
|
return presentation;
|
|
@@ -45,11 +45,11 @@ function buildTabSnapshotRecoveryAction(options) {
|
|
|
45
45
|
});
|
|
46
46
|
}
|
|
47
47
|
return buildNextToolAction({
|
|
48
|
-
args: options.sessionArgs(["batch"]),
|
|
48
|
+
args: options.sessionArgs(["batch", "--bail"]),
|
|
49
49
|
id: options.id,
|
|
50
|
-
reason: `${options.reason} The batch selects the stable tab before snapshotting.`,
|
|
51
|
-
safety: `${options.safety}
|
|
52
|
-
stdin: JSON.stringify([["tab", options.tabId], ["snapshot", "-i"]]),
|
|
50
|
+
reason: `${options.reason} The batch selects and verifies the stable tab before snapshotting.`,
|
|
51
|
+
safety: `${options.safety} A failed tab selection or URL check stops before the snapshot.`,
|
|
52
|
+
stdin: JSON.stringify([["tab", options.tabId], ["get", "url"], ["snapshot", "-i"]]),
|
|
53
53
|
});
|
|
54
54
|
}
|
|
55
55
|
export function buildRecoveryNextActions(recovery) {
|
|
@@ -2,7 +2,7 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
2
2
|
import { basename } from "node:path";
|
|
3
3
|
import { extractUpstreamCommandTokens, findCommandStartIndex, parseArgvDescriptor, parseCommandInfo, } from "./argv-descriptor.js";
|
|
4
4
|
import { batchHasSuccessfulCloseAll, getSuccessfulBatchCloseLifecycle } from "./batch-lifecycle.js";
|
|
5
|
-
import { canonicalizeAgentBrowserNamespace, extractExplicitNamespace, extractExplicitSessionName, getAgentBrowserSessionIdentityKey, getBooleanFlagValue, GLOBAL_BOOLEAN_FLAGS_WITH_OPTIONAL_VALUES, GLOBAL_VALUE_FLAGS_ALLOWING_DASH_VALUE, isAgentBrowserSessionIdentityKeyInNamespace, isUpstreamEnvFlagEnabled, PREVALIDATED_VALUE_FLAGS, resolveAgentBrowserNamespace, scanUpstreamGlobalFlagOccurrences, } from "./argv-grammar.js";
|
|
5
|
+
import { canonicalizeAgentBrowserNamespace, extractExplicitNamespace, extractExplicitSessionName, getAgentBrowserSessionIdentityKey, getBooleanFlagValue, GLOBAL_BOOLEAN_FLAGS_WITH_OPTIONAL_VALUES, GLOBAL_VALUE_FLAGS_ALLOWING_DASH_VALUE, isAgentBrowserSessionIdentityKeyInNamespace, isUpstreamEnvFlagEnabled, PREVALIDATED_VALUE_FLAGS, resolveAgentBrowserNamespace, scanUpstreamGlobalFlagOccurrences, stripUpstreamGlobalFlags, } from "./argv-grammar.js";
|
|
6
6
|
import { needsManagedSession } from "./command-policy.js";
|
|
7
7
|
import { isCloseAllCommand, isCloseCommand, isOpenNavigationCommand } from "./command-taxonomy.js";
|
|
8
8
|
import { hasLaunchScopedFlagToken, LAUNCH_SCOPED_FLAG_DEFINITIONS, LAUNCH_SCOPED_FLAG_LABEL, } from "./launch-scoped-flags.js";
|
|
@@ -607,6 +607,18 @@ function getUnsupportedInlineWaitDownloadError(args) {
|
|
|
607
607
|
return undefined;
|
|
608
608
|
return `agent-browser ${TARGET_AGENT_BROWSER_VERSION} does not support \`wait --download=<path>\`. Pass the optional path as a separate argument: \`wait --download <path>\` (or \`wait -d <path>\`).`;
|
|
609
609
|
}
|
|
610
|
+
function getBareNoSandboxValidationError(args, batchStep) {
|
|
611
|
+
// Native batch rows skip global parsing; --args is effective only on the outer CLI call.
|
|
612
|
+
const tokens = batchStep ? args : stripUpstreamGlobalFlags(args);
|
|
613
|
+
const command = tokens[0];
|
|
614
|
+
const leading = command === "--no-sandbox";
|
|
615
|
+
if (!leading && (!isOpenNavigationCommand(command) || !tokens.slice(1).includes("--no-sandbox")))
|
|
616
|
+
return undefined;
|
|
617
|
+
const explanation = leading
|
|
618
|
+
? "`--no-sandbox` is not an agent-browser command."
|
|
619
|
+
: `\`--no-sandbox\` is ignored as an option by \`${command}\`.`;
|
|
620
|
+
return `${explanation} It is a Chromium launch argument. Put it in top-level \`--args\` and start a fresh session: { args: ["--args", "--no-sandbox", "open", "https://example.com"], sessionMode: "fresh" }. For batch, put --args before batch, not inside a step.`;
|
|
621
|
+
}
|
|
610
622
|
export function validateToolArgs(args, options = {}) {
|
|
611
623
|
if (args.length === 0) {
|
|
612
624
|
return "`args` must contain at least one agent-browser command token.";
|
|
@@ -623,7 +635,8 @@ export function validateToolArgs(args, options = {}) {
|
|
|
623
635
|
const invalidValueFlag = inspection ? undefined : getInvalidValueFlagDetails(args, !options.batchStep);
|
|
624
636
|
if (invalidValueFlag?.reason === "unsupported-assignment")
|
|
625
637
|
return formatInvalidValueFlagError(invalidValueFlag, options.batchStep);
|
|
626
|
-
return
|
|
638
|
+
return (inspection ? undefined : getBareNoSandboxValidationError(args, options.batchStep === true))
|
|
639
|
+
?? getBareMcpValidationError(args) ?? getSingleKeyCommandValidationError(args) ?? getUnsupportedInlineWaitDownloadError(args);
|
|
627
640
|
}
|
|
628
641
|
function getInvalidValueFlagDetails(args, allowRestoreAssignment = true) {
|
|
629
642
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -885,6 +898,9 @@ export function buildExecutionPlan(args, options) {
|
|
|
885
898
|
managedSessionName = options.freshSessionName;
|
|
886
899
|
sessionName = options.freshSessionName;
|
|
887
900
|
}
|
|
901
|
+
if (commandInfo.command !== undefined && !sessionName && !explicitNamespacePresent) {
|
|
902
|
+
namespace = resolveAgentBrowserNamespace(args, getAgentBrowserProcessEnvironment().AGENT_BROWSER_NAMESPACE);
|
|
903
|
+
}
|
|
888
904
|
const targetsActiveManagedSession = options.managedSessionActive
|
|
889
905
|
&& sessionName
|
|
890
906
|
&& getAgentBrowserSessionIdentityKey(sessionName, namespace) === getAgentBrowserSessionIdentityKey(options.managedSessionName, options.managedSessionNamespace);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { extractUpstreamCommandTokens } from "./argv-descriptor.js";
|
|
2
2
|
import { getAgentBrowserSessionIdentityKey, isAgentBrowserSessionIdentityKeyInNamespace } from "./argv-grammar.js";
|
|
3
3
|
import { batchHasSuccessfulCloseAll, getSuccessfulBatchCloseLifecycle } from "./batch-lifecycle.js";
|
|
4
|
-
import { isCloseAllCommand, isCloseCommand, isReadOnlyDiagnosticSessionTargetCommand, isRecordPageTransitionCommand, isUnverifiedPageTransitionCommand, isWebMcpPageMutationCommand } from "./command-taxonomy.js";
|
|
4
|
+
import { isCloseAllCommand, isCloseCommand, isReadOnlyDiagnosticSessionTargetCommand, isRecordPageTransitionCommand, isUnverifiedPageTransitionCommand, isWebMcpPageMutationCommand, isWindowOrDiffPageTransitionCommand } from "./command-taxonomy.js";
|
|
5
5
|
import { isRecord } from "./parsing.js";
|
|
6
6
|
import { getEditableRefEvidence } from "./results/editable-ref-evidence.js";
|
|
7
7
|
import { enrichSnapshotRefEntries, getSnapshotRefEntries } from "./results/snapshot-refs.js";
|
|
@@ -24,8 +24,8 @@ export function normalizeSessionTabTarget(target) {
|
|
|
24
24
|
if (!target) {
|
|
25
25
|
return undefined;
|
|
26
26
|
}
|
|
27
|
-
const url =
|
|
28
|
-
if (!url) {
|
|
27
|
+
const url = target.url?.trim();
|
|
28
|
+
if (!url || normalizeComparableUrl(url) === undefined) {
|
|
29
29
|
return undefined;
|
|
30
30
|
}
|
|
31
31
|
const title = target.title?.trim();
|
|
@@ -38,7 +38,7 @@ export function isAboutBlankSessionTabTarget(target) {
|
|
|
38
38
|
return isAboutBlankUrl(target?.url);
|
|
39
39
|
}
|
|
40
40
|
export function commandExplicitlyTargetsAboutBlank(commandTokens) {
|
|
41
|
-
return commandTokens.some((token) => isAboutBlankUrl(token));
|
|
41
|
+
return (commandTokens[0] === "window" && commandTokens[1] === "new") || commandTokens.some((token) => isAboutBlankUrl(token));
|
|
42
42
|
}
|
|
43
43
|
export function targetsMatch(left, right) {
|
|
44
44
|
if (!left || !right)
|
|
@@ -90,10 +90,15 @@ export function extractSessionTabTargetFromBatchResults(data) {
|
|
|
90
90
|
let currentTarget;
|
|
91
91
|
let pendingTitle;
|
|
92
92
|
for (const item of data) {
|
|
93
|
-
if (!isRecord(item)
|
|
93
|
+
if (!isRecord(item))
|
|
94
94
|
continue;
|
|
95
|
+
const [name, subcommand] = extractUpstreamCommandTokens(extractBatchResultCommand(item));
|
|
96
|
+
if (isWindowOrDiffPageTransitionCommand(name, subcommand)) {
|
|
97
|
+
currentTarget = undefined;
|
|
98
|
+
pendingTitle = undefined;
|
|
95
99
|
}
|
|
96
|
-
|
|
100
|
+
if (item.success === false)
|
|
101
|
+
continue;
|
|
97
102
|
const result = item.result;
|
|
98
103
|
if (isCloseCommand(name)) {
|
|
99
104
|
currentTarget = undefined;
|
|
@@ -213,6 +218,9 @@ export function buildPageTransitionRefSnapshotInvalidation(summary) {
|
|
|
213
218
|
export function getCommandRefSnapshotInvalidation(commandTokens) {
|
|
214
219
|
if (isRecordPageTransitionCommand(commandTokens))
|
|
215
220
|
return buildPageTransitionRefSnapshotInvalidation();
|
|
221
|
+
if (isWindowOrDiffPageTransitionCommand(commandTokens[0], commandTokens[1])) {
|
|
222
|
+
return buildPageTransitionRefSnapshotInvalidation("A window new or diff url command replaced or navigated the active page and invalidated prior refs. Run snapshot -i before using page-scoped refs.");
|
|
223
|
+
}
|
|
216
224
|
if (isWebMcpPageMutationCommand(commandTokens)) {
|
|
217
225
|
return buildPageTransitionRefSnapshotInvalidation("A WebMCP invoke, result, or cancel command can mutate, rerender, or navigate the page, so the prior snapshot refs were invalidated. Run snapshot -i before using page-scoped refs.");
|
|
218
226
|
}
|
|
@@ -228,7 +236,7 @@ export function extractLatestRefSnapshotStateFromBatchResults(data) {
|
|
|
228
236
|
for (const item of data) {
|
|
229
237
|
if (!isRecord(item))
|
|
230
238
|
continue;
|
|
231
|
-
const commandTokens = extractBatchResultCommand(item);
|
|
239
|
+
const commandTokens = extractUpstreamCommandTokens(extractBatchResultCommand(item));
|
|
232
240
|
const [name] = commandTokens;
|
|
233
241
|
if (item.success !== false && isCloseCommand(name)) {
|
|
234
242
|
latestState = undefined;
|
|
@@ -372,9 +380,10 @@ export class SessionPageState {
|
|
|
372
380
|
}
|
|
373
381
|
const tabTarget = getRestoredSessionTabTarget(details, command, subcommand);
|
|
374
382
|
const tabTargetUnknown = details.sessionTabTargetUnknown === true;
|
|
383
|
+
const reopenPending = typeof details.sessionTabReopenPending === "boolean" ? details.sessionTabReopenPending : undefined;
|
|
375
384
|
const refSnapshotInvalidation = getRestoredRefSnapshotInvalidation(details, command);
|
|
376
385
|
const refSnapshot = refSnapshotInvalidation ? undefined : getRestoredRefSnapshot(details);
|
|
377
|
-
if (!tabTarget && !tabTargetUnknown && !refSnapshotInvalidation && !refSnapshot)
|
|
386
|
+
if (!tabTarget && !tabTargetUnknown && !refSnapshotInvalidation && !refSnapshot && reopenPending === undefined)
|
|
378
387
|
continue;
|
|
379
388
|
restoredOrder += 1;
|
|
380
389
|
if (tabTargetUnknown) {
|
|
@@ -389,8 +398,11 @@ export class SessionPageState {
|
|
|
389
398
|
}
|
|
390
399
|
if (tabTarget) {
|
|
391
400
|
state.tabTargetUnknownOrders.delete(sessionKey);
|
|
392
|
-
state.tabTargets.set(sessionKey, { order: restoredOrder, target: tabTarget });
|
|
401
|
+
state.tabTargets.set(sessionKey, { order: restoredOrder, reopenPending: state.tabTargets.get(sessionKey)?.reopenPending, target: tabTarget });
|
|
393
402
|
}
|
|
403
|
+
const currentTarget = state.tabTargets.get(sessionKey);
|
|
404
|
+
if (currentTarget && reopenPending !== undefined)
|
|
405
|
+
currentTarget.reopenPending = reopenPending;
|
|
394
406
|
if (refSnapshotInvalidation) {
|
|
395
407
|
state.refSnapshots.delete(sessionKey);
|
|
396
408
|
state.refSnapshotInvalidations.set(sessionKey, { ...refSnapshotInvalidation, order: restoredOrder });
|
|
@@ -421,6 +433,7 @@ export class SessionPageState {
|
|
|
421
433
|
return {};
|
|
422
434
|
return {
|
|
423
435
|
pinningReason: this.tabPinningReasons.get(sessionName),
|
|
436
|
+
...(this.tabTargets.get(sessionName)?.reopenPending !== undefined ? { tabReopenPending: this.tabTargets.get(sessionName)?.reopenPending } : {}),
|
|
424
437
|
refSnapshot: stripRefSnapshotOrder(this.refSnapshots.get(sessionName)),
|
|
425
438
|
refSnapshotInvalidation: stripRefSnapshotInvalidationOrder(this.refSnapshotInvalidations.get(sessionName)),
|
|
426
439
|
...(this.tabTargetUnknownOrders.has(sessionName) ? { tabTargetUnknown: true } : {}),
|
|
@@ -433,9 +446,15 @@ export class SessionPageState {
|
|
|
433
446
|
return { ...this.get(options.sessionName), applied: false, stale: true };
|
|
434
447
|
}
|
|
435
448
|
this.tabTargetUnknownOrders.delete(options.sessionName);
|
|
436
|
-
this.tabTargets.set(options.sessionName, { order: options.update, target: options.target });
|
|
449
|
+
this.tabTargets.set(options.sessionName, { order: options.update, reopenPending: current?.reopenPending, target: options.target });
|
|
437
450
|
return { ...this.get(options.sessionName), applied: true };
|
|
438
451
|
}
|
|
452
|
+
setTabReopenPending(options) {
|
|
453
|
+
const current = this.tabTargets.get(options.sessionName);
|
|
454
|
+
if (!current || !shouldApplyTabTargetUpdate(current, this.tabTargetUnknownOrders.get(options.sessionName), options.update))
|
|
455
|
+
return;
|
|
456
|
+
this.tabTargets.set(options.sessionName, { ...current, order: options.update, reopenPending: options.pending });
|
|
457
|
+
}
|
|
439
458
|
applyRefSnapshot(options) {
|
|
440
459
|
if (!shouldApplyRefStateUpdate({
|
|
441
460
|
currentInvalidation: this.refSnapshotInvalidations.get(options.sessionName),
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -69,6 +69,8 @@ Browser isolation is separate from language isolation. A pre-spawn Pi custom ent
|
|
|
69
69
|
|
|
70
70
|
### Agent-first UX
|
|
71
71
|
|
|
72
|
+
Artifact directory preparation has one filesystem-error boundary inside the existing cleanup/finally path: direct, stdin and raw-argv failures return structured validation and the attempted directory without launching the requested command. Raw batch strings and argv-over-stdin precedence stay native; absolute artifact paths avoid differing daemon/Pi working directories. Artifact metadata retains known requested and reported/resolved paths without a new canonicalization pass. A bounded 16-byte regular-file header read recognizes the existing PNG/JPEG/GIF/WebP formats; inline screenshots use the same classifier under their existing byte limit, and unknown MIME types are omitted. Recording page warnings use the shared transition predicate and confirmed CLI/reached-row evidence through the existing prose/JSON warning path, independently of conservative ref-state invalidation after an uncertain batch.
|
|
73
|
+
|
|
72
74
|
The primary UX is the agent calling the tool directly.
|
|
73
75
|
|
|
74
76
|
That means:
|
|
@@ -157,6 +159,7 @@ V1 ownership rule:
|
|
|
157
159
|
Practical policy:
|
|
158
160
|
- preserve the current branch-visible extension-managed session across `/reload`, exact-session relaunch, `/resume`, and Pi 0.84.0+ `session_tree` branch transitions so persisted sessions can keep following the live browser after lifecycle changes
|
|
159
161
|
- close the active extension-managed session when the originating `pi` process quits, while leaving explicit caller-provided sessions alone
|
|
162
|
+
- after branch restore, use the existing locked daemon inspection to distinguish a confirmed inactive wrapper-owned daemon from a live, unknown, or unavailable one. For compatible automatic managed restore only, keep the pending reopen in ordered session page state and persist it as `sessionTabReopenPending`. Non-page calls such as `tab list` and explicit HTTP reads can start a daemon without fulfilling it, including across branch/reload replay. Before the first current-page operation (`get url`, history commands and relative `pushstate` included), reopen the complete recorded URL with native `open`, invalidate old refs with the existing `page-transition` state, and verify the actual tab. Native `open` resets frame scope. Consume the obligation on that attempt or an executed explicit context/navigation/close command, not an unreached batch row; a failed open does not permit repeated navigation of a now-live browser. After the reopen CLI starts, cancellation returns a structured `aborted` result through the ordinary result path with the exact namespace/session, consumed marker and ref invalidation; it does not throw away replay state or run later browser helpers. Cancellation before the CLI starts does not consume the pending reopen. Internal URLs retain their fragments while tab/ref comparisons remain fragment-insensitive and presentation keeps normal redaction. Older transcripts cannot recover a fragment they did not record. Caller-owned/attached browsers and restore-disabled sessions do not take this path; live wrong-tab recovery still only selects an existing target. Reopening reloads the URL, not unsaved forms, JavaScript memory, or history. There is no second restore store or lifecycle lock.
|
|
160
163
|
- set an idle timeout on extension-managed sessions as a backstop for abnormal exits or cleanup failures, and apply that same `AGENT_BROWSER_IDLE_TIMEOUT_MS` value to every upstream subprocess (including wrapper helper snapshots, tab lists, and navigation-summary reads) because changing the launch environment between calls can make upstream restart the background browser, discard the active tab, and invalidate fresh refs
|
|
161
164
|
- for wrapper-owned implicit sessions only, set a transcript- and checkout-scoped `AGENT_BROWSER_RESTORE` key on compatible calls so cookies and web storage can survive idle shutdown, reload, and resume. Explicit caller sessions, restore/state choices, profiles, upstream config, file access, launch arguments, environment variables, local pages, output paths, and close arguments remain upstream-owned and pass through unchanged. `piab-*` names are not reserved; session/state lists and restore identifiers are not filtered or redacted. The wrapper validates only its automatic restore checkout/storage identity and coordinates same-daemon reuse so its own restore pools cannot mix. Ambiguous page-target transitions still require live `get url` verification before content calls. The current v3 ticket-claim lock is the only managed-daemon coordination protocol; no earlier lock bridge or compatibility path remains.
|
|
162
165
|
- redact snapshot spill payloads before writing them, clean up process-private temp spill artifacts on shutdown, and keep persisted-session spill files in a private session-scoped artifact directory with a bounded per-session budget so `details.fullOutputPath` stays usable after reload/resume without unbounded growth
|
|
@@ -172,16 +175,16 @@ Practical policy:
|
|
|
172
175
|
- expose `details.browserWindow` and one visible login handoff only when a successful first/fresh local wrapper-managed headed result, including `batch`, is not an attachment and has upstream `lifecycle.effectiveLaunch.browserLaunched: true` and a `created`/`replaced` managed-session outcome. Keep `visibility: "unverified"`: this is launch evidence, never a claim about the user's OS desktop
|
|
173
176
|
- leave explicit caller-provided `--session` choices alone unless the caller closes them explicitly, but before any content-bearing read or interaction against a caller-owned explicit session, live-probe that session with `get url` instead of trusting missing or stale transcript page state; hold the effective canonical namespace/session queue from that probe through semantic snapshot resolution and the main command so another same-instance call cannot change tabs in between. Non-bail batch analysis retains every possible page left by a failed transition up to a fixed bound and blocks later content only when the target is unverified; exceeding the bound also fails closed to exact `batch --bail` guidance. Nested `batch` steps remain unsupported, and raw batch command strings mirror upstream's ASCII-space tokenizer, including quote/backslash handling, rather than splitting on other Unicode whitespace.
|
|
174
177
|
- after profiled `open` / `goto` / `navigate` calls, verify the active tab still matches the returned page URL and best-effort switch back when restored profile tabs steal focus
|
|
175
|
-
- once the wrapper observes tab-drift risk for a session (profile restore correction, overlapping stale opens, or restored session state), later active-tab commands
|
|
178
|
+
- once the wrapper observes tab-drift risk for a session (profile restore correction, overlapping stale opens, or restored session state), later active-tab commands verify the intended tab under the existing session queue before semantic/ref helpers and user commands. Native selection runs only when the intended tab is not already active, because upstream selection clears refs and frame scope even on same-tab reselection. Missing targets, failed selection, and post-selection target mismatches fail before user commands. Caller argv/stdin and native `--pin-tab` / `--no-pin-tab` preferences remain unchanged. Local commands, live `get url`, explicit HTTP `read <url>` (including its flags), URL `a11y`/`vitals`/`web-vitals`, `diff url`, `window new`, URL-bearing recording commands, and explicit tab/navigation/`connect`/`state load` recovery do not require the old target; history back/forward/reload, `pushstate`, and page-content operations still do. The same classifier scans effective batch rows past non-page prefixes until a page dependency or explicit context change, without rewriting user rows or changing bail behavior. For `window new` and `diff url`, observe the resulting URL instead of retaining the old target or treating the requested second URL as redirect evidence. Fold only reached native batch rows when available, discard observations and refs from before those transitions, and let later successful snapshots rebuild refs even when another batch row fails. Retain an observed blank destination after either command instead of recovering the old page; if no final target is observed, use the existing unknown-target state. Caller batch arguments, stdin and bail behavior stay unchanged. Routine same-session commands avoid `tab list` preflights
|
|
176
179
|
- for sessions with observed tab-drift risk, after a successful command on a known tab target, the wrapper may best-effort restore that same target again if restored/background tabs steal focus after the command returns; routine same-session commands skip this post-command `tab list` probe
|
|
177
180
|
- after successful standalone tab selection or `tab close`, read the now-active URL and fresh non-blank title—even when two tabs share a URL—before updating per-session page state because upstream selection/close payloads are not sufficient page-target evidence; retain an explicitly selected existing `about:blank` tab or a blank tab revealed by close instead of treating either as accidental drift
|
|
178
|
-
- keep a per-session `refSnapshot` aligned with the last successful `snapshot` (including refs merged from a successful `batch` by taking the last successful `snapshot` step in batch result order): restore it from persisted tool `details` when reloading, resuming, or moving to a different Pi session-tree branch, store bounded ref role/name metadata from the same snapshot for wrapper-side current-ref diagnostics, drop it on successful close commands (`close`, `quit`, or `exit`), replace it with a persisted `page-transition` invalidation after any upstream-executed `record start` attempt (direct or batch; upstream swaps the session to a fresh active page before its already-active check, so failed starts count), a `record restart` with a URL operand, or WebMCP `invoke` / `result` / `cancel` (these page-provided tools can mutate, rerender, or navigate; when a spawned `batch` yields no parseable result rows, for example after a wrapper timeout, planned transition steps still record the invalidation), or after a failed non-batch transition command (`eval`, `back`, `forward`, `reload`, `connect`, `state load`, `tab` selection) whose live URL re-verification probe observed the page (a failed transition can still have mutated or replaced the document before throwing, so keeping the verified URL must not keep the prior refs; transcript replay preserves the persisted invalidation summary), and refuse page-scoped `@e…` argv before spawn when the active tab URL no longer matches the snapshot URL, when a ref id was never in that snapshot, when the snapshot state is invalidated, or when a `batch` step would reuse `@e…` on a guarded getter or mutation step after an earlier invalidating step (including `record start`, URL-bearing `record restart`, and WebMCP `invoke` / `result` / `cancel`) without a later `snapshot` step in the same plan; batch steps come from the source upstream actually executes (raw batch argument strings exclusively when any exist — filtering only the exact `--bail` token like upstream — stdin only otherwise, via `getUpstreamEffectiveBatchSteps` in `extensions/agent-browser/lib/orchestration/batch-stdin.ts`);
|
|
181
|
+
- keep a per-session `refSnapshot` aligned with the last successful `snapshot` (including refs merged from a successful `batch` by taking the last successful `snapshot` step in batch result order): restore it from persisted tool `details` when reloading, resuming, or moving to a different Pi session-tree branch, store bounded ref role/name metadata from the same snapshot for wrapper-side current-ref diagnostics, drop it on successful close commands (`close`, `quit`, or `exit`), replace it with a persisted `page-transition` invalidation after any upstream-executed `record start` attempt (direct or batch; upstream swaps the session to a fresh active page before its already-active check, so failed starts count), a `record restart` with a URL operand, `window new`, `diff url`, or WebMCP `invoke` / `result` / `cancel` (these page-provided tools can mutate, rerender, or navigate; when a spawned `batch` yields no parseable result rows, for example after a wrapper timeout, planned transition steps still record the invalidation), or after a failed non-batch transition command (`eval`, `back`, `forward`, `reload`, `connect`, `state load`, `tab` selection) whose live URL re-verification probe observed the page (a failed transition can still have mutated or replaced the document before throwing, so keeping the verified URL must not keep the prior refs; transcript replay preserves the persisted invalidation summary), and refuse page-scoped `@e…` argv before spawn when the active tab URL no longer matches the snapshot URL, when a ref id was never in that snapshot, when the snapshot state is invalidated, or when a `batch` step would reuse `@e…` on a guarded getter or mutation step after an earlier invalidating step (including `record start`, URL-bearing `record restart`, and WebMCP `invoke` / `result` / `cancel`) without a later `snapshot` step in the same plan; batch steps come from the source upstream actually executes (raw batch argument strings exclusively when any exist — filtering only the exact `--bail` token like upstream — stdin only otherwise, via `getUpstreamEffectiveBatchSteps` in `extensions/agent-browser/lib/orchestration/batch-stdin.ts`); tab recovery (which leaves user argv/stdin and continue-on-error control flow unchanged), artifact/recording preflight, batch screenshot path preparation (parent directories are created for effective raw rows too, without rewriting raw strings), and stale-ref echo args use that same selection so pinning and preflights cannot act on upstream-ignored stdin, while the pre-spawn state-policy validator deliberately keeps scanning parseable stdin alongside argv as a fail-closed content superset and treats stdin parse failures as fatal only when upstream would actually read stdin (its raw-token filter also uses the exact `--bail` token only). Same-snapshot `fill @e…` rows are guarded but do not themselves set that invalidation latch, so ordinary form fills can precede a click/submit row in one batch—see [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#details) for the agent-visible contract and failure text; typed per-session tab/ref/pinning state lives in `extensions/agent-browser/lib/session-page-state.ts` and is updated from `extensions/agent-browser/index.ts` after each tool result
|
|
179
182
|
- when a direct or batched WebMCP call returns `status: "pending"`, or `result` / `cancel` fails while that target is unknown, keep its tab target unknown and discard same-call snapshot evidence instead of treating an immediate post-dispatch URL probe as stable; `webmcp result` / `cancel`, `get url`, and explicit navigation remain available while unknown; replace the generic blocked snapshot action with `verify-page-target-after-pending-webmcp` (`get url`), and let a completed `batch --bail` use that verification before `snapshot -i` to re-establish both target and refs
|
|
180
|
-
- for top-level non-Electron direct `click` commands with an eligible target, install a bounded in-page target-specific event probe before upstream runs; if upstream reports success but no trusted pointer/mouse/click event reached the resolved target, fail the tool and report `details.clickDispatch` with explicit retry/inspect next actions (the wrapper does not replay clicks in-page). The probe covers `xpath=` targets and current `@e…` / `ref=` refs whose latest stored `refSnapshot.refs` role is `button`, `checkbox`, `menuitem`, `radio`, `switch`, or `tab`; it
|
|
183
|
+
- for top-level non-Electron direct `click` commands with an eligible target, install a bounded in-page target-specific event probe before upstream runs; if upstream reports success but no trusted pointer/mouse/click event reached the resolved target, fail the tool and report `details.clickDispatch` with explicit retry/inspect next actions (the wrapper does not replay clicks in-page). The probe covers `xpath=` targets and current `@e…` / `ref=` refs whose latest stored `refSnapshot.refs` role is `button`, `checkbox`, `menuitem`, `radio`, `switch`, or `tab`; it requires a unique role/name in the saved snapshot and the live candidates instead of taking a fresh pre-click snapshot that could recycle upstream refs. Duplicate-name refs pass through without a probe: their old ordinal is not target identity. The probe is intentionally skipped for CSS selector clicks, unresolved `find … click` locators, and `batch`/`job`/`qa` click steps
|
|
181
184
|
- derive narrow prompt guards only for concrete evidence invariants: explicitly requested screenshot/recording output paths block browser close until the artifact manifest verifies those paths, while bare inbound attachment paths remain inputs. The wrapper intentionally does not infer broad business/user intent from prompt text such as order/payment/post boundaries; agents must follow those instructions themselves. The artifact guard is bounded preflight policy (`details.promptGuard`, `failureCategory: "policy-blocked"`), not a reusable browser recipe layer
|
|
182
185
|
- reject direct and effective batch `scrollintoview text=...` / `scrollinto text=...` before dispatch because current upstream can falsely report success without movement, while leaving help forms untouched; return only native recovery (`find text ... hover` or fresh snapshot/ref), leaving CSS, XPath, and current-ref behavior upstream-owned
|
|
183
186
|
- after successful `get text` on a qualifying non-ref CSS selector, optionally issue one read-only `eval --stdin` probe per selector when multiple DOM matches or a hidden first match with visible peers could misread tabbed or off-screen content; simple id selectors and sensitive-looking literals skip this probe. Merge `details.selectorTextVisibility` / `selectorTextVisibilityAll`, visible warning lines, and `inspect-visible-text-candidates*` next actions as documented in [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#details) and `RQ-0074` in [`SUPPORT_MATRIX.md`](SUPPORT_MATRIX.md)
|
|
184
|
-
- for local Unix launches, set a short private socket directory so extension-generated session names do not fail on the upstream Unix socket-path length limit; require the selected path to be absolute, owned by the current uid, mode `0700`, under
|
|
187
|
+
- for local Unix launches, set a short private socket directory so extension-generated session names do not fail on the upstream Unix socket-path length limit; require the selected path to be absolute, owned by the current uid, mode `0700`, under checked ancestry, and free of symlink, foreign-owner, or special planted entries; reject pre-existing unsafe modes instead of repairing them, then recheck before spawn. The actual filesystem root `/` is supplied by the trusted operating environment: a directory without group/other write bits is accepted regardless of its reported owner, which can be unmapped in a Linux user namespace. Existing root-owned sticky-directory acceptance is unchanged. This boundary does not protect against whoever controls the root filesystem. Every non-root ancestor still needs trusted ownership and permissions, including the destination ancestry of root-owned aliases; a matching overflow UID is not trusted. Android/Termux uses `/data/data/<package>/piab`, treats the owner-only app-data directory as the trust anchor, permits the app's matching private uid/gid ancestry, compacts generated managed identities to one 80-bit digest so ordinary namespace plus fresh-session paths remain within the limit, and places policy-lock coordination under `os.tmpdir()` because Android `/tmp` is shell-owned and inaccessible
|
|
185
188
|
- keep wrapper-spawned upstream CLI calls bounded by clamping `AGENT_BROWSER_DEFAULT_TIMEOUT` to the upstream documented 25-second default while deriving a longer subprocess watchdog for explicit long `wait <ms>` / `wait --timeout <ms>`, read, and WebMCP calls from the effective direct or raw-argument-else-stdin batch steps; dialog commands, likely dialog-trigger clicks/taps/finds, and `eval --stdin` snippets that look like alert/confirm/prompt/dialog triggers use shorter wrapper subprocess budgets so blocking JavaScript prompts surface recovery actions before the full default watchdog. Timeout recovery removes standalone snapshots when the target is unknown and emits one executable session-scoped `batch --bail` (`get url`, then `snapshot -i`); blocking-dialog status/accept/dismiss remains allowed under the same unknown-target guard
|
|
186
189
|
|
|
187
190
|
This is primarily about ownership clarity and avoiding surprise, not adding a heavy safety wrapper. If the extension invented the session, the extension should own its lifecycle without breaking reload, resume, or branch-tree semantics. If the caller explicitly chose the upstream session model, the extension should stay out of the way.
|