pi-agent-browser-native 0.3.0 → 0.5.0

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.
Files changed (73) hide show
  1. package/CHANGELOG.md +127 -0
  2. package/README.md +63 -20
  3. package/dist/extensions/agent-browser/index.js +787 -105
  4. package/dist/extensions/agent-browser/lib/argv-descriptor.js +35 -3
  5. package/dist/extensions/agent-browser/lib/argv-grammar.js +44 -2
  6. package/dist/extensions/agent-browser/lib/batch-lifecycle.js +71 -0
  7. package/dist/extensions/agent-browser/lib/command-policy.js +1 -1
  8. package/dist/extensions/agent-browser/lib/command-taxonomy.js +35 -2
  9. package/dist/extensions/agent-browser/lib/input-modes/job.js +61 -4
  10. package/dist/extensions/agent-browser/lib/input-modes/lookups.js +2 -2
  11. package/dist/extensions/agent-browser/lib/input-modes/params.js +22 -23
  12. package/dist/extensions/agent-browser/lib/input-modes/script.js +462 -0
  13. package/dist/extensions/agent-browser/lib/input-modes/semantic-action.js +51 -12
  14. package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +8 -0
  15. package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +3 -1
  16. package/dist/extensions/agent-browser/lib/managed-session-restore.js +26 -36
  17. package/dist/extensions/agent-browser/lib/managed-session-snapshots.js +2 -4
  18. package/dist/extensions/agent-browser/lib/managed-session-state-policy.js +47 -29
  19. package/dist/extensions/agent-browser/lib/managed-session-storage.js +50 -24
  20. package/dist/extensions/agent-browser/lib/orchestration/batch-stdin.js +26 -5
  21. package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.js +110 -30
  22. package/dist/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.js +2 -1
  23. package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +26 -25
  24. package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +17 -3
  25. package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +2 -1
  26. package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +6 -4
  27. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +129 -32
  28. package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +191 -55
  29. package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +57 -29
  30. package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +15 -11
  31. package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +36 -18
  32. package/dist/extensions/agent-browser/lib/orchestration/script-mode.js +299 -0
  33. package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +32 -10
  34. package/dist/extensions/agent-browser/lib/playbook.js +18 -15
  35. package/dist/extensions/agent-browser/lib/process-environment.js +14 -0
  36. package/dist/extensions/agent-browser/lib/process-identity.js +4 -4
  37. package/dist/extensions/agent-browser/lib/process.js +131 -41
  38. package/dist/extensions/agent-browser/lib/recording-reservations.js +183 -0
  39. package/dist/extensions/agent-browser/lib/results/action-recommendations.js +62 -5
  40. package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +62 -4
  41. package/dist/extensions/agent-browser/lib/results/categories.js +6 -1
  42. package/dist/extensions/agent-browser/lib/results/next-actions.js +19 -5
  43. package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +85 -38
  44. package/dist/extensions/agent-browser/lib/results/presentation/batch.js +66 -11
  45. package/dist/extensions/agent-browser/lib/results/presentation/common.js +18 -0
  46. package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +7 -1
  47. package/dist/extensions/agent-browser/lib/results/presentation/errors.js +2 -1
  48. package/dist/extensions/agent-browser/lib/results/presentation/navigation.js +26 -11
  49. package/dist/extensions/agent-browser/lib/results/presentation/registry.js +58 -13
  50. package/dist/extensions/agent-browser/lib/results/presentation/semantic-action.js +1 -10
  51. package/dist/extensions/agent-browser/lib/results/presentation.js +6 -3
  52. package/dist/extensions/agent-browser/lib/results/recovery-actions.js +2 -0
  53. package/dist/extensions/agent-browser/lib/results/selector-recovery.js +51 -8
  54. package/dist/extensions/agent-browser/lib/results/snapshot-high-value-controls.js +13 -7
  55. package/dist/extensions/agent-browser/lib/runtime.js +116 -39
  56. package/dist/extensions/agent-browser/lib/session-page-state.js +62 -10
  57. package/dist/extensions/agent-browser/lib/upstream-version.js +14 -0
  58. package/dist/extensions/agent-browser/script-worker.js +169 -0
  59. package/dist/scripts/agent-browser-target.mjs +3 -0
  60. package/docs/ARCHITECTURE.md +40 -21
  61. package/docs/COMMAND_REFERENCE.md +90 -35
  62. package/docs/RELEASE.md +3 -3
  63. package/docs/REQUIREMENTS.md +4 -2
  64. package/docs/SUPPORT_MATRIX.md +26 -19
  65. package/docs/TOOL_CONTRACT.md +93 -52
  66. package/package.json +3 -1
  67. package/platform-smoke.config.mjs +2 -2
  68. package/scripts/agent-browser-capability-baseline.mjs +24 -6
  69. package/scripts/agent-browser-target.mjs +3 -0
  70. package/scripts/build.mjs +41 -0
  71. package/scripts/doctor.mjs +7 -6
  72. package/scripts/platform-smoke/browser-dogfood-windows.ps1 +9 -3
  73. package/scripts/platform-smoke/targets.mjs +12 -6
@@ -2,26 +2,37 @@ import { existsSync, readFileSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { Text } from "@earendil-works/pi-tui";
5
+ import { batchHasSuccessfulCloseAll, getSuccessfulBatchCloseLifecycle } from "./lib/batch-lifecycle.js";
5
6
  import { PROJECT_RULE_PROMPT, buildBrowserDefaultProfileGuideline, buildBrowserExecutablePathGuideline, buildToolPromptGuidelines, } from "./lib/playbook.js";
6
7
  import { SessionPageState } from "./lib/session-page-state.js";
7
- import { canUseHeadlessCompatibilityUserAgent, createEphemeralSessionSeed, createFreshSessionName, createImplicitSessionName, extractCommandTokens, getImplicitSessionCloseTimeoutMs, getImplicitSessionIdleTimeoutMs, isRestorableManagedSessionName, restoreManagedSessionStateFromBranch, validateToolArgs, } from "./lib/runtime.js";
8
- import { extractExplicitNamespace, extractExplicitSessionName, isUpstreamEnvFlagEnabled, resolveAgentBrowserNamespace } from "./lib/argv-grammar.js";
8
+ import { canUseHeadlessCompatibilityUserAgent, createEphemeralSessionSeed, createFreshSessionName, createImplicitSessionName, extractUpstreamCommandTokens, getImplicitSessionCloseTimeoutMs, getImplicitSessionIdleTimeoutMs, isRestorableManagedSessionName, restoreManagedSessionStateFromBranch, validateToolArgs, redactSensitiveText, isPlainTextInspectionArgs, } from "./lib/runtime.js";
9
+ import { extractExplicitNamespace, extractExplicitSessionName, getAgentBrowserSessionIdentityKey, isAgentBrowserSessionIdentityKeyInNamespace, isUpstreamEnvFlagEnabled, resolveAgentBrowserNamespace } from "./lib/argv-grammar.js";
10
+ import { parseArgvDescriptor } from "./lib/argv-descriptor.js";
11
+ import { needsManagedSession } from "./lib/command-policy.js";
9
12
  import { cleanupManagedSessionRestoreConfig, ManagedSessionRestoreState } from "./lib/managed-session-restore.js";
10
13
  import { isRecord } from "./lib/parsing.js";
14
+ import { runAgentBrowserProcess } from "./lib/process.js";
15
+ import { getAgentBrowserProcessEnvironment, withIsolatedAgentBrowserEnvironment } from "./lib/process-environment.js";
16
+ import { TARGET_AGENT_BROWSER_VERSION, TARGET_AGENT_BROWSER_VERSION_LABEL, getAgentBrowserVersionValidationError, parseAgentBrowserVersionOutput, } from "./lib/upstream-version.js";
11
17
  import { buildPromptPolicy, getLatestUserPrompt, shouldAppendBrowserSystemPrompt } from "./lib/prompt-policy.js";
12
- import { isCloseCommand } from "./lib/command-taxonomy.js";
18
+ import { isCloseAllCommand, isCloseCommand } from "./lib/command-taxonomy.js";
13
19
  import { hasLaunchScopedFlagToken } from "./lib/launch-scoped-flags.js";
14
20
  import { cleanupSecureTempArtifacts } from "./lib/temp.js";
15
21
  import { AGENT_BROWSER_PARAMS } from "./lib/input-modes/params.js";
22
+ import { AGENT_BROWSER_SCRIPT_DEFAULT_TIMEOUT_MS, AGENT_BROWSER_SCRIPT_NAMESPACE, createAgentBrowserScriptSessionName, isAgentBrowserScriptSessionName, runAgentBrowserScript, } from "./lib/input-modes/script.js";
16
23
  import { parseAllowedDomainsPolicyFromArgs } from "./lib/navigation-policy.js";
17
24
  import { closeManagedSession, getSessionContextKey, runAgentBrowserTool } from "./lib/orchestration/browser-run/index.js";
25
+ import { canonicalizeExplicitArtifactDestination, getExplicitArtifactDestination } from "./lib/orchestration/browser-run/artifact-paths.js";
18
26
  import { findElectronLaunchRecordForSession, getActiveElectronRecords } from "./lib/orchestration/browser-run/session-state.js";
19
- import { parseBatchStdinJsonArray } from "./lib/orchestration/batch-stdin.js";
27
+ import { parseBatchCommandArgument, parseUserBatchStdin } from "./lib/orchestration/batch-stdin.js";
20
28
  import { ELECTRON_POST_COMMAND_STATUS_SETTLE_MS, ELECTRON_PROFILE_ISOLATION_DETAILS, cleanupActiveElectronHostLaunches, handleElectronHostInput, restoreElectronLaunchRecordsFromBranch, } from "./lib/orchestration/electron-host/index.js";
21
29
  import { buildValidationFailureResult, resolveAgentBrowserInput } from "./lib/orchestration/input-plan.js";
22
- import { applyAgentBrowserOutputPath, getAgentBrowserOutputPathValidationError } from "./lib/orchestration/output-file.js";
23
- import { formatSessionArtifactRetentionSummary, getSessionArtifactManifestEntryKey, isSessionArtifactManifest, mergeSessionArtifactManifest } from "./lib/results/artifact-manifest.js";
30
+ import { applyAgentBrowserOutputPath, getAgentBrowserOutputPathValidationError, normalizeRequestedOutputPath } from "./lib/orchestration/output-file.js";
31
+ import { appendScriptSessionLease, buildScriptBrowserEnvelope, buildScriptToolResult, getScriptSessionLeasesFromBranch } from "./lib/orchestration/script-mode.js";
32
+ import { formatSessionArtifactRetentionSummary, getSessionArtifactManifestEntryKey, isPendingRecordingCommand, isSessionArtifactManifest, mergeSessionArtifactManifest, retirePendingRecordingManifestEntries } from "./lib/results/artifact-manifest.js";
33
+ import { appendUniqueAgentBrowserNextActions, applyNamespaceToNextActions, applySessionToNextActions, buildNextToolAction } from "./lib/results/next-actions.js";
24
34
  import { canRegisterWebSearchTool, loadAgentBrowserConfigSync } from "./lib/config.js";
35
+ import { appendRecordingReservationTransition, applyRecordingArtifactsToReservations, restoreRecordingReservationStateFromBranch, retireRecordingReservation, } from "./lib/recording-reservations.js";
25
36
  import { createAgentBrowserWebSearchTool } from "./lib/web-search.js";
26
37
  import { isDirectAgentBrowserBashAllowed, isHarmlessAgentBrowserInspectionCommand, looksLikeDirectAgentBrowserBash, } from "./lib/bash-guard.js";
27
38
  import { AgentBrowserResultComponent, buildAgentBrowserToolResultPatch, formatAgentBrowserRenderCall, formatAgentBrowserRenderResult, } from "./lib/pi-tool-rendering.js";
@@ -30,22 +41,94 @@ function isBashToolCallEvent(event) {
30
41
  return false;
31
42
  return typeof event.input.command === "string";
32
43
  }
33
- function getBatchPreflightValidationError(args, stdin) {
34
- const commandTokens = extractCommandTokens(args);
35
- if (commandTokens[0] !== "batch" || stdin === undefined) {
36
- return undefined;
44
+ function getArtifactCommandSteps(args, stdin) {
45
+ const commandTokens = extractUpstreamCommandTokens(args);
46
+ const batch = commandTokens[0] === "batch";
47
+ if (!batch)
48
+ return { batch, steps: commandTokens.length > 0 ? [commandTokens] : [] };
49
+ const steps = [];
50
+ for (const command of commandTokens.slice(1)) {
51
+ if (command === "--bail")
52
+ continue;
53
+ const parsed = parseBatchCommandArgument(command);
54
+ if (parsed.error || !parsed.step)
55
+ return { batch, error: `Unsupported batch step ${steps.length + 1}: ${parsed.error ?? "command could not be parsed safely"}`, steps };
56
+ steps.push(parsed.step);
37
57
  }
38
- const parsed = parseBatchStdinJsonArray(stdin);
39
- if (parsed.error || parsed.steps === undefined) {
40
- return undefined;
58
+ // Upstream executes raw argument steps exclusively when any exist, so ignored
59
+ // stdin must not add artifact/lifecycle steps or fail this preflight.
60
+ if (steps.length > 0)
61
+ return { batch, steps };
62
+ const parsed = parseUserBatchStdin(stdin);
63
+ return parsed.error ? { batch, error: parsed.error, steps } : { batch, steps: parsed.steps ?? [] };
64
+ }
65
+ function getArtifactPreflightValidationError(options) {
66
+ const { batch, error, steps } = getArtifactCommandSteps(options.args, options.stdin);
67
+ if (error)
68
+ return error;
69
+ const activeRecordingDestinations = new Set();
70
+ const cleanupOnly = steps.length > 0 && steps.every((step) => {
71
+ const [command, subcommand] = extractUpstreamCommandTokens(step);
72
+ return isCloseCommand(command) || (command === "record" && subcommand === "stop");
73
+ });
74
+ for (const reservation of options.activeRecordingReservations ?? []) {
75
+ try {
76
+ activeRecordingDestinations.add(canonicalizeExplicitArtifactDestination(reservation.cwd, reservation.absolutePath));
77
+ }
78
+ catch (canonicalizationError) {
79
+ if (!cleanupOnly)
80
+ return canonicalizationError instanceof Error ? canonicalizationError.message : "An active recording destination could not be resolved safely.";
81
+ }
41
82
  }
42
- for (const [index, step] of parsed.steps.entries()) {
43
- if (!Array.isArray(step) || !step.every((token) => typeof token === "string") || step.length === 0)
44
- continue;
45
- const stepValidationError = validateToolArgs(step);
46
- if (stepValidationError)
47
- return `Unsupported batch step ${index + 1}: ${stepValidationError}`;
48
- if (step[0] === "screenshot" && step.includes("--annotate")) {
83
+ let canonicalOutputPath;
84
+ if (options.outputPath) {
85
+ try {
86
+ canonicalOutputPath = canonicalizeExplicitArtifactDestination(options.cwd, normalizeRequestedOutputPath(options.outputPath));
87
+ if (activeRecordingDestinations.has(canonicalOutputPath)) {
88
+ return `Unsupported outputPath: ${options.outputPath} is reserved by an active recording. Stop that recording first or use a distinct path.`;
89
+ }
90
+ }
91
+ catch (canonicalizationError) {
92
+ return canonicalizationError instanceof Error ? canonicalizationError.message : `outputPath ${options.outputPath} could not be resolved safely.`;
93
+ }
94
+ }
95
+ const artifactDestinations = new Map();
96
+ let sawBatchClose = false;
97
+ for (const [index, step] of steps.entries()) {
98
+ const commandStep = extractUpstreamCommandTokens(step);
99
+ if (batch) {
100
+ const stepValidationError = validateToolArgs(step);
101
+ if (stepValidationError)
102
+ return `Unsupported batch step ${index + 1}: ${stepValidationError}`;
103
+ if (sawBatchClose && commandStep[0] === "record" && (commandStep[1] === "start" || commandStep[1] === "restart")) {
104
+ return `Unsupported batch step ${index + 1}: record ${commandStep[1]} cannot follow close, quit, or exit in one upstream batch because upstream can report success without starting a recording. Split the close and recording into separate agent_browser calls.`;
105
+ }
106
+ if (isCloseCommand(commandStep[0]))
107
+ sawBatchClose = true;
108
+ }
109
+ const artifactDestination = getExplicitArtifactDestination(commandStep);
110
+ if (artifactDestination) {
111
+ let canonicalDestination;
112
+ try {
113
+ canonicalDestination = canonicalizeExplicitArtifactDestination(options.cwd, artifactDestination);
114
+ }
115
+ catch (canonicalizationError) {
116
+ return canonicalizationError instanceof Error ? canonicalizationError.message : `Artifact destination ${artifactDestination} could not be resolved safely.`;
117
+ }
118
+ if (canonicalOutputPath === canonicalDestination) {
119
+ return `Unsupported outputPath: ${options.outputPath} resolves to the same destination as artifact path ${artifactDestination}. Use distinct paths so the tool-result JSON cannot overwrite the browser artifact.`;
120
+ }
121
+ if (activeRecordingDestinations.has(canonicalDestination)) {
122
+ const prefix = batch ? `Unsupported batch artifact destination in step ${index + 1}` : "Unsupported artifact destination";
123
+ return `${prefix}: ${artifactDestination} is reserved by an active recording. Stop that recording first or use a distinct path.`;
124
+ }
125
+ const priorStep = artifactDestinations.get(canonicalDestination);
126
+ if (priorStep !== undefined) {
127
+ return `Unsupported batch artifact destination in step ${index + 1}: ${artifactDestination} is already written by step ${priorStep + 1}. Use distinct paths or split the batch so each artifact can be verified independently.`;
128
+ }
129
+ artifactDestinations.set(canonicalDestination, index);
130
+ }
131
+ if (batch && commandStep[0] === "screenshot" && step.includes("--annotate")) {
49
132
  return [
50
133
  `Unsupported batch screenshot annotation in step ${index + 1}: put --annotate in top-level args, not inside the batch step.`,
51
134
  `Use: { "args": ["--annotate", "batch"], "stdin": "[[\\"screenshot\\",\\"/path/to/image.png\\"]]" }`,
@@ -54,6 +137,51 @@ function getBatchPreflightValidationError(args, stdin) {
54
137
  }
55
138
  return undefined;
56
139
  }
140
+ function commandClosesAllSessions(args, stdin) {
141
+ const parsed = getArtifactCommandSteps(args, stdin);
142
+ return !parsed.error && parsed.steps.some((step) => isCloseAllCommand(extractUpstreamCommandTokens(step)));
143
+ }
144
+ function commandTouchesArtifactLifecycle(args, stdin, outputPath) {
145
+ if (outputPath)
146
+ return true;
147
+ const parsed = getArtifactCommandSteps(args, stdin);
148
+ if (parsed.error)
149
+ return true;
150
+ return parsed.steps.some((step) => {
151
+ const commandStep = extractUpstreamCommandTokens(step);
152
+ return getExplicitArtifactDestination(commandStep) !== undefined || commandStep[0] === "record" || commandStep[0] === "screenshot" || isCloseCommand(commandStep[0]);
153
+ });
154
+ }
155
+ function isResultFileArtifact(artifact) {
156
+ return isRecord(artifact)
157
+ && typeof artifact.absolutePath === "string"
158
+ && typeof artifact.kind === "string"
159
+ && typeof artifact.path === "string";
160
+ }
161
+ function getResultFileArtifacts(result) {
162
+ const details = isRecord(result.details) ? result.details : undefined;
163
+ return Array.isArray(details?.artifacts) ? details.artifacts.filter(isResultFileArtifact) : [];
164
+ }
165
+ function reportsNoRecordingInProgress(value) {
166
+ try {
167
+ return /no recording in progress/i.test(JSON.stringify(value));
168
+ }
169
+ catch {
170
+ return false;
171
+ }
172
+ }
173
+ function batchStepReportsNoRecordingInProgress(step) {
174
+ if (!isRecord(step) || step.success !== false)
175
+ return false;
176
+ const command = Array.isArray(step.command) && step.command.every((token) => typeof token === "string") ? extractUpstreamCommandTokens(step.command) : [];
177
+ return command[0] === "record" && command[1] === "stop" && reportsNoRecordingInProgress(step);
178
+ }
179
+ function resultReportsNoRecordingInProgress(result) {
180
+ if (result.isError !== true)
181
+ return false;
182
+ const details = isRecord(result.details) ? result.details : undefined;
183
+ return details?.command === "record" && details.subcommand === "stop" && reportsNoRecordingInProgress(result.content);
184
+ }
57
185
  function restoreArtifactManifestFromBranch(branch) {
58
186
  let restoredManifest;
59
187
  for (const entry of branch) {
@@ -69,6 +197,12 @@ function restoreArtifactManifestFromBranch(branch) {
69
197
  }
70
198
  return restoredManifest;
71
199
  }
200
+ function getRecognizedCompatibilityWorkaround(value) {
201
+ const workaround = isRecord(value) ? value : undefined;
202
+ return (workaround?.id === "chatgpt-headless-user-agent" || workaround?.id === "cloudflare-headless-user-agent") && typeof workaround.reason === "string"
203
+ ? { id: workaround.id, reason: workaround.reason }
204
+ : undefined;
205
+ }
72
206
  function restoreManagedSessionCompatibilityWorkaroundFromBranch(branch, sessionName, namespace) {
73
207
  let restored;
74
208
  const targetKey = getSessionContextKey(sessionName, namespace);
@@ -81,12 +215,9 @@ function restoreManagedSessionCompatibilityWorkaroundFromBranch(branch, sessionN
81
215
  const details = isRecord(message.details) ? message.details : undefined;
82
216
  if (!details)
83
217
  continue;
84
- const workaround = isRecord(details.compatibilityWorkaround) ? details.compatibilityWorkaround : undefined;
85
218
  if (getSessionContextKey(typeof details.sessionName === "string" ? details.sessionName : undefined, typeof details.namespace === "string" ? details.namespace : undefined) !== targetKey)
86
219
  continue;
87
- const recognizedWorkaround = (workaround?.id === "chatgpt-headless-user-agent" || workaround?.id === "cloudflare-headless-user-agent") && typeof workaround.reason === "string"
88
- ? { id: workaround.id, reason: workaround.reason }
89
- : undefined;
220
+ const recognizedWorkaround = getRecognizedCompatibilityWorkaround(details.compatibilityWorkaround);
90
221
  const succeeded = getSuccessfulToolResult(details, message);
91
222
  const outcome = getManagedSessionOutcome(details);
92
223
  const activeAfterFailure = recognizedWorkaround
@@ -163,9 +294,21 @@ function getToolResultArgs(details) {
163
294
  return details.effectiveArgs;
164
295
  return [];
165
296
  }
166
- function isAttachedBrowserInvocation(args, env = process.env) {
297
+ function detailsReportCloseAllApplied(details, succeeded) {
298
+ const args = getToolResultArgs(details);
299
+ return details.closeAllApplied === true
300
+ || (succeeded && isCloseAllCommand(extractUpstreamCommandTokens(args)))
301
+ || batchHasSuccessfulCloseAll(details.batchSteps);
302
+ }
303
+ function deleteIdentityKeysInNamespace(entries, namespace) {
304
+ for (const key of entries.keys()) {
305
+ if (isAgentBrowserSessionIdentityKeyInNamespace(key, namespace))
306
+ entries.delete(key);
307
+ }
308
+ }
309
+ function isAttachedBrowserInvocation(args, env = getAgentBrowserProcessEnvironment()) {
167
310
  const autoConnectEnv = env.AGENT_BROWSER_AUTO_CONNECT;
168
- return extractCommandTokens(args)[0] === "connect"
311
+ return extractUpstreamCommandTokens(args)[0] === "connect"
169
312
  || hasLaunchScopedFlagToken(args, "--cdp")
170
313
  || hasLaunchScopedFlagToken(args, "--auto-connect")
171
314
  || env.AGENT_BROWSER_CDP !== undefined
@@ -184,15 +327,32 @@ function restoreAttachedSessionKeysFromBranch(branch) {
184
327
  continue;
185
328
  const managedSessionOutcome = isRecord(details.managedSessionOutcome) ? details.managedSessionOutcome : undefined;
186
329
  const retainedFailedAttachment = details.attachedBrowserSession === true && managedSessionOutcome?.activeAfter === true;
187
- if (!getSuccessfulToolResult(details, message) && !retainedFailedAttachment)
188
- continue;
330
+ const succeeded = getSuccessfulToolResult(details, message);
331
+ const batchCloseLifecycle = getSuccessfulBatchCloseLifecycle(details.batchSteps);
332
+ const terminalBatchClose = batchCloseLifecycle?.endsClosed === true;
189
333
  const args = getToolResultArgs(details);
334
+ const namespace = typeof details.namespace === "string" ? details.namespace : extractExplicitNamespace(args);
190
335
  const sessionName = typeof details.sessionName === "string" ? details.sessionName : extractExplicitSessionName(args);
336
+ const electron = isRecord(details.electron) ? details.electron : undefined;
337
+ const cleanup = isRecord(electron?.cleanup) ? electron.cleanup : undefined;
338
+ for (const cleanupResult of Array.isArray(cleanup?.results) ? cleanup.results : []) {
339
+ for (const identity of getCleanupResultClosedManagedSessionIdentities(cleanupResult, namespace)) {
340
+ attachedSessionKeys.delete(getSessionContextKey(identity.sessionName, identity.namespace) ?? identity.sessionName);
341
+ }
342
+ }
343
+ if (detailsReportCloseAllApplied(details, succeeded)) {
344
+ deleteIdentityKeysInNamespace(attachedSessionKeys, namespace);
345
+ if (sessionName && details.attachedBrowserSession === true && batchCloseLifecycle?.endsClosed === false) {
346
+ attachedSessionKeys.add(getSessionContextKey(sessionName, namespace) ?? sessionName);
347
+ }
348
+ continue;
349
+ }
350
+ if (!succeeded && !retainedFailedAttachment && !terminalBatchClose)
351
+ continue;
191
352
  if (!sessionName)
192
353
  continue;
193
- const namespace = typeof details.namespace === "string" ? details.namespace : extractExplicitNamespace(args);
194
354
  const sessionKey = getSessionContextKey(sessionName, namespace) ?? sessionName;
195
- if (isCloseCommand(extractCommandTokens(args)[0]))
355
+ if ((succeeded && isCloseCommand(extractUpstreamCommandTokens(args)[0])) || terminalBatchClose)
196
356
  attachedSessionKeys.delete(sessionKey);
197
357
  else if (details.attachedBrowserSession === true || isAttachedBrowserInvocation(args, {}))
198
358
  attachedSessionKeys.add(sessionKey);
@@ -211,12 +371,15 @@ function restoreAllowedDomainsBySessionFromBranch(branch) {
211
371
  if (!details)
212
372
  continue;
213
373
  const succeeded = getSuccessfulToolResult(details, message);
374
+ const batchCloseLifecycle = getSuccessfulBatchCloseLifecycle(details.batchSteps);
214
375
  const args = getToolResultArgs(details);
215
- const command = typeof details.command === "string" ? details.command : extractCommandTokens(args)[0];
376
+ const command = typeof details.command === "string" ? details.command : extractUpstreamCommandTokens(args)[0];
216
377
  const sessionName = typeof details.sessionName === "string" ? details.sessionName : undefined;
217
378
  const namespace = typeof details.namespace === "string" ? details.namespace : undefined;
218
379
  const sessionKey = getSessionContextKey(sessionName, namespace);
219
380
  const explicitSessionName = extractExplicitSessionName(args);
381
+ if (detailsReportCloseAllApplied(details, succeeded))
382
+ deleteIdentityKeysInNamespace(restoredPolicies, namespace);
220
383
  const outcome = getManagedSessionOutcome(details);
221
384
  const outcomeSucceeded = outcome?.succeeded === true;
222
385
  const outcomeStatus = typeof outcome?.status === "string" ? outcome.status : undefined;
@@ -233,7 +396,7 @@ function restoreAllowedDomainsBySessionFromBranch(branch) {
233
396
  if (replacedSessionName)
234
397
  restoredPolicies.delete(getSessionContextKey(replacedSessionName, replacedSessionNamespace) ?? replacedSessionName);
235
398
  }
236
- if (succeeded && isCloseCommand(command)) {
399
+ if ((succeeded && isCloseCommand(command)) || batchCloseLifecycle) {
237
400
  const closedSessionName = explicitSessionName ?? sessionName ?? outcomeAttemptedSessionName ?? outcomeCurrentSessionName;
238
401
  if (closedSessionName)
239
402
  restoredPolicies.delete(getSessionContextKey(closedSessionName, namespace) ?? closedSessionName);
@@ -242,13 +405,14 @@ function restoreAllowedDomainsBySessionFromBranch(branch) {
242
405
  const cleanup = isRecord(electron?.cleanup) ? electron.cleanup : undefined;
243
406
  const cleanupResults = Array.isArray(cleanup?.results) ? cleanup.results : [];
244
407
  for (const cleanupResult of cleanupResults) {
245
- for (const closedSessionName of getCleanupResultClosedManagedSessionNames(cleanupResult))
246
- restoredPolicies.delete(closedSessionName);
408
+ for (const identity of getCleanupResultClosedManagedSessionIdentities(cleanupResult, namespace)) {
409
+ restoredPolicies.delete(getSessionContextKey(identity.sessionName, identity.namespace) ?? identity.sessionName);
410
+ }
247
411
  }
248
412
  const outcomeKeepsSessionCurrent = outcome?.activeAfter === true
249
413
  && (outcomeStatus === "created" || outcomeStatus === "replaced" || outcomeStatus === "unchanged")
250
414
  && outcomeCurrentSessionName === sessionName;
251
- const policy = (succeeded || outcomeKeepsSessionCurrent) && sessionKey && !isCloseCommand(command) ? parseAllowedDomainsPolicyFromArgs(args) : undefined;
415
+ const policy = (succeeded || outcomeKeepsSessionCurrent) && sessionKey && !isCloseCommand(command) && batchCloseLifecycle?.endsClosed !== true ? parseAllowedDomainsPolicyFromArgs(args) : undefined;
252
416
  if (policy && sessionKey)
253
417
  restoredPolicies.set(sessionKey, policy);
254
418
  }
@@ -260,9 +424,12 @@ function trackOwnedManagedSession(sessions, sessionName, cwd, options = {}) {
260
424
  const key = getSessionContextKey(sessionName, options.namespace) ?? sessionName;
261
425
  const existing = sessions.get(key);
262
426
  const branchOwned = existing && !existing.branchOwned ? false : options.branchOwned === true;
427
+ const compatibilityWorkaround = Object.hasOwn(options, "compatibilityWorkaround")
428
+ ? options.compatibilityWorkaround
429
+ : existing?.compatibilityWorkaround;
263
430
  const headedManagedAutosaveDisabled = options.headedManagedAutosaveDisabled ?? existing?.headedManagedAutosaveDisabled;
264
431
  const headedManagedAutosaveInterval = options.headedManagedAutosaveInterval ?? existing?.headedManagedAutosaveInterval;
265
- sessions.set(key, { branchOwned, cwd, headedManagedAutosaveDisabled, headedManagedAutosaveInterval, namespace: options.namespace, sessionName });
432
+ sessions.set(key, { branchOwned, compatibilityWorkaround, cwd, headedManagedAutosaveDisabled, headedManagedAutosaveInterval, namespace: options.namespace, sessionName });
266
433
  }
267
434
  function untrackOwnedManagedSession(sessions, sessionName, namespace) {
268
435
  if (!sessionName)
@@ -291,20 +458,21 @@ function syncOwnedManagedSessionsFromResult(sessions, result, cwd) {
291
458
  const status = typeof outcome.status === "string" ? outcome.status : undefined;
292
459
  const currentSessionName = typeof outcome.currentSessionName === "string" ? outcome.currentSessionName : undefined;
293
460
  const attemptedSessionName = typeof outcome.attemptedSessionName === "string" ? outcome.attemptedSessionName : undefined;
461
+ const namespace = isRecord(details) && typeof details.namespace === "string" ? details.namespace : undefined;
294
462
  if (outcome.activeAfter === true && (status === "created" || status === "replaced" || status === "unchanged")) {
295
- const namespace = isRecord(details) && typeof details.namespace === "string" ? details.namespace : undefined;
296
463
  trackOwnedManagedSession(sessions, currentSessionName, cwd, {
464
+ compatibilityWorkaround: getRecognizedCompatibilityWorkaround(details?.compatibilityWorkaround),
297
465
  headedManagedAutosaveDisabled: details?.managedSessionHeadedAutosaveDisabled === true,
298
466
  headedManagedAutosaveInterval: typeof details?.managedSessionHeadedAutosaveInterval === "string" ? details.managedSessionHeadedAutosaveInterval : undefined,
299
467
  namespace,
300
468
  });
301
469
  }
302
470
  if (succeeded && status === "closed") {
303
- untrackOwnedManagedSession(sessions, attemptedSessionName ?? currentSessionName);
471
+ untrackOwnedManagedSession(sessions, attemptedSessionName ?? currentSessionName, namespace);
304
472
  }
305
473
  }
306
- function getTouchedElectronLaunchIds(sessionName, records) {
307
- const record = findElectronLaunchRecordForSession(sessionName, records);
474
+ function getTouchedElectronLaunchIds(sessionName, records, namespace) {
475
+ const record = findElectronLaunchRecordForSession(sessionName, records, namespace);
308
476
  return record ? new Set([record.launchId]) : undefined;
309
477
  }
310
478
  function mergeActiveElectronLaunchRecords(target, source, options = {}) {
@@ -372,10 +540,10 @@ function getElectronHostLaunchRecordsForInput(options) {
372
540
  }
373
541
  return options.branchRecords;
374
542
  }
375
- function getCleanupResultClosedManagedSessionNames(result) {
543
+ function getCleanupResultClosedManagedSessionIdentities(result, fallbackNamespace) {
376
544
  if (!isRecord(result) || !Array.isArray(result.steps))
377
545
  return [];
378
- const closedSessionNames = new Set();
546
+ const identities = new Map();
379
547
  const record = isRecord(result.record) ? result.record : undefined;
380
548
  for (const step of result.steps) {
381
549
  if (!isRecord(step) || step.resource !== "managed-session")
@@ -385,24 +553,29 @@ function getCleanupResultClosedManagedSessionNames(result) {
385
553
  const sessionName = typeof step.sessionName === "string"
386
554
  ? step.sessionName
387
555
  : typeof record?.sessionName === "string" ? record.sessionName : undefined;
556
+ const namespace = typeof step.namespace === "string"
557
+ ? step.namespace
558
+ : typeof record?.namespace === "string" ? record.namespace : fallbackNamespace;
388
559
  if (sessionName)
389
- closedSessionNames.add(sessionName);
560
+ identities.set(getSessionContextKey(sessionName, namespace) ?? sessionName, { namespace, sessionName });
390
561
  }
391
- return [...closedSessionNames];
562
+ return [...identities.values()];
392
563
  }
393
- function getCleanupResultsClosedManagedSessionNames(cleanupResults) {
394
- const closedSessionNames = new Set();
564
+ function getCleanupResultsClosedManagedSessionIdentities(cleanupResults, fallbackNamespace) {
565
+ const identities = new Map();
395
566
  for (const result of cleanupResults) {
396
- for (const sessionName of getCleanupResultClosedManagedSessionNames(result))
397
- closedSessionNames.add(sessionName);
567
+ for (const identity of getCleanupResultClosedManagedSessionIdentities(result, fallbackNamespace)) {
568
+ identities.set(getSessionContextKey(identity.sessionName, identity.namespace) ?? identity.sessionName, identity);
569
+ }
398
570
  }
399
- return [...closedSessionNames];
571
+ return [...identities.values()];
400
572
  }
401
573
  function isElectronLaunchRecord(value) {
402
574
  if (!isRecord(value))
403
575
  return false;
404
576
  return value.version === 1
405
577
  && value.launchedByWrapper === true
578
+ && (value.namespace === undefined || typeof value.namespace === "string")
406
579
  && typeof value.launchId === "string"
407
580
  && typeof value.appName === "string"
408
581
  && typeof value.executablePath === "string"
@@ -460,18 +633,20 @@ function collectBranchManagedResourceEvents(branch) {
460
633
  eventRank += 1;
461
634
  const succeeded = getSuccessfulToolResult(details, message);
462
635
  const args = Array.isArray(details.args) && details.args.every((arg) => typeof arg === "string") ? details.args : [];
463
- const command = typeof details.command === "string" ? details.command : extractCommandTokens(args)[0];
636
+ const command = typeof details.command === "string" ? details.command : extractUpstreamCommandTokens(args)[0];
464
637
  const sessionName = typeof details.sessionName === "string" ? details.sessionName : undefined;
465
638
  const namespace = typeof details.namespace === "string" ? details.namespace : undefined;
466
639
  const sessionMode = details.sessionMode === "fresh" || details.sessionMode === "auto" ? details.sessionMode : undefined;
467
640
  const usedImplicitSession = details.usedImplicitSession === true;
468
641
  const explicitSessionName = extractExplicitSessionName(args);
642
+ const batchCloseLifecycle = getSuccessfulBatchCloseLifecycle(details.batchSteps);
643
+ const closeAllApplied = detailsReportCloseAllApplied(details, succeeded);
469
644
  const outcome = getManagedSessionOutcome(details);
470
645
  const outcomeSucceeded = outcome?.succeeded === true;
471
646
  const outcomeStatus = typeof outcome?.status === "string" ? outcome.status : undefined;
472
647
  const outcomeCurrentSessionName = typeof outcome?.currentSessionName === "string" ? outcome.currentSessionName : undefined;
473
648
  const outcomeAttemptedSessionName = typeof outcome?.attemptedSessionName === "string" ? outcome.attemptedSessionName : undefined;
474
- if (outcomeSucceeded && outcome.activeAfter === true && (outcomeStatus === "created" || outcomeStatus === "replaced" || outcomeStatus === "unchanged")) {
649
+ if (outcome?.activeAfter === true && (outcomeStatus === "created" || outcomeStatus === "replaced" || outcomeStatus === "unchanged")) {
475
650
  setBranchManagedSessionActive(events, outcomeCurrentSessionName, namespace, eventRank);
476
651
  }
477
652
  if (outcomeSucceeded && outcomeStatus === "closed") {
@@ -487,6 +662,14 @@ function collectBranchManagedResourceEvents(branch) {
487
662
  if (succeeded && isCloseCommand(command)) {
488
663
  setBranchRankForString(events.managedSessionCloseRanks, getSessionContextKey(explicitSessionName ?? sessionName ?? outcomeAttemptedSessionName ?? outcomeCurrentSessionName, namespace), eventRank);
489
664
  }
665
+ if (closeAllApplied) {
666
+ const retainedSessionKey = batchCloseLifecycle?.endsClosed === false ? getSessionContextKey(sessionName, namespace) : undefined;
667
+ for (const sessionKey of events.managedSessionActiveIdentities.keys()) {
668
+ if (sessionKey !== retainedSessionKey && isAgentBrowserSessionIdentityKeyInNamespace(sessionKey, namespace)) {
669
+ events.managedSessionCloseRanks.set(sessionKey, eventRank);
670
+ }
671
+ }
672
+ }
490
673
  const electron = isRecord(details.electron) ? details.electron : undefined;
491
674
  const launch = electron && isElectronLaunchRecord(electron.launch) ? electron.launch : undefined;
492
675
  if (launch && getActiveElectronRecords(new Map([[launch.launchId, launch]])).length > 0) {
@@ -503,8 +686,8 @@ function collectBranchManagedResourceEvents(branch) {
503
686
  if (isRecord(cleanupResult) && isElectronLaunchRecord(cleanupResult.record)) {
504
687
  events.electronLaunchCleanupRanks.set(cleanupResult.record.launchId, eventRank);
505
688
  }
506
- for (const closedSessionName of getCleanupResultClosedManagedSessionNames(cleanupResult)) {
507
- events.managedSessionCloseRanks.set(closedSessionName, eventRank);
689
+ for (const identity of getCleanupResultClosedManagedSessionIdentities(cleanupResult, namespace)) {
690
+ events.managedSessionCloseRanks.set(getSessionContextKey(identity.sessionName, identity.namespace) ?? identity.sessionName, eventRank);
508
691
  }
509
692
  }
510
693
  }
@@ -523,23 +706,25 @@ function getCleanupResultsPreservedUserDataDirs(cleanupResults) {
523
706
  }
524
707
  return [...userDataDirs];
525
708
  }
526
- function syncElectronCleanupManagedSessions(sessions, cleanupResults) {
527
- for (const sessionName of getCleanupResultsClosedManagedSessionNames(cleanupResults)) {
528
- untrackOwnedManagedSession(sessions, sessionName);
709
+ function syncElectronCleanupManagedSessions(sessions, cleanupResults, fallbackNamespace) {
710
+ for (const identity of getCleanupResultsClosedManagedSessionIdentities(cleanupResults, fallbackNamespace)) {
711
+ untrackOwnedManagedSession(sessions, identity.sessionName, identity.namespace);
529
712
  }
530
713
  }
531
- async function closeOwnedManagedSessionsExcept(sessions, restoreState, keepSessionName, timeoutMs, attachedSessionKeys, keepNamespace) {
714
+ async function closeOwnedManagedSessionsExcept(sessions, restoreState, keepSessionName, timeoutMs, attachedSessionKeys, keepNamespace, onClosed) {
532
715
  const keepKey = getSessionContextKey(keepSessionName, keepNamespace);
533
716
  for (const [key, owner] of [...sessions]) {
534
717
  if (key === keepKey)
535
718
  continue;
536
719
  const error = await closeManagedSession({ cwd: owner.cwd, headedManagedAutosaveInterval: owner.headedManagedAutosaveInterval, namespace: owner.namespace, preserveAttachedBrowserSession: attachedSessionKeys.has(key), restoreState, sessionName: owner.sessionName, timeoutMs });
537
- if (!error)
720
+ if (!error) {
538
721
  sessions.delete(key);
722
+ onClosed?.(owner);
723
+ }
539
724
  }
540
725
  }
541
- async function closeOwnedManagedSessions(sessions, restoreState, timeoutMs, attachedSessionKeys) {
542
- await closeOwnedManagedSessionsExcept(sessions, restoreState, undefined, timeoutMs, attachedSessionKeys);
726
+ async function closeOwnedManagedSessions(sessions, restoreState, timeoutMs, attachedSessionKeys, onClosed) {
727
+ await closeOwnedManagedSessionsExcept(sessions, restoreState, undefined, timeoutMs, attachedSessionKeys, undefined, onClosed);
543
728
  }
544
729
  function getOffBranchOwnedElectronLaunchRecords(ownedRecords, branchRecords) {
545
730
  const activeBranchLaunchIds = new Set(getActiveElectronRecords(branchRecords).map((record) => record.launchId));
@@ -579,14 +764,19 @@ class AsyncExecutionQueue {
579
764
  })();
580
765
  }
581
766
  }
582
- class KeyedAsyncExecutionQueue {
767
+ export class KeyedAsyncExecutionQueue {
768
+ barriers = new Map();
583
769
  entries = new Map();
584
- async run(key, work) {
770
+ async run(key, namespace, work) {
585
771
  const entry = this.entries.get(key) ?? { queue: new AsyncExecutionQueue(), users: 0 };
772
+ const barrier = this.barriers.get(getAgentBrowserSessionIdentityKey("", namespace)) ?? Promise.resolve();
586
773
  entry.users += 1;
587
774
  this.entries.set(key, entry);
588
775
  try {
589
- return await entry.queue.run(work);
776
+ return await entry.queue.run(async () => {
777
+ await barrier;
778
+ return await work();
779
+ });
590
780
  }
591
781
  finally {
592
782
  entry.users -= 1;
@@ -594,6 +784,29 @@ class KeyedAsyncExecutionQueue {
594
784
  this.entries.delete(key);
595
785
  }
596
786
  }
787
+ async runExclusive(namespace, work) {
788
+ const namespaceKey = getAgentBrowserSessionIdentityKey("", namespace);
789
+ const previous = this.barriers.get(namespaceKey) ?? Promise.resolve();
790
+ let release;
791
+ const blocked = new Promise((resolve) => {
792
+ release = resolve;
793
+ });
794
+ const barrier = previous.then(() => blocked);
795
+ this.barriers.set(namespaceKey, barrier);
796
+ const drains = [...this.entries]
797
+ .filter(([key]) => isAgentBrowserSessionIdentityKeyInNamespace(key, namespace))
798
+ .map(([, { queue }]) => queue.run(async () => undefined));
799
+ await previous;
800
+ await Promise.all(drains);
801
+ try {
802
+ return await work();
803
+ }
804
+ finally {
805
+ release();
806
+ if (this.barriers.get(namespaceKey) === barrier)
807
+ this.barriers.delete(namespaceKey);
808
+ }
809
+ }
597
810
  }
598
811
  function mergeBrowserRunMap(current, initial, updated) {
599
812
  if (updated === initial)
@@ -609,11 +822,19 @@ function mergeBrowserRunMap(current, initial, updated) {
609
822
  }
610
823
  return merged;
611
824
  }
612
- function mergeBrowserRunArtifactManifest(current, initial, updated) {
825
+ export function mergeBrowserRunArtifactManifest(current, initial, updated) {
613
826
  if (!updated || updated === initial)
614
827
  return current;
828
+ if (current === initial)
829
+ return updated;
615
830
  const initialEntries = new Map((initial?.entries ?? []).map((entry) => [getSessionArtifactManifestEntryKey(entry), entry]));
616
- const changedEntries = updated.entries.filter((entry) => initialEntries.get(getSessionArtifactManifestEntryKey(entry)) !== entry);
831
+ const changedEntries = updated.entries
832
+ .map((entry, index) => ({ entry, index }))
833
+ .filter(({ entry }) => initialEntries.get(getSessionArtifactManifestEntryKey(entry)) !== entry)
834
+ .sort((left, right) => left.entry.createdAtMs - right.entry.createdAtMs
835
+ || Number(isPendingRecordingCommand(left.entry.command, left.entry.subcommand, left.entry.kind)) - Number(isPendingRecordingCommand(right.entry.command, right.entry.subcommand, right.entry.kind))
836
+ || left.index - right.index)
837
+ .map(({ entry }) => entry);
617
838
  return changedEntries.length === 0
618
839
  ? current
619
840
  : mergeSessionArtifactManifest({
@@ -681,6 +902,9 @@ export default function agentBrowserExtension(pi) {
681
902
  let sessionPageState = new SessionPageState();
682
903
  let traceOwners = new Map();
683
904
  let artifactManifest;
905
+ let activeRecordingReservations = new Map();
906
+ let recordingSessionTombstones = new Map();
907
+ let recordingSessionTombstonesToPersist = new Map();
684
908
  let allowedDomainsBySession = new Map();
685
909
  let attachedSessionKeys = new Set();
686
910
  let networkRoutesBySession = new Map();
@@ -691,9 +915,160 @@ export default function agentBrowserExtension(pi) {
691
915
  const managedSessionRestoreState = new ManagedSessionRestoreState();
692
916
  const ownedManagedSessions = new Map();
693
917
  const managedSessionExecutionQueue = new AsyncExecutionQueue();
918
+ const artifactExecutionQueue = new AsyncExecutionQueue();
694
919
  const callerOwnedSessionExecutionQueues = new KeyedAsyncExecutionQueue();
920
+ const activeScriptControllers = new Set();
921
+ const activeScriptExecutions = new Set();
695
922
  let branchRestoreGeneration = 0;
696
923
  let branchStateGeneration = 0;
924
+ const validatedUpstreamPathKeys = new Set();
925
+ const appendRecordingTransitions = (transitions) => {
926
+ for (const transition of transitions) {
927
+ const key = getAgentBrowserSessionIdentityKey(transition.reservation.sessionName, transition.reservation.namespace);
928
+ if (transition.state === "active") {
929
+ recordingSessionTombstones.delete(key);
930
+ recordingSessionTombstonesToPersist.delete(key);
931
+ }
932
+ else {
933
+ recordingSessionTombstones.set(key, transition.reservation);
934
+ }
935
+ try {
936
+ appendRecordingReservationTransition(pi, transition);
937
+ recordingSessionTombstonesToPersist.delete(key);
938
+ }
939
+ catch {
940
+ if (transition.state === "closed")
941
+ recordingSessionTombstonesToPersist.set(key, transition.reservation);
942
+ }
943
+ }
944
+ };
945
+ const appendActiveRecordingCleanupAction = (result, reservation) => {
946
+ if (result.isError !== true)
947
+ return result;
948
+ const details = isRecord(result.details) ? result.details : {};
949
+ const nextActions = Array.isArray(details.nextActions) ? [...details.nextActions] : [];
950
+ if (nextActions.some((action) => action.id === "stop-pending-recording"))
951
+ return result;
952
+ const stopActions = applyNamespaceToNextActions(applySessionToNextActions([
953
+ buildNextToolAction({
954
+ args: ["record", "stop"],
955
+ id: "stop-pending-recording",
956
+ reason: "Stop the active recording so the requested video can be finalized and verified on disk.",
957
+ safety: "The file remains pending until record stop succeeds; verify details.artifactVerification afterward.",
958
+ }),
959
+ ], reservation.sessionName), reservation.namespace);
960
+ appendUniqueAgentBrowserNextActions(nextActions, stopActions);
961
+ const cleanupNotice = "An active recording remains open. Use the exact stop-pending-recording payload in details.nextActions before leaving this session.";
962
+ let noticeAppended = false;
963
+ const content = result.content.map((item) => {
964
+ if (noticeAppended || item.type !== "text")
965
+ return item;
966
+ noticeAppended = true;
967
+ return { ...item, text: `${item.text}\n\n${cleanupNotice}` };
968
+ });
969
+ if (!noticeAppended)
970
+ content.push({ type: "text", text: cleanupNotice });
971
+ return { ...result, content, details: { ...details, nextActions } };
972
+ };
973
+ const retireRecordingSession = (sessionName, namespace, retireManifest = true) => {
974
+ const reservation = retireRecordingReservation(activeRecordingReservations, sessionName, namespace);
975
+ const previousManifest = artifactManifest;
976
+ if (retireManifest && artifactManifest)
977
+ artifactManifest = retirePendingRecordingManifestEntries(artifactManifest, sessionName, namespace);
978
+ if (!reservation && artifactManifest === previousManifest)
979
+ return;
980
+ const terminalReservation = reservation ?? { absolutePath: "", cwd: managedSessionCwd, namespace, path: "", sessionName };
981
+ const terminalKey = getAgentBrowserSessionIdentityKey(sessionName, namespace);
982
+ recordingSessionTombstones.set(terminalKey, terminalReservation);
983
+ try {
984
+ appendRecordingReservationTransition(pi, {
985
+ reservation: terminalReservation,
986
+ state: "closed",
987
+ });
988
+ recordingSessionTombstonesToPersist.delete(terminalKey);
989
+ }
990
+ catch {
991
+ recordingSessionTombstonesToPersist.set(terminalKey, terminalReservation);
992
+ }
993
+ };
994
+ const syncRecordingReservationsFromResult = (result) => {
995
+ const handledClosedSessionKeys = new Set();
996
+ const details = isRecord(result.details) ? result.details : undefined;
997
+ const batchSteps = Array.isArray(details?.batchSteps) ? details.batchSteps : undefined;
998
+ const resultSessionName = typeof details?.sessionName === "string" ? details.sessionName : undefined;
999
+ const resultNamespace = typeof details?.namespace === "string" ? details.namespace : undefined;
1000
+ if (!batchSteps) {
1001
+ appendRecordingTransitions(applyRecordingArtifactsToReservations(activeRecordingReservations, getResultFileArtifacts(result)));
1002
+ return handledClosedSessionKeys;
1003
+ }
1004
+ let sessionClosed = false;
1005
+ for (const step of batchSteps) {
1006
+ if (!isRecord(step))
1007
+ continue;
1008
+ const command = Array.isArray(step.command) && step.command.every((token) => typeof token === "string") ? step.command : undefined;
1009
+ const commandTokens = command ? extractUpstreamCommandTokens(command) : [];
1010
+ const commandName = commandTokens[0];
1011
+ if (resultSessionName && batchStepReportsNoRecordingInProgress(step)) {
1012
+ const sessionKey = getAgentBrowserSessionIdentityKey(resultSessionName, resultNamespace);
1013
+ retireRecordingSession(resultSessionName, resultNamespace, false);
1014
+ handledClosedSessionKeys.add(sessionKey);
1015
+ continue;
1016
+ }
1017
+ if (step.success === true && commandName && isCloseCommand(commandName) && resultSessionName) {
1018
+ const sessionKey = getAgentBrowserSessionIdentityKey(resultSessionName, resultNamespace);
1019
+ retireRecordingSession(resultSessionName, resultNamespace, false);
1020
+ handledClosedSessionKeys.add(sessionKey);
1021
+ sessionClosed = true;
1022
+ continue;
1023
+ }
1024
+ if (sessionClosed && commandName === "record")
1025
+ continue;
1026
+ if (step.success === true && sessionClosed)
1027
+ sessionClosed = false;
1028
+ const artifacts = Array.isArray(step.artifacts) ? step.artifacts.filter(isResultFileArtifact) : [];
1029
+ appendRecordingTransitions(applyRecordingArtifactsToReservations(activeRecordingReservations, artifacts));
1030
+ }
1031
+ for (const sessionKey of handledClosedSessionKeys) {
1032
+ if (!activeRecordingReservations.has(sessionKey) && artifactManifest && resultSessionName) {
1033
+ artifactManifest = retirePendingRecordingManifestEntries(artifactManifest, resultSessionName, resultNamespace);
1034
+ }
1035
+ }
1036
+ return handledClosedSessionKeys;
1037
+ };
1038
+ const validateUpstreamVersion = async (cwd, signal) => {
1039
+ const processEnvironment = getAgentBrowserProcessEnvironment();
1040
+ const pathKey = `${cwd}\0${processEnvironment.PATH ?? processEnvironment.Path ?? ""}`;
1041
+ if (validatedUpstreamPathKeys.has(pathKey))
1042
+ return undefined;
1043
+ const probe = await runAgentBrowserProcess({ args: ["--version"], cwd, signal, timeoutMs: 5_000 });
1044
+ if (probe.spawnError?.code === "ENOENT" || probe.exitCode === 127 || probe.aborted)
1045
+ return undefined;
1046
+ let error;
1047
+ let observedVersion;
1048
+ if (probe.spawnError || probe.exitCode !== 0) {
1049
+ const detail = redactSensitiveText(probe.spawnError?.message ?? (probe.stderr.trim() || `exit ${probe.exitCode}`));
1050
+ error = `agent-browser --version could not be validated (${detail}). Run pi-agent-browser-doctor before browser-backed calls.`;
1051
+ }
1052
+ else {
1053
+ observedVersion = parseAgentBrowserVersionOutput(probe.stdout);
1054
+ error = getAgentBrowserVersionValidationError(probe.stdout);
1055
+ }
1056
+ if (!error) {
1057
+ validatedUpstreamPathKeys.add(pathKey);
1058
+ return undefined;
1059
+ }
1060
+ return {
1061
+ content: [{ type: "text", text: error }],
1062
+ details: {
1063
+ expectedVersion: TARGET_AGENT_BROWSER_VERSION,
1064
+ failureCategory: "validation-error",
1065
+ observedVersion,
1066
+ resultCategory: "failure",
1067
+ versionValidation: { expected: TARGET_AGENT_BROWSER_VERSION_LABEL, observed: observedVersion },
1068
+ },
1069
+ isError: true,
1070
+ };
1071
+ };
697
1072
  const clearSessionScopedBrowserState = (sessionName, namespace) => {
698
1073
  const key = getSessionContextKey(sessionName, namespace) ?? sessionName;
699
1074
  allowedDomainsBySession = new Map(allowedDomainsBySession);
@@ -701,8 +1076,51 @@ export default function agentBrowserExtension(pi) {
701
1076
  attachedSessionKeys.delete(key);
702
1077
  networkRoutesBySession = new Map(networkRoutesBySession);
703
1078
  networkRoutesBySession.delete(key);
1079
+ traceOwners.delete(key);
704
1080
  sessionPageState.clearSession(key);
705
1081
  };
1082
+ const closeScriptSessionLeaseWithinQueue = async (sessionName, cwd) => {
1083
+ const closeError = await withIsolatedAgentBrowserEnvironment(() => closeManagedSession({
1084
+ cwd,
1085
+ namespace: AGENT_BROWSER_SCRIPT_NAMESPACE,
1086
+ restoreState: managedSessionRestoreState,
1087
+ sessionName,
1088
+ timeoutMs: implicitSessionCloseTimeoutMs,
1089
+ }));
1090
+ if (closeError) {
1091
+ try {
1092
+ appendScriptSessionLease(pi, sessionName, "failed");
1093
+ }
1094
+ catch { }
1095
+ return redactSensitiveText(closeError);
1096
+ }
1097
+ try {
1098
+ appendScriptSessionLease(pi, sessionName, "closed");
1099
+ }
1100
+ catch {
1101
+ managedSessionRestoreState.disable(sessionName);
1102
+ return "The isolated session closed, but its durable cleanup record could not be saved.";
1103
+ }
1104
+ untrackOwnedManagedSession(ownedManagedSessions, sessionName, AGENT_BROWSER_SCRIPT_NAMESPACE);
1105
+ managedSessionRestoreState.clear(sessionName, AGENT_BROWSER_SCRIPT_NAMESPACE);
1106
+ retireRecordingSession(sessionName, AGENT_BROWSER_SCRIPT_NAMESPACE);
1107
+ clearSessionScopedBrowserState(sessionName, AGENT_BROWSER_SCRIPT_NAMESPACE);
1108
+ return undefined;
1109
+ };
1110
+ const recoverScriptSessionLeasesWithinQueue = async (ctx) => {
1111
+ const pendingSessionNames = new Set([...ownedManagedSessions.values()]
1112
+ .map((session) => session.sessionName)
1113
+ .filter(isAgentBrowserScriptSessionName));
1114
+ for (const lease of getScriptSessionLeasesFromBranch(ctx.sessionManager.getBranch()).values()) {
1115
+ if (lease.cleanup !== "closed")
1116
+ pendingSessionNames.add(lease.sessionName);
1117
+ }
1118
+ for (const sessionName of pendingSessionNames) {
1119
+ trackOwnedManagedSession(ownedManagedSessions, sessionName, ctx.cwd, { branchOwned: true, namespace: AGENT_BROWSER_SCRIPT_NAMESPACE });
1120
+ managedSessionRestoreState.disable(sessionName, AGENT_BROWSER_SCRIPT_NAMESPACE);
1121
+ await closeScriptSessionLeaseWithinQueue(sessionName, ctx.cwd);
1122
+ }
1123
+ };
706
1124
  const restoreBranchBackedState = (ctx, options) => {
707
1125
  branchRestoreGeneration += 1;
708
1126
  branchStateGeneration += 1;
@@ -750,13 +1168,29 @@ export default function agentBrowserExtension(pi) {
750
1168
  sessionPageState = SessionPageState.fromBranch(branch);
751
1169
  traceOwners = new Map();
752
1170
  artifactManifest = restoreArtifactManifestFromBranch(branch);
1171
+ const restoredRecordingState = restoreRecordingReservationStateFromBranch(branch);
1172
+ for (const [key, reservation] of recordingSessionTombstones) {
1173
+ if (restoredRecordingState.terminal.has(key))
1174
+ recordingSessionTombstonesToPersist.delete(key);
1175
+ else
1176
+ recordingSessionTombstonesToPersist.set(key, reservation);
1177
+ }
1178
+ for (const [key, reservation] of restoredRecordingState.terminal) {
1179
+ if (!activeRecordingReservations.has(key))
1180
+ recordingSessionTombstones.set(key, reservation);
1181
+ }
1182
+ for (const key of recordingSessionTombstones.keys())
1183
+ restoredRecordingState.active.delete(key);
1184
+ for (const [key, reservation] of activeRecordingReservations)
1185
+ restoredRecordingState.active.set(key, reservation);
1186
+ activeRecordingReservations = restoredRecordingState.active;
753
1187
  allowedDomainsBySession = restoreAllowedDomainsBySessionFromBranch(branch);
754
1188
  attachedSessionKeys = restoreAttachedSessionKeysFromBranch(branch);
755
1189
  networkRoutesBySession = new Map();
756
1190
  electronLaunchRecords = restoreElectronLaunchRecordsFromBranch(branch);
757
1191
  for (const record of getActiveElectronRecords(electronLaunchRecords)) {
758
1192
  if (record.sessionName)
759
- attachedSessionKeys.add(getSessionContextKey(record.sessionName) ?? record.sessionName);
1193
+ attachedSessionKeys.add(getSessionContextKey(record.sessionName, record.namespace) ?? record.sessionName);
760
1194
  }
761
1195
  if (options.resetRuntimeOwnership) {
762
1196
  ownedManagedSessions.clear();
@@ -778,6 +1212,7 @@ export default function agentBrowserExtension(pi) {
778
1212
  continue;
779
1213
  trackOwnedManagedSession(ownedManagedSessions, identity.sessionName, ctx.cwd, {
780
1214
  branchOwned: true,
1215
+ compatibilityWorkaround: restoreManagedSessionCompatibilityWorkaroundFromBranch(branch, identity.sessionName, identity.namespace),
781
1216
  headedManagedAutosaveDisabled: restoreManagedSessionHeadedAutosaveDisabledFromBranch(branch, identity.sessionName, identity.namespace),
782
1217
  headedManagedAutosaveInterval: restoreManagedSessionHeadedAutosaveIntervalFromBranch(branch, identity.sessionName, identity.namespace),
783
1218
  namespace: identity.namespace,
@@ -786,6 +1221,7 @@ export default function agentBrowserExtension(pi) {
786
1221
  if (restoredState.active) {
787
1222
  trackOwnedManagedSession(ownedManagedSessions, restoredState.sessionName, ctx.cwd, {
788
1223
  branchOwned: true,
1224
+ compatibilityWorkaround: managedSessionCompatibilityWorkaround,
789
1225
  headedManagedAutosaveDisabled: managedSessionHeadedAutosaveDisabled,
790
1226
  headedManagedAutosaveInterval: managedSessionHeadedAutosaveInterval,
791
1227
  namespace: restoredState.namespace,
@@ -841,17 +1277,25 @@ export default function agentBrowserExtension(pi) {
841
1277
  cwd: ctx.cwd,
842
1278
  includeProjectConfig: shouldIncludeProjectConfig(ctx),
843
1279
  }));
1280
+ await artifactExecutionQueue.run(() => managedSessionExecutionQueue.run(() => recoverScriptSessionLeasesWithinQueue(ctx)));
844
1281
  });
845
1282
  pi.on("session_tree", async (_event, ctx) => {
846
- await managedSessionExecutionQueue.run(async () => {
1283
+ for (const controller of activeScriptControllers)
1284
+ controller.abort();
1285
+ await Promise.allSettled([...activeScriptExecutions]);
1286
+ await artifactExecutionQueue.run(() => managedSessionExecutionQueue.run(async () => {
847
1287
  restoreBranchBackedState(ctx, { resetRuntimeOwnership: false });
848
- });
1288
+ await recoverScriptSessionLeasesWithinQueue(ctx);
1289
+ }));
849
1290
  });
850
1291
  pi.on("session_shutdown", async (event, ctx) => {
1292
+ for (const controller of activeScriptControllers)
1293
+ controller.abort();
1294
+ await Promise.allSettled([...activeScriptExecutions]);
851
1295
  branchRestoreGeneration += 1;
852
1296
  branchStateGeneration += 1;
853
1297
  let preservedElectronProfileDirs = [];
854
- await managedSessionExecutionQueue.run(async () => {
1298
+ await artifactExecutionQueue.run(() => managedSessionExecutionQueue.run(async () => {
855
1299
  const shutdownCwd = ctx?.cwd ?? managedSessionCwd;
856
1300
  const quitting = event?.reason === "quit";
857
1301
  preservedElectronProfileDirs = quitting
@@ -874,21 +1318,38 @@ export default function agentBrowserExtension(pi) {
874
1318
  ...getCleanupResultsPreservedUserDataDirs(electronCleanupResults),
875
1319
  ])];
876
1320
  syncElectronCleanupManagedSessions(ownedManagedSessions, electronCleanupResults);
1321
+ for (const identity of getCleanupResultsClosedManagedSessionIdentities(electronCleanupResults))
1322
+ retireRecordingSession(identity.sessionName, identity.namespace);
877
1323
  if (quitting) {
878
- await closeOwnedManagedSessions(ownedManagedSessions, managedSessionRestoreState, implicitSessionCloseTimeoutMs, attachedSessionKeys);
1324
+ await closeOwnedManagedSessions(ownedManagedSessions, managedSessionRestoreState, implicitSessionCloseTimeoutMs, attachedSessionKeys, (owner) => retireRecordingSession(owner.sessionName, owner.namespace));
879
1325
  }
880
1326
  else {
881
- await closeOwnedManagedSessionsExcept(ownedManagedSessions, managedSessionRestoreState, managedSessionActive ? managedSessionName : undefined, implicitSessionCloseTimeoutMs, attachedSessionKeys, managedSessionActive ? managedSessionNamespace : undefined);
1327
+ await closeOwnedManagedSessionsExcept(ownedManagedSessions, managedSessionRestoreState, managedSessionActive ? managedSessionName : undefined, implicitSessionCloseTimeoutMs, attachedSessionKeys, managedSessionActive ? managedSessionNamespace : undefined, (owner) => retireRecordingSession(owner.sessionName, owner.namespace));
882
1328
  }
883
- });
1329
+ }));
884
1330
  managedSessionActive = false;
885
1331
  managedSessionCompatibilityWorkaround = undefined;
886
1332
  managedSessionHeadedAutosaveDisabled = false;
887
1333
  managedSessionHeadedAutosaveInterval = undefined;
888
1334
  managedSessionNamespace = undefined;
1335
+ for (const reservation of recordingSessionTombstonesToPersist.values()) {
1336
+ try {
1337
+ appendRecordingReservationTransition(pi, { reservation, state: "closed" });
1338
+ }
1339
+ catch { }
1340
+ }
1341
+ for (const reservation of activeRecordingReservations.values()) {
1342
+ try {
1343
+ appendRecordingReservationTransition(pi, { reservation, state: "active" });
1344
+ }
1345
+ catch { }
1346
+ }
889
1347
  sessionPageState.reset();
890
1348
  traceOwners = new Map();
891
1349
  artifactManifest = undefined;
1350
+ activeRecordingReservations = new Map();
1351
+ recordingSessionTombstones = new Map();
1352
+ recordingSessionTombstonesToPersist = new Map();
892
1353
  allowedDomainsBySession = new Map();
893
1354
  attachedSessionKeys = new Set();
894
1355
  networkRoutesBySession = new Map();
@@ -937,16 +1398,16 @@ export default function agentBrowserExtension(pi) {
937
1398
  }
938
1399
  });
939
1400
  pi.on("tool_result", async (event) => buildAgentBrowserToolResultPatch(event));
940
- pi.registerTool({
1401
+ const agentBrowserTool = {
941
1402
  name: "agent_browser",
942
1403
  label: "Agent Browser",
943
- description: "Browse and interact with websites using agent-browser. Use this for web research, reading live docs, opening pages, taking snapshots or screenshots, clicking links, filling forms, extracting page content, and authenticated/profile-based browser work. Input choice: default `args` for open → snapshot -i → click/fill @refs; `semanticAction` for stable role/text/label targets; `job` or `qa` for multi-step checks; `electron` only for desktop apps; experimental `sourceLookup` / `networkSourceLookup` for candidates only.",
1404
+ description: "Browse and interact with websites using agent-browser. Use this for web research, reading live docs, opening pages, taking snapshots or screenshots, clicking links, filling forms, extracting page content, and authenticated/profile-based browser work. Input choice: `script` for one-shot JavaScript orchestration; default `args` for open → snapshot -i → click/fill @refs; `semanticAction` for stable role/text/label targets; `job` or `qa` for multi-step checks; `electron` only for desktop apps; experimental `sourceLookup` / `networkSourceLookup` for candidates only.",
944
1405
  promptSnippet: "Browse websites, read live docs, click and fill pages, extract browser content, take screenshots, and automate real web workflows.",
945
1406
  promptGuidelines: toolPromptGuidelines,
946
1407
  parameters: AGENT_BROWSER_PARAMS,
947
1408
  renderCall(args, theme, context) {
948
1409
  const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
949
- text.setText(formatAgentBrowserRenderCall(args, theme));
1410
+ text.setText(formatAgentBrowserRenderCall(args, theme, context.expanded));
950
1411
  return text;
951
1412
  },
952
1413
  renderResult(result, options, theme, context) {
@@ -956,11 +1417,11 @@ export default function agentBrowserExtension(pi) {
956
1417
  component.setState(formatAgentBrowserRenderResult(result, options, theme, context.isError), options.expanded, theme);
957
1418
  return component;
958
1419
  },
959
- async execute(_toolCallId, params, signal, onUpdate, ctx) {
1420
+ async execute(toolCallId, params, signal, onUpdate, ctx) {
960
1421
  const promptPolicy = buildPromptPolicy(getLatestUserPrompt(ctx.sessionManager.getBranch()));
961
1422
  const outputPath = isRecord(params) && typeof params.outputPath === "string" ? params.outputPath : undefined;
962
1423
  const resolvedInput = resolveAgentBrowserInput({
963
- getBatchPreflightValidationError,
1424
+ getBatchPreflightValidationError: (args, stdin) => getArtifactPreflightValidationError({ args, cwd: ctx.cwd, outputPath, stdin }),
964
1425
  managedSessionActive,
965
1426
  params,
966
1427
  });
@@ -971,6 +1432,126 @@ export default function agentBrowserExtension(pi) {
971
1432
  if (outputPathValidationError) {
972
1433
  return buildValidationFailureResult({ attemptedKind: resolvedInput.kind, kind: "invalid", redactedArgs: resolvedInput.redactedArgs, status: "invalid", toolArgs: resolvedInput.toolArgs, toolStdin: resolvedInput.toolStdin, validationError: outputPathValidationError });
973
1434
  }
1435
+ const applyUnserializedOutputPath = async (result, preserveTextContent = false) => {
1436
+ if (!outputPath || result.isError === true || (isRecord(result.details) && result.details.resultCategory === "failure"))
1437
+ return result;
1438
+ return artifactExecutionQueue.run(async () => {
1439
+ const reservationError = getArtifactPreflightValidationError({
1440
+ activeRecordingReservations: activeRecordingReservations.values(),
1441
+ args: [],
1442
+ cwd: ctx.cwd,
1443
+ outputPath,
1444
+ });
1445
+ if (reservationError) {
1446
+ return buildValidationFailureResult({ attemptedKind: resolvedInput.kind, kind: "invalid", redactedArgs: resolvedInput.redactedArgs, status: "invalid", toolArgs: resolvedInput.toolArgs, toolStdin: resolvedInput.toolStdin, validationError: reservationError });
1447
+ }
1448
+ return applyAgentBrowserOutputPath({ cwd: ctx.cwd, outputPath, preserveTextContent, result });
1449
+ });
1450
+ };
1451
+ const versionCheckCommand = extractUpstreamCommandTokens(resolvedInput.toolArgs)[0];
1452
+ const electronHostOnlyAction = resolvedInput.kind === "electron" && ["cleanup", "list", "status"].includes(resolvedInput.compiledElectron.action);
1453
+ const browserBackedVersionCheck = needsManagedSession(parseArgvDescriptor(resolvedInput.toolArgs));
1454
+ if (!electronHostOnlyAction && browserBackedVersionCheck && !isPlainTextInspectionArgs(resolvedInput.toolArgs) && !isCloseCommand(versionCheckCommand) && signal?.aborted !== true) {
1455
+ const versionFailure = resolvedInput.kind === "script"
1456
+ ? await withIsolatedAgentBrowserEnvironment(() => validateUpstreamVersion(ctx.cwd, signal))
1457
+ : await validateUpstreamVersion(ctx.cwd, signal);
1458
+ if (versionFailure)
1459
+ return applyAgentBrowserOutputPath({ cwd: ctx.cwd, outputPath, result: versionFailure });
1460
+ }
1461
+ if (resolvedInput.kind === "script") {
1462
+ if (!ctx.sessionManager.getSessionFile()) {
1463
+ return buildValidationFailureResult({
1464
+ attemptedKind: "script",
1465
+ kind: "invalid",
1466
+ redactedArgs: [],
1467
+ status: "invalid",
1468
+ toolArgs: [],
1469
+ validationError: "script requires a persisted Pi session so its isolated browser-session cleanup lease survives restart; relaunch Pi without --no-session.",
1470
+ });
1471
+ }
1472
+ const sessionName = createAgentBrowserScriptSessionName();
1473
+ const innerResults = [];
1474
+ const scriptTimeoutMs = params.timeoutMs ?? AGENT_BROWSER_SCRIPT_DEFAULT_TIMEOUT_MS;
1475
+ const deadline = Date.now() + scriptTimeoutMs;
1476
+ let leased = false;
1477
+ let cleanupError;
1478
+ let run = {
1479
+ callCount: 0,
1480
+ emitCount: 0,
1481
+ error: "Script sandbox execution failed.",
1482
+ failureCategory: "upstream-error",
1483
+ ok: false,
1484
+ rejectedCallCount: 0,
1485
+ steps: [],
1486
+ };
1487
+ const scriptController = new AbortController();
1488
+ const abortScript = () => scriptController.abort();
1489
+ signal?.addEventListener("abort", abortScript, { once: true });
1490
+ if (signal?.aborted)
1491
+ scriptController.abort();
1492
+ activeScriptControllers.add(scriptController);
1493
+ let finishScriptExecution;
1494
+ const scriptExecution = new Promise((resolve) => {
1495
+ finishScriptExecution = resolve;
1496
+ });
1497
+ activeScriptExecutions.add(scriptExecution);
1498
+ try {
1499
+ const pendingRun = runAgentBrowserScript({
1500
+ beforeFirstCall() {
1501
+ appendScriptSessionLease(pi, sessionName, "active");
1502
+ trackOwnedManagedSession(ownedManagedSessions, sessionName, ctx.cwd, { namespace: AGENT_BROWSER_SCRIPT_NAMESPACE });
1503
+ managedSessionRestoreState.disable(sessionName, AGENT_BROWSER_SCRIPT_NAMESPACE);
1504
+ leased = true;
1505
+ },
1506
+ code: resolvedInput.compiledScript.code,
1507
+ dispatch: async (innerParams, innerSignal) => {
1508
+ const remainingMs = Math.max(1, deadline - Date.now());
1509
+ const innerTimeoutMs = Math.min(innerParams.timeoutMs ?? remainingMs, remainingMs);
1510
+ const innerResult = await withIsolatedAgentBrowserEnvironment(() => agentBrowserTool.execute(`${toolCallId}:script:${innerResults.length + 1}`, {
1511
+ args: ["--namespace", AGENT_BROWSER_SCRIPT_NAMESPACE, "--session", sessionName, ...innerParams.args],
1512
+ stdin: innerParams.stdin,
1513
+ timeoutMs: innerTimeoutMs,
1514
+ }, innerSignal, undefined, ctx));
1515
+ innerResults.push(innerResult);
1516
+ return await buildScriptBrowserEnvelope(innerResult, innerParams.args, sessionName);
1517
+ },
1518
+ signal: scriptController.signal,
1519
+ timeoutMs: scriptTimeoutMs,
1520
+ });
1521
+ run = await pendingRun;
1522
+ }
1523
+ catch { }
1524
+ finally {
1525
+ activeScriptControllers.delete(scriptController);
1526
+ signal?.removeEventListener("abort", abortScript);
1527
+ if (leased) {
1528
+ try {
1529
+ cleanupError = await artifactExecutionQueue.run(() => managedSessionExecutionQueue.run(() => closeScriptSessionLeaseWithinQueue(sessionName, ctx.cwd)));
1530
+ }
1531
+ catch {
1532
+ cleanupError = "The isolated script session cleanup operation failed.";
1533
+ try {
1534
+ appendScriptSessionLease(pi, sessionName, "failed");
1535
+ }
1536
+ catch { }
1537
+ }
1538
+ }
1539
+ activeScriptExecutions.delete(scriptExecution);
1540
+ finishScriptExecution();
1541
+ }
1542
+ let scriptResult = buildScriptToolResult({ cleanupError, innerResults, run, sessionName: leased ? sessionName : undefined });
1543
+ if (artifactManifest) {
1544
+ scriptResult = {
1545
+ ...scriptResult,
1546
+ details: {
1547
+ ...(isRecord(scriptResult.details) ? scriptResult.details : {}),
1548
+ artifactManifest,
1549
+ artifactRetentionSummary: formatSessionArtifactRetentionSummary(artifactManifest),
1550
+ },
1551
+ };
1552
+ }
1553
+ return applyUnserializedOutputPath(scriptResult);
1554
+ }
974
1555
  const { toolArgs } = resolvedInput;
975
1556
  const compiledElectron = resolvedInput.kind === "electron" ? resolvedInput.compiledElectron : undefined;
976
1557
  const redactedCompiledElectron = resolvedInput.kind === "electron" ? resolvedInput.redactedCompiledElectron : undefined;
@@ -980,7 +1561,7 @@ export default function agentBrowserExtension(pi) {
980
1561
  compiledElectron,
981
1562
  ownedRecords: ownedElectronLaunchRecords,
982
1563
  });
983
- const electronHostResult = await handleElectronHostInput({
1564
+ let electronHostResult = await handleElectronHostInput({
984
1565
  attachedSessionKeys,
985
1566
  compiledElectron,
986
1567
  cwd: ctx.cwd,
@@ -1012,11 +1593,16 @@ export default function agentBrowserExtension(pi) {
1012
1593
  }
1013
1594
  replaceWithActiveElectronLaunchRecords(ownedElectronLaunchRecords, electronHostLaunchRecords, branchOwnedElectronLaunchIds, cleanedLaunchIds);
1014
1595
  mergeElectronCleanupRecords(electronLaunchRecords, cleanupRecords);
1015
- const closedSessionNames = getCleanupResultsClosedManagedSessionNames(cleanupRecords);
1016
- syncElectronCleanupManagedSessions(ownedManagedSessions, cleanupRecords);
1017
- for (const closedSessionName of closedSessionNames) {
1018
- clearSessionScopedBrowserState(closedSessionName);
1019
- if (closedSessionName === managedSessionName) {
1596
+ const cleanupNamespace = isRecord(electronHostResult.details) && typeof electronHostResult.details.namespace === "string"
1597
+ ? electronHostResult.details.namespace
1598
+ : undefined;
1599
+ const closedSessionIdentities = getCleanupResultsClosedManagedSessionIdentities(cleanupRecords, cleanupNamespace);
1600
+ syncElectronCleanupManagedSessions(ownedManagedSessions, cleanupRecords, cleanupNamespace);
1601
+ for (const identity of closedSessionIdentities) {
1602
+ retireRecordingSession(identity.sessionName, identity.namespace);
1603
+ const closedSessionKey = getSessionContextKey(identity.sessionName, identity.namespace) ?? identity.sessionName;
1604
+ clearSessionScopedBrowserState(closedSessionKey);
1605
+ if (closedSessionKey === (getSessionContextKey(managedSessionName, managedSessionNamespace) ?? managedSessionName)) {
1020
1606
  managedSessionActive = false;
1021
1607
  managedSessionCompatibilityWorkaround = undefined;
1022
1608
  managedSessionHeadedAutosaveDisabled = false;
@@ -1026,14 +1612,39 @@ export default function agentBrowserExtension(pi) {
1026
1612
  managedSessionName = createFreshSessionName(managedSessionBaseName, ephemeralSessionSeed, freshSessionOrdinal);
1027
1613
  }
1028
1614
  }
1615
+ if (artifactManifest) {
1616
+ electronHostResult = {
1617
+ ...electronHostResult,
1618
+ details: {
1619
+ ...(isRecord(electronHostResult.details) ? electronHostResult.details : {}),
1620
+ artifactManifest,
1621
+ artifactRetentionSummary: formatSessionArtifactRetentionSummary(artifactManifest),
1622
+ },
1623
+ };
1624
+ }
1029
1625
  }
1030
1626
  return electronHostResult;
1031
1627
  };
1032
- const electronHostResult = shouldSerializeElectronHostInput(compiledElectron)
1033
- ? await managedSessionExecutionQueue.run(runElectronHostInput)
1034
- : await runElectronHostInput();
1628
+ const runSerializedElectronHostInput = () => shouldSerializeElectronHostInput(compiledElectron)
1629
+ ? managedSessionExecutionQueue.run(runElectronHostInput)
1630
+ : runElectronHostInput();
1631
+ const electronHostResult = compiledElectron?.action === "cleanup"
1632
+ ? await artifactExecutionQueue.run(async () => {
1633
+ const reservationError = outputPath ? getArtifactPreflightValidationError({
1634
+ activeRecordingReservations: activeRecordingReservations.values(),
1635
+ args: [],
1636
+ cwd: ctx.cwd,
1637
+ outputPath,
1638
+ }) : undefined;
1639
+ if (reservationError) {
1640
+ return buildValidationFailureResult({ attemptedKind: resolvedInput.kind, kind: "invalid", redactedArgs: resolvedInput.redactedArgs, status: "invalid", toolArgs: resolvedInput.toolArgs, toolStdin: resolvedInput.toolStdin, validationError: reservationError });
1641
+ }
1642
+ const result = await runSerializedElectronHostInput();
1643
+ return result ? applyAgentBrowserOutputPath({ cwd: ctx.cwd, outputPath, result }) : result;
1644
+ })
1645
+ : await runSerializedElectronHostInput();
1035
1646
  if (electronHostResult) {
1036
- return applyAgentBrowserOutputPath({ cwd: ctx.cwd, outputPath, result: electronHostResult });
1647
+ return compiledElectron?.action === "cleanup" ? electronHostResult : applyUnserializedOutputPath(electronHostResult);
1037
1648
  }
1038
1649
  const explicitSessionName = extractExplicitSessionName(toolArgs);
1039
1650
  const explicitNamespace = extractExplicitNamespace(toolArgs);
@@ -1044,8 +1655,11 @@ export default function agentBrowserExtension(pi) {
1044
1655
  ownedElectronLaunchRecords,
1045
1656
  ownedManagedSessions,
1046
1657
  });
1658
+ const callerOwnedSessionNamespace = explicitSessionName
1659
+ ? resolveAgentBrowserNamespace(toolArgs, getAgentBrowserProcessEnvironment().AGENT_BROWSER_NAMESPACE)
1660
+ : undefined;
1047
1661
  const callerOwnedSessionQueueKey = !serializeBrowserCommand && explicitSessionName
1048
- ? getSessionContextKey(explicitSessionName, resolveAgentBrowserNamespace(toolArgs, process.env.AGENT_BROWSER_NAMESPACE)) ?? explicitSessionName
1662
+ ? getSessionContextKey(explicitSessionName, callerOwnedSessionNamespace) ?? explicitSessionName
1049
1663
  : undefined;
1050
1664
  const runBrowserCommand = async () => {
1051
1665
  const branchRestoreGenerationAtStart = branchRestoreGeneration;
@@ -1103,20 +1717,30 @@ export default function agentBrowserExtension(pi) {
1103
1717
  state: browserRunState,
1104
1718
  });
1105
1719
  const branchRestoreStillCurrent = branchRestoreGenerationAtStart === branchRestoreGeneration;
1720
+ const resultDetails = isRecord(result.details) ? result.details : undefined;
1721
+ const resultSessionName = typeof resultDetails?.sessionName === "string"
1722
+ ? resultDetails.sessionName
1723
+ : extractExplicitSessionName(toolArgs);
1724
+ const resultNamespace = typeof resultDetails?.namespace === "string"
1725
+ ? resultDetails.namespace
1726
+ : resolveAgentBrowserNamespace(toolArgs, getAgentBrowserProcessEnvironment().AGENT_BROWSER_NAMESPACE);
1106
1727
  if (branchRestoreStillCurrent) {
1107
- const resultDetails = isRecord(result.details) ? result.details : undefined;
1108
- const resultSessionName = typeof resultDetails?.sessionName === "string"
1109
- ? resultDetails.sessionName
1110
- : extractExplicitSessionName(toolArgs);
1111
- const resultNamespace = typeof resultDetails?.namespace === "string"
1112
- ? resultDetails.namespace
1113
- : resolveAgentBrowserNamespace(toolArgs, process.env.AGENT_BROWSER_NAMESPACE);
1728
+ const resultBatchCloseLifecycle = getSuccessfulBatchCloseLifecycle(resultDetails?.batchSteps);
1114
1729
  const resultSessionKey = getSessionContextKey(resultSessionName, resultNamespace) ?? resultSessionName;
1115
1730
  const managedSessionOutcome = isRecord(resultDetails?.managedSessionOutcome) ? resultDetails.managedSessionOutcome : undefined;
1731
+ const closeAllApplied = resultDetails?.closeAllApplied === true;
1116
1732
  const attachedSessionRemainsActive = result.isError !== true
1117
- || (attachedSessionRequested && managedSessionOutcome?.activeAfter === true);
1118
- const closesAttachedSession = result.isError !== true && isCloseCommand(extractCommandTokens(toolArgs)[0]);
1119
- if (resultSessionKey && closesAttachedSession)
1733
+ || ((attachedSessionRequested || attachedSessionKnown) && managedSessionOutcome?.activeAfter === true);
1734
+ const closesAttachedSession = (result.isError !== true && isCloseCommand(extractUpstreamCommandTokens(toolArgs)[0]))
1735
+ || resultBatchCloseLifecycle?.endsClosed === true;
1736
+ if (closeAllApplied) {
1737
+ deleteIdentityKeysInNamespace(attachedSessionKeys, resultNamespace);
1738
+ if (resultSessionKey && attachedSessionRemainsActive && (attachedSessionRequested || attachedSessionKnown) && resultBatchCloseLifecycle?.endsClosed === false) {
1739
+ attachedSessionKeys.add(resultSessionKey);
1740
+ result = { ...result, details: { ...(resultDetails ?? {}), attachedBrowserSession: true } };
1741
+ }
1742
+ }
1743
+ else if (resultSessionKey && closesAttachedSession)
1120
1744
  attachedSessionKeys.delete(resultSessionKey);
1121
1745
  else if (resultSessionKey && attachedSessionRemainsActive && (attachedSessionRequested || attachedSessionKnown)) {
1122
1746
  attachedSessionKeys.add(resultSessionKey);
@@ -1127,6 +1751,29 @@ export default function agentBrowserExtension(pi) {
1127
1751
  allowedDomainsBySession = mergeBrowserRunMap(allowedDomainsBySession, initialAllowedDomainsBySession, browserRunState.allowedDomainsBySession);
1128
1752
  networkRoutesBySession = mergeBrowserRunMap(networkRoutesBySession, initialNetworkRoutesBySession, browserRunState.networkRoutesBySession);
1129
1753
  artifactManifest = mergeBrowserRunArtifactManifest(artifactManifest, initialArtifactManifest, browserRunState.artifactManifest);
1754
+ const handledBatchCloseKeys = syncRecordingReservationsFromResult(result);
1755
+ if (resultDetails?.closeAllApplied === true) {
1756
+ for (const [sessionKey, reservation] of [...activeRecordingReservations]) {
1757
+ if (isAgentBrowserSessionIdentityKeyInNamespace(sessionKey, resultNamespace)) {
1758
+ retireRecordingSession(reservation.sessionName, reservation.namespace);
1759
+ }
1760
+ }
1761
+ }
1762
+ for (const closedSessionKey of browserRunState.closedManagedSessionNames) {
1763
+ if (handledBatchCloseKeys.has(closedSessionKey))
1764
+ continue;
1765
+ const reservation = activeRecordingReservations.get(closedSessionKey);
1766
+ if (reservation)
1767
+ retireRecordingSession(reservation.sessionName, reservation.namespace);
1768
+ }
1769
+ if (resultSessionName && resultReportsNoRecordingInProgress(result)) {
1770
+ retireRecordingSession(resultSessionName, resultNamespace);
1771
+ }
1772
+ if (resultSessionName) {
1773
+ const reservation = activeRecordingReservations.get(getAgentBrowserSessionIdentityKey(resultSessionName, resultNamespace));
1774
+ if (reservation)
1775
+ result = appendActiveRecordingCleanupAction(result, reservation);
1776
+ }
1130
1777
  if (artifactManifest) {
1131
1778
  result = {
1132
1779
  ...result,
@@ -1155,7 +1802,7 @@ export default function agentBrowserExtension(pi) {
1155
1802
  mergeActiveElectronLaunchRecords(ownedElectronLaunchRecords, electronLaunchRecords, {
1156
1803
  branchOwnedLaunchIds: branchOwnedElectronLaunchIds,
1157
1804
  touchedLaunchIds: !result.isError
1158
- ? getTouchedElectronLaunchIds(explicitSessionName ?? browserRunState.managedSessionName, electronLaunchRecords)
1805
+ ? getTouchedElectronLaunchIds(explicitSessionName ?? browserRunState.managedSessionName, electronLaunchRecords, explicitSessionName ? resolveAgentBrowserNamespace(toolArgs, getAgentBrowserProcessEnvironment().AGENT_BROWSER_NAMESPACE) : browserRunState.managedSessionNamespace)
1159
1806
  : undefined,
1160
1807
  });
1161
1808
  if (serializeBrowserCommand)
@@ -1163,12 +1810,47 @@ export default function agentBrowserExtension(pi) {
1163
1810
  }
1164
1811
  return applyAgentBrowserOutputPath({ cwd: ctx.cwd, outputPath, preserveTextContent: Array.isArray(params.args) && params.args.includes("--json"), result });
1165
1812
  };
1166
- if (serializeBrowserCommand)
1167
- return managedSessionExecutionQueue.run(runBrowserCommand);
1168
- return callerOwnedSessionQueueKey
1169
- ? callerOwnedSessionExecutionQueues.run(callerOwnedSessionQueueKey, runBrowserCommand)
1170
- : runBrowserCommand();
1813
+ const closesAllSessions = commandClosesAllSessions(toolArgs, resolvedInput.toolStdin);
1814
+ const closeAllNamespace = closesAllSessions
1815
+ ? resolveAgentBrowserNamespace(toolArgs, getAgentBrowserProcessEnvironment().AGENT_BROWSER_NAMESPACE)
1816
+ : undefined;
1817
+ const runWithinSessionQueue = () => {
1818
+ if (closesAllSessions)
1819
+ return managedSessionExecutionQueue.run(() => callerOwnedSessionExecutionQueues.runExclusive(closeAllNamespace, runBrowserCommand));
1820
+ if (serializeBrowserCommand)
1821
+ return managedSessionExecutionQueue.run(runBrowserCommand);
1822
+ return callerOwnedSessionQueueKey
1823
+ ? callerOwnedSessionExecutionQueues.run(callerOwnedSessionQueueKey, callerOwnedSessionNamespace, runBrowserCommand)
1824
+ : runBrowserCommand();
1825
+ };
1826
+ if (!commandTouchesArtifactLifecycle(toolArgs, resolvedInput.toolStdin, outputPath))
1827
+ return runWithinSessionQueue();
1828
+ return artifactExecutionQueue.run(async () => {
1829
+ const artifactValidationError = getArtifactPreflightValidationError({
1830
+ activeRecordingReservations: activeRecordingReservations.values(),
1831
+ args: toolArgs,
1832
+ cwd: ctx.cwd,
1833
+ outputPath,
1834
+ stdin: resolvedInput.toolStdin,
1835
+ });
1836
+ if (!artifactValidationError)
1837
+ return runWithinSessionQueue();
1838
+ return applyAgentBrowserOutputPath({
1839
+ cwd: ctx.cwd,
1840
+ outputPath,
1841
+ result: buildValidationFailureResult({
1842
+ attemptedKind: resolvedInput.kind,
1843
+ kind: "invalid",
1844
+ redactedArgs: resolvedInput.redactedArgs,
1845
+ status: "invalid",
1846
+ toolArgs: resolvedInput.toolArgs,
1847
+ toolStdin: resolvedInput.toolStdin,
1848
+ validationError: artifactValidationError,
1849
+ }),
1850
+ });
1851
+ });
1171
1852
  },
1172
- });
1853
+ };
1854
+ pi.registerTool(agentBrowserTool);
1173
1855
  registerWebSearchToolIfAvailable(agentBrowserConfig);
1174
1856
  }