pi-agent-browser-native 0.3.0 → 0.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/CHANGELOG.md +265 -0
  2. package/README.md +130 -54
  3. package/dist/extensions/agent-browser/index.js +781 -169
  4. package/dist/extensions/agent-browser/lib/argv-descriptor.js +35 -3
  5. package/dist/extensions/agent-browser/lib/argv-grammar.js +50 -2
  6. package/dist/extensions/agent-browser/lib/batch-lifecycle.js +71 -0
  7. package/dist/extensions/agent-browser/lib/command-policy.js +5 -8
  8. package/dist/extensions/agent-browser/lib/command-taxonomy.js +53 -12
  9. package/dist/extensions/agent-browser/lib/config-policy.js +25 -1
  10. package/dist/extensions/agent-browser/lib/config.js +1 -1
  11. package/dist/extensions/agent-browser/lib/input-modes/job.js +61 -13
  12. package/dist/extensions/agent-browser/lib/input-modes/lookups.js +2 -2
  13. package/dist/extensions/agent-browser/lib/input-modes/params.js +23 -24
  14. package/dist/extensions/agent-browser/lib/input-modes/script.js +462 -0
  15. package/dist/extensions/agent-browser/lib/input-modes/semantic-action.js +51 -12
  16. package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +26 -4
  17. package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +6 -139
  18. package/dist/extensions/agent-browser/lib/managed-session-restore.js +26 -116
  19. package/dist/extensions/agent-browser/lib/managed-session-snapshots.js +2 -4
  20. package/dist/extensions/agent-browser/lib/managed-session-storage.js +54 -25
  21. package/dist/extensions/agent-browser/lib/orchestration/batch-stdin.js +26 -5
  22. package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.js +110 -30
  23. package/dist/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.js +2 -1
  24. package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +54 -48
  25. package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +71 -5
  26. package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +2 -1
  27. package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +6 -7
  28. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/snapshot-filter.js +119 -2
  29. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/wait-timeouts.js +7 -6
  30. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +152 -64
  31. package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +244 -102
  32. package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +63 -37
  33. package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +20 -21
  34. package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +36 -18
  35. package/dist/extensions/agent-browser/lib/orchestration/output-file.js +41 -21
  36. package/dist/extensions/agent-browser/lib/orchestration/script-mode.js +299 -0
  37. package/dist/extensions/agent-browser/lib/page-target-validation.js +270 -0
  38. package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +32 -10
  39. package/dist/extensions/agent-browser/lib/playbook.js +29 -25
  40. package/dist/extensions/agent-browser/lib/process-environment.js +14 -0
  41. package/dist/extensions/agent-browser/lib/process-identity.js +5 -12
  42. package/dist/extensions/agent-browser/lib/process.js +130 -104
  43. package/dist/extensions/agent-browser/lib/recording-reservations.js +116 -0
  44. package/dist/extensions/agent-browser/lib/results/action-recommendations.js +63 -6
  45. package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +62 -4
  46. package/dist/extensions/agent-browser/lib/results/categories.js +6 -1
  47. package/dist/extensions/agent-browser/lib/results/next-actions.js +19 -5
  48. package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +85 -38
  49. package/dist/extensions/agent-browser/lib/results/presentation/batch.js +86 -18
  50. package/dist/extensions/agent-browser/lib/results/presentation/common.js +38 -2
  51. package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +18 -17
  52. package/dist/extensions/agent-browser/lib/results/presentation/errors.js +2 -1
  53. package/dist/extensions/agent-browser/lib/results/presentation/navigation.js +38 -20
  54. package/dist/extensions/agent-browser/lib/results/presentation/registry.js +60 -15
  55. package/dist/extensions/agent-browser/lib/results/presentation/semantic-action.js +1 -10
  56. package/dist/extensions/agent-browser/lib/results/presentation.js +36 -6
  57. package/dist/extensions/agent-browser/lib/results/recovery-actions.js +3 -1
  58. package/dist/extensions/agent-browser/lib/results/recovery-next-actions.js +9 -0
  59. package/dist/extensions/agent-browser/lib/results/selector-recovery.js +54 -11
  60. package/dist/extensions/agent-browser/lib/results/snapshot-high-value-controls.js +13 -7
  61. package/dist/extensions/agent-browser/lib/results/snapshot-spill.js +2 -1
  62. package/dist/extensions/agent-browser/lib/results/snapshot.js +4 -4
  63. package/dist/extensions/agent-browser/lib/runtime.js +186 -108
  64. package/dist/extensions/agent-browser/lib/session-page-state.js +71 -10
  65. package/dist/extensions/agent-browser/lib/temp.js +1 -2
  66. package/dist/extensions/agent-browser/lib/upstream-version.js +14 -0
  67. package/dist/extensions/agent-browser/lib/web-search.js +108 -24
  68. package/dist/extensions/agent-browser/script-worker.js +169 -0
  69. package/dist/scripts/agent-browser-target.mjs +21 -0
  70. package/docs/ARCHITECTURE.md +57 -34
  71. package/docs/COMMAND_REFERENCE.md +255 -68
  72. package/docs/ELECTRON.md +2 -2
  73. package/docs/RELEASE.md +12 -10
  74. package/docs/REQUIREMENTS.md +11 -8
  75. package/docs/SUPPORT_MATRIX.md +36 -24
  76. package/docs/TOOL_CONTRACT.md +169 -95
  77. package/package.json +3 -1
  78. package/platform-smoke.config.mjs +2 -2
  79. package/scripts/agent-browser-capability-baseline.mjs +87 -9
  80. package/scripts/agent-browser-target.mjs +21 -0
  81. package/scripts/build.mjs +41 -0
  82. package/scripts/config.mjs +1 -0
  83. package/scripts/doctor.mjs +16 -9
  84. package/scripts/platform-smoke/browser-dogfood-windows.ps1 +9 -3
  85. package/scripts/platform-smoke/targets.mjs +12 -6
  86. package/dist/extensions/agent-browser/lib/managed-session-capabilities.js +0 -20
  87. package/dist/extensions/agent-browser/lib/managed-session-state-policy.js +0 -583
  88. package/dist/extensions/agent-browser/lib/navigation-policy.js +0 -78
  89. package/dist/extensions/agent-browser/lib/results/presentation/managed-list-filter.js +0 -37
@@ -2,26 +2,36 @@ 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";
9
- import { cleanupManagedSessionRestoreConfig, ManagedSessionRestoreState } from "./lib/managed-session-restore.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";
12
+ import { 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 { MINIMUM_AGENT_BROWSER_VERSION, SUPPORTED_AGENT_BROWSER_VERSION_LABEL, TARGET_AGENT_BROWSER_VERSION, 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";
16
- import { parseAllowedDomainsPolicyFromArgs } from "./lib/navigation-policy.js";
22
+ import { AGENT_BROWSER_SCRIPT_DEFAULT_TIMEOUT_MS, AGENT_BROWSER_SCRIPT_NAMESPACE, createAgentBrowserScriptSessionName, isAgentBrowserScriptSessionName, runAgentBrowserScript, } from "./lib/input-modes/script.js";
17
23
  import { closeManagedSession, getSessionContextKey, runAgentBrowserTool } from "./lib/orchestration/browser-run/index.js";
24
+ import { canonicalizeExplicitArtifactDestination, getExplicitArtifactDestination } from "./lib/orchestration/browser-run/artifact-paths.js";
18
25
  import { findElectronLaunchRecordForSession, getActiveElectronRecords } from "./lib/orchestration/browser-run/session-state.js";
19
- import { parseBatchStdinJsonArray } from "./lib/orchestration/batch-stdin.js";
26
+ import { parseBatchCommandArgument, parseUserBatchStdin } from "./lib/orchestration/batch-stdin.js";
20
27
  import { ELECTRON_POST_COMMAND_STATUS_SETTLE_MS, ELECTRON_PROFILE_ISOLATION_DETAILS, cleanupActiveElectronHostLaunches, handleElectronHostInput, restoreElectronLaunchRecordsFromBranch, } from "./lib/orchestration/electron-host/index.js";
21
28
  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";
29
+ import { applyAgentBrowserOutputPath, normalizeRequestedOutputPath } from "./lib/orchestration/output-file.js";
30
+ import { appendScriptSessionLease, buildScriptBrowserEnvelope, buildScriptToolResult, getScriptSessionLeasesFromBranch } from "./lib/orchestration/script-mode.js";
31
+ import { formatSessionArtifactRetentionSummary, getSessionArtifactManifestEntryKey, isPendingRecordingCommand, isSessionArtifactManifest, mergeSessionArtifactManifest, retirePendingRecordingManifestEntries } from "./lib/results/artifact-manifest.js";
32
+ import { appendUniqueAgentBrowserNextActions, applyNamespaceToNextActions, applySessionToNextActions, buildNextToolAction } from "./lib/results/next-actions.js";
24
33
  import { canRegisterWebSearchTool, loadAgentBrowserConfigSync } from "./lib/config.js";
34
+ import { appendRecordingReservationTransition, applyRecordingArtifactsToReservations, restoreRecordingReservationStateFromBranch, retireRecordingReservation, } from "./lib/recording-reservations.js";
25
35
  import { createAgentBrowserWebSearchTool } from "./lib/web-search.js";
26
36
  import { isDirectAgentBrowserBashAllowed, isHarmlessAgentBrowserInspectionCommand, looksLikeDirectAgentBrowserBash, } from "./lib/bash-guard.js";
27
37
  import { AgentBrowserResultComponent, buildAgentBrowserToolResultPatch, formatAgentBrowserRenderCall, formatAgentBrowserRenderResult, } from "./lib/pi-tool-rendering.js";
@@ -30,22 +40,94 @@ function isBashToolCallEvent(event) {
30
40
  return false;
31
41
  return typeof event.input.command === "string";
32
42
  }
33
- function getBatchPreflightValidationError(args, stdin) {
34
- const commandTokens = extractCommandTokens(args);
35
- if (commandTokens[0] !== "batch" || stdin === undefined) {
36
- return undefined;
43
+ function getArtifactCommandSteps(args, stdin) {
44
+ const commandTokens = extractUpstreamCommandTokens(args);
45
+ const batch = commandTokens[0] === "batch";
46
+ if (!batch)
47
+ return { batch, steps: commandTokens.length > 0 ? [commandTokens] : [] };
48
+ const steps = [];
49
+ for (const command of commandTokens.slice(1)) {
50
+ if (command === "--bail")
51
+ continue;
52
+ const parsed = parseBatchCommandArgument(command);
53
+ if (parsed.error || !parsed.step)
54
+ return { batch, error: `Unsupported batch step ${steps.length + 1}: ${parsed.error ?? "command could not be parsed safely"}`, steps };
55
+ steps.push(parsed.step);
37
56
  }
38
- const parsed = parseBatchStdinJsonArray(stdin);
39
- if (parsed.error || parsed.steps === undefined) {
40
- return undefined;
57
+ // Upstream executes raw argument steps exclusively when any exist, so ignored
58
+ // stdin must not add artifact/lifecycle steps or fail this preflight.
59
+ if (steps.length > 0)
60
+ return { batch, steps };
61
+ const parsed = parseUserBatchStdin(stdin);
62
+ return parsed.error ? { batch, error: parsed.error, steps } : { batch, steps: parsed.steps ?? [] };
63
+ }
64
+ function getArtifactPreflightValidationError(options) {
65
+ const { batch, error, steps } = getArtifactCommandSteps(options.args, options.stdin);
66
+ if (error)
67
+ return error;
68
+ const activeRecordingDestinations = new Set();
69
+ const cleanupOnly = steps.length > 0 && steps.every((step) => {
70
+ const [command, subcommand] = extractUpstreamCommandTokens(step);
71
+ return isCloseCommand(command) || (command === "record" && subcommand === "stop");
72
+ });
73
+ for (const reservation of options.activeRecordingReservations ?? []) {
74
+ try {
75
+ activeRecordingDestinations.add(canonicalizeExplicitArtifactDestination(reservation.cwd, reservation.absolutePath));
76
+ }
77
+ catch (canonicalizationError) {
78
+ if (!cleanupOnly)
79
+ return canonicalizationError instanceof Error ? canonicalizationError.message : "An active recording destination could not be resolved safely.";
80
+ }
41
81
  }
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")) {
82
+ let canonicalOutputPath;
83
+ if (options.outputPath) {
84
+ try {
85
+ canonicalOutputPath = canonicalizeExplicitArtifactDestination(options.cwd, normalizeRequestedOutputPath(options.outputPath));
86
+ if (activeRecordingDestinations.has(canonicalOutputPath)) {
87
+ return `Unsupported outputPath: ${options.outputPath} is reserved by an active recording. Stop that recording first or use a distinct path.`;
88
+ }
89
+ }
90
+ catch (canonicalizationError) {
91
+ return canonicalizationError instanceof Error ? canonicalizationError.message : `outputPath ${options.outputPath} could not be resolved safely.`;
92
+ }
93
+ }
94
+ const artifactDestinations = new Map();
95
+ let sawBatchClose = false;
96
+ for (const [index, step] of steps.entries()) {
97
+ const commandStep = extractUpstreamCommandTokens(step);
98
+ if (batch) {
99
+ const stepValidationError = validateToolArgs(step, { batchStep: true });
100
+ if (stepValidationError)
101
+ return `Unsupported batch step ${index + 1}: ${stepValidationError}`;
102
+ if (sawBatchClose && commandStep[0] === "record" && (commandStep[1] === "start" || commandStep[1] === "restart")) {
103
+ 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.`;
104
+ }
105
+ if (isCloseCommand(commandStep[0]))
106
+ sawBatchClose = true;
107
+ }
108
+ const artifactDestination = getExplicitArtifactDestination(commandStep);
109
+ if (artifactDestination) {
110
+ let canonicalDestination;
111
+ try {
112
+ canonicalDestination = canonicalizeExplicitArtifactDestination(options.cwd, artifactDestination);
113
+ }
114
+ catch (canonicalizationError) {
115
+ return canonicalizationError instanceof Error ? canonicalizationError.message : `Artifact destination ${artifactDestination} could not be resolved safely.`;
116
+ }
117
+ if (canonicalOutputPath === canonicalDestination) {
118
+ 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.`;
119
+ }
120
+ if (activeRecordingDestinations.has(canonicalDestination)) {
121
+ const prefix = batch ? `Unsupported batch artifact destination in step ${index + 1}` : "Unsupported artifact destination";
122
+ return `${prefix}: ${artifactDestination} is reserved by an active recording. Stop that recording first or use a distinct path.`;
123
+ }
124
+ const priorStep = artifactDestinations.get(canonicalDestination);
125
+ if (priorStep !== undefined) {
126
+ 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.`;
127
+ }
128
+ artifactDestinations.set(canonicalDestination, index);
129
+ }
130
+ if (batch && commandStep[0] === "screenshot" && step.includes("--annotate")) {
49
131
  return [
50
132
  `Unsupported batch screenshot annotation in step ${index + 1}: put --annotate in top-level args, not inside the batch step.`,
51
133
  `Use: { "args": ["--annotate", "batch"], "stdin": "[[\\"screenshot\\",\\"/path/to/image.png\\"]]" }`,
@@ -54,6 +136,51 @@ function getBatchPreflightValidationError(args, stdin) {
54
136
  }
55
137
  return undefined;
56
138
  }
139
+ function commandClosesAllSessions(args, stdin) {
140
+ const parsed = getArtifactCommandSteps(args, stdin);
141
+ return !parsed.error && parsed.steps.some((step) => isCloseAllCommand(extractUpstreamCommandTokens(step)));
142
+ }
143
+ function commandTouchesArtifactLifecycle(args, stdin, outputPath) {
144
+ if (outputPath)
145
+ return true;
146
+ const parsed = getArtifactCommandSteps(args, stdin);
147
+ if (parsed.error)
148
+ return true;
149
+ return parsed.steps.some((step) => {
150
+ const commandStep = extractUpstreamCommandTokens(step);
151
+ return getExplicitArtifactDestination(commandStep) !== undefined || commandStep[0] === "record" || commandStep[0] === "screenshot" || isCloseCommand(commandStep[0]);
152
+ });
153
+ }
154
+ function isResultFileArtifact(artifact) {
155
+ return isRecord(artifact)
156
+ && typeof artifact.absolutePath === "string"
157
+ && typeof artifact.kind === "string"
158
+ && typeof artifact.path === "string";
159
+ }
160
+ function getResultFileArtifacts(result) {
161
+ const details = isRecord(result.details) ? result.details : undefined;
162
+ return Array.isArray(details?.artifacts) ? details.artifacts.filter(isResultFileArtifact) : [];
163
+ }
164
+ function reportsNoRecordingInProgress(value) {
165
+ try {
166
+ return /no recording in progress/i.test(JSON.stringify(value));
167
+ }
168
+ catch {
169
+ return false;
170
+ }
171
+ }
172
+ function batchStepReportsNoRecordingInProgress(step) {
173
+ if (!isRecord(step) || step.success !== false)
174
+ return false;
175
+ const command = Array.isArray(step.command) && step.command.every((token) => typeof token === "string") ? extractUpstreamCommandTokens(step.command) : [];
176
+ return command[0] === "record" && command[1] === "stop" && reportsNoRecordingInProgress(step);
177
+ }
178
+ function resultReportsNoRecordingInProgress(result) {
179
+ if (result.isError !== true)
180
+ return false;
181
+ const details = isRecord(result.details) ? result.details : undefined;
182
+ return details?.command === "record" && details.subcommand === "stop" && reportsNoRecordingInProgress(result.content);
183
+ }
57
184
  function restoreArtifactManifestFromBranch(branch) {
58
185
  let restoredManifest;
59
186
  for (const entry of branch) {
@@ -69,6 +196,12 @@ function restoreArtifactManifestFromBranch(branch) {
69
196
  }
70
197
  return restoredManifest;
71
198
  }
199
+ function getRecognizedCompatibilityWorkaround(value) {
200
+ const workaround = isRecord(value) ? value : undefined;
201
+ return (workaround?.id === "chatgpt-headless-user-agent" || workaround?.id === "cloudflare-headless-user-agent") && typeof workaround.reason === "string"
202
+ ? { id: workaround.id, reason: workaround.reason }
203
+ : undefined;
204
+ }
72
205
  function restoreManagedSessionCompatibilityWorkaroundFromBranch(branch, sessionName, namespace) {
73
206
  let restored;
74
207
  const targetKey = getSessionContextKey(sessionName, namespace);
@@ -81,12 +214,9 @@ function restoreManagedSessionCompatibilityWorkaroundFromBranch(branch, sessionN
81
214
  const details = isRecord(message.details) ? message.details : undefined;
82
215
  if (!details)
83
216
  continue;
84
- const workaround = isRecord(details.compatibilityWorkaround) ? details.compatibilityWorkaround : undefined;
85
217
  if (getSessionContextKey(typeof details.sessionName === "string" ? details.sessionName : undefined, typeof details.namespace === "string" ? details.namespace : undefined) !== targetKey)
86
218
  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;
219
+ const recognizedWorkaround = getRecognizedCompatibilityWorkaround(details.compatibilityWorkaround);
90
220
  const succeeded = getSuccessfulToolResult(details, message);
91
221
  const outcome = getManagedSessionOutcome(details);
92
222
  const activeAfterFailure = recognizedWorkaround
@@ -163,9 +293,21 @@ function getToolResultArgs(details) {
163
293
  return details.effectiveArgs;
164
294
  return [];
165
295
  }
166
- function isAttachedBrowserInvocation(args, env = process.env) {
296
+ function detailsReportCloseAllApplied(details, succeeded) {
297
+ const args = getToolResultArgs(details);
298
+ return details.closeAllApplied === true
299
+ || (succeeded && isCloseAllCommand(extractUpstreamCommandTokens(args)))
300
+ || batchHasSuccessfulCloseAll(details.batchSteps);
301
+ }
302
+ function deleteIdentityKeysInNamespace(entries, namespace) {
303
+ for (const key of entries.keys()) {
304
+ if (isAgentBrowserSessionIdentityKeyInNamespace(key, namespace))
305
+ entries.delete(key);
306
+ }
307
+ }
308
+ function isAttachedBrowserInvocation(args, env = getAgentBrowserProcessEnvironment()) {
167
309
  const autoConnectEnv = env.AGENT_BROWSER_AUTO_CONNECT;
168
- return extractCommandTokens(args)[0] === "connect"
310
+ return extractUpstreamCommandTokens(args)[0] === "connect"
169
311
  || hasLaunchScopedFlagToken(args, "--cdp")
170
312
  || hasLaunchScopedFlagToken(args, "--auto-connect")
171
313
  || env.AGENT_BROWSER_CDP !== undefined
@@ -184,85 +326,50 @@ function restoreAttachedSessionKeysFromBranch(branch) {
184
326
  continue;
185
327
  const managedSessionOutcome = isRecord(details.managedSessionOutcome) ? details.managedSessionOutcome : undefined;
186
328
  const retainedFailedAttachment = details.attachedBrowserSession === true && managedSessionOutcome?.activeAfter === true;
187
- if (!getSuccessfulToolResult(details, message) && !retainedFailedAttachment)
188
- continue;
329
+ const succeeded = getSuccessfulToolResult(details, message);
330
+ const batchCloseLifecycle = getSuccessfulBatchCloseLifecycle(details.batchSteps);
331
+ const terminalBatchClose = batchCloseLifecycle?.endsClosed === true;
189
332
  const args = getToolResultArgs(details);
333
+ const namespace = typeof details.namespace === "string" ? details.namespace : extractExplicitNamespace(args);
190
334
  const sessionName = typeof details.sessionName === "string" ? details.sessionName : extractExplicitSessionName(args);
335
+ const electron = isRecord(details.electron) ? details.electron : undefined;
336
+ const cleanup = isRecord(electron?.cleanup) ? electron.cleanup : undefined;
337
+ for (const cleanupResult of Array.isArray(cleanup?.results) ? cleanup.results : []) {
338
+ for (const identity of getCleanupResultClosedManagedSessionIdentities(cleanupResult, namespace)) {
339
+ attachedSessionKeys.delete(getSessionContextKey(identity.sessionName, identity.namespace) ?? identity.sessionName);
340
+ }
341
+ }
342
+ if (detailsReportCloseAllApplied(details, succeeded)) {
343
+ deleteIdentityKeysInNamespace(attachedSessionKeys, namespace);
344
+ if (sessionName && details.attachedBrowserSession === true && batchCloseLifecycle?.endsClosed === false) {
345
+ attachedSessionKeys.add(getSessionContextKey(sessionName, namespace) ?? sessionName);
346
+ }
347
+ continue;
348
+ }
349
+ if (!succeeded && !retainedFailedAttachment && !terminalBatchClose)
350
+ continue;
191
351
  if (!sessionName)
192
352
  continue;
193
- const namespace = typeof details.namespace === "string" ? details.namespace : extractExplicitNamespace(args);
194
353
  const sessionKey = getSessionContextKey(sessionName, namespace) ?? sessionName;
195
- if (isCloseCommand(extractCommandTokens(args)[0]))
354
+ if ((succeeded && isCloseCommand(extractUpstreamCommandTokens(args)[0])) || terminalBatchClose)
196
355
  attachedSessionKeys.delete(sessionKey);
197
356
  else if (details.attachedBrowserSession === true || isAttachedBrowserInvocation(args, {}))
198
357
  attachedSessionKeys.add(sessionKey);
199
358
  }
200
359
  return attachedSessionKeys;
201
360
  }
202
- function restoreAllowedDomainsBySessionFromBranch(branch) {
203
- const restoredPolicies = new Map();
204
- for (const entry of branch) {
205
- if (!isRecord(entry) || entry.type !== "message")
206
- continue;
207
- const message = isRecord(entry.message) ? entry.message : undefined;
208
- if (!message || message.toolName !== "agent_browser")
209
- continue;
210
- const details = isRecord(message.details) ? message.details : undefined;
211
- if (!details)
212
- continue;
213
- const succeeded = getSuccessfulToolResult(details, message);
214
- const args = getToolResultArgs(details);
215
- const command = typeof details.command === "string" ? details.command : extractCommandTokens(args)[0];
216
- const sessionName = typeof details.sessionName === "string" ? details.sessionName : undefined;
217
- const namespace = typeof details.namespace === "string" ? details.namespace : undefined;
218
- const sessionKey = getSessionContextKey(sessionName, namespace);
219
- const explicitSessionName = extractExplicitSessionName(args);
220
- const outcome = getManagedSessionOutcome(details);
221
- const outcomeSucceeded = outcome?.succeeded === true;
222
- const outcomeStatus = typeof outcome?.status === "string" ? outcome.status : undefined;
223
- const outcomeCurrentSessionName = typeof outcome?.currentSessionName === "string" ? outcome.currentSessionName : undefined;
224
- const outcomeAttemptedSessionName = typeof outcome?.attemptedSessionName === "string" ? outcome.attemptedSessionName : undefined;
225
- if (outcomeSucceeded && outcomeStatus === "closed") {
226
- const closedSessionName = outcomeAttemptedSessionName ?? outcomeCurrentSessionName ?? sessionName;
227
- if (closedSessionName)
228
- restoredPolicies.delete(getSessionContextKey(closedSessionName, namespace) ?? closedSessionName);
229
- }
230
- if (outcomeSucceeded && outcomeStatus === "replaced") {
231
- const replacedSessionName = typeof outcome.replacedSessionName === "string" ? outcome.replacedSessionName : undefined;
232
- const replacedSessionNamespace = typeof outcome.replacedSessionNamespace === "string" ? outcome.replacedSessionNamespace : namespace;
233
- if (replacedSessionName)
234
- restoredPolicies.delete(getSessionContextKey(replacedSessionName, replacedSessionNamespace) ?? replacedSessionName);
235
- }
236
- if (succeeded && isCloseCommand(command)) {
237
- const closedSessionName = explicitSessionName ?? sessionName ?? outcomeAttemptedSessionName ?? outcomeCurrentSessionName;
238
- if (closedSessionName)
239
- restoredPolicies.delete(getSessionContextKey(closedSessionName, namespace) ?? closedSessionName);
240
- }
241
- const electron = isRecord(details.electron) ? details.electron : undefined;
242
- const cleanup = isRecord(electron?.cleanup) ? electron.cleanup : undefined;
243
- const cleanupResults = Array.isArray(cleanup?.results) ? cleanup.results : [];
244
- for (const cleanupResult of cleanupResults) {
245
- for (const closedSessionName of getCleanupResultClosedManagedSessionNames(cleanupResult))
246
- restoredPolicies.delete(closedSessionName);
247
- }
248
- const outcomeKeepsSessionCurrent = outcome?.activeAfter === true
249
- && (outcomeStatus === "created" || outcomeStatus === "replaced" || outcomeStatus === "unchanged")
250
- && outcomeCurrentSessionName === sessionName;
251
- const policy = (succeeded || outcomeKeepsSessionCurrent) && sessionKey && !isCloseCommand(command) ? parseAllowedDomainsPolicyFromArgs(args) : undefined;
252
- if (policy && sessionKey)
253
- restoredPolicies.set(sessionKey, policy);
254
- }
255
- return restoredPolicies;
256
- }
257
361
  function trackOwnedManagedSession(sessions, sessionName, cwd, options = {}) {
258
362
  if (!sessionName)
259
363
  return;
260
364
  const key = getSessionContextKey(sessionName, options.namespace) ?? sessionName;
261
365
  const existing = sessions.get(key);
262
366
  const branchOwned = existing && !existing.branchOwned ? false : options.branchOwned === true;
367
+ const compatibilityWorkaround = Object.hasOwn(options, "compatibilityWorkaround")
368
+ ? options.compatibilityWorkaround
369
+ : existing?.compatibilityWorkaround;
263
370
  const headedManagedAutosaveDisabled = options.headedManagedAutosaveDisabled ?? existing?.headedManagedAutosaveDisabled;
264
371
  const headedManagedAutosaveInterval = options.headedManagedAutosaveInterval ?? existing?.headedManagedAutosaveInterval;
265
- sessions.set(key, { branchOwned, cwd, headedManagedAutosaveDisabled, headedManagedAutosaveInterval, namespace: options.namespace, sessionName });
372
+ sessions.set(key, { branchOwned, compatibilityWorkaround, cwd, headedManagedAutosaveDisabled, headedManagedAutosaveInterval, namespace: options.namespace, sessionName });
266
373
  }
267
374
  function untrackOwnedManagedSession(sessions, sessionName, namespace) {
268
375
  if (!sessionName)
@@ -291,20 +398,21 @@ function syncOwnedManagedSessionsFromResult(sessions, result, cwd) {
291
398
  const status = typeof outcome.status === "string" ? outcome.status : undefined;
292
399
  const currentSessionName = typeof outcome.currentSessionName === "string" ? outcome.currentSessionName : undefined;
293
400
  const attemptedSessionName = typeof outcome.attemptedSessionName === "string" ? outcome.attemptedSessionName : undefined;
401
+ const namespace = isRecord(details) && typeof details.namespace === "string" ? details.namespace : undefined;
294
402
  if (outcome.activeAfter === true && (status === "created" || status === "replaced" || status === "unchanged")) {
295
- const namespace = isRecord(details) && typeof details.namespace === "string" ? details.namespace : undefined;
296
403
  trackOwnedManagedSession(sessions, currentSessionName, cwd, {
404
+ compatibilityWorkaround: getRecognizedCompatibilityWorkaround(details?.compatibilityWorkaround),
297
405
  headedManagedAutosaveDisabled: details?.managedSessionHeadedAutosaveDisabled === true,
298
406
  headedManagedAutosaveInterval: typeof details?.managedSessionHeadedAutosaveInterval === "string" ? details.managedSessionHeadedAutosaveInterval : undefined,
299
407
  namespace,
300
408
  });
301
409
  }
302
410
  if (succeeded && status === "closed") {
303
- untrackOwnedManagedSession(sessions, attemptedSessionName ?? currentSessionName);
411
+ untrackOwnedManagedSession(sessions, attemptedSessionName ?? currentSessionName, namespace);
304
412
  }
305
413
  }
306
- function getTouchedElectronLaunchIds(sessionName, records) {
307
- const record = findElectronLaunchRecordForSession(sessionName, records);
414
+ function getTouchedElectronLaunchIds(sessionName, records, namespace) {
415
+ const record = findElectronLaunchRecordForSession(sessionName, records, namespace);
308
416
  return record ? new Set([record.launchId]) : undefined;
309
417
  }
310
418
  function mergeActiveElectronLaunchRecords(target, source, options = {}) {
@@ -372,10 +480,10 @@ function getElectronHostLaunchRecordsForInput(options) {
372
480
  }
373
481
  return options.branchRecords;
374
482
  }
375
- function getCleanupResultClosedManagedSessionNames(result) {
483
+ function getCleanupResultClosedManagedSessionIdentities(result, fallbackNamespace) {
376
484
  if (!isRecord(result) || !Array.isArray(result.steps))
377
485
  return [];
378
- const closedSessionNames = new Set();
486
+ const identities = new Map();
379
487
  const record = isRecord(result.record) ? result.record : undefined;
380
488
  for (const step of result.steps) {
381
489
  if (!isRecord(step) || step.resource !== "managed-session")
@@ -385,24 +493,29 @@ function getCleanupResultClosedManagedSessionNames(result) {
385
493
  const sessionName = typeof step.sessionName === "string"
386
494
  ? step.sessionName
387
495
  : typeof record?.sessionName === "string" ? record.sessionName : undefined;
496
+ const namespace = typeof step.namespace === "string"
497
+ ? step.namespace
498
+ : typeof record?.namespace === "string" ? record.namespace : fallbackNamespace;
388
499
  if (sessionName)
389
- closedSessionNames.add(sessionName);
500
+ identities.set(getSessionContextKey(sessionName, namespace) ?? sessionName, { namespace, sessionName });
390
501
  }
391
- return [...closedSessionNames];
502
+ return [...identities.values()];
392
503
  }
393
- function getCleanupResultsClosedManagedSessionNames(cleanupResults) {
394
- const closedSessionNames = new Set();
504
+ function getCleanupResultsClosedManagedSessionIdentities(cleanupResults, fallbackNamespace) {
505
+ const identities = new Map();
395
506
  for (const result of cleanupResults) {
396
- for (const sessionName of getCleanupResultClosedManagedSessionNames(result))
397
- closedSessionNames.add(sessionName);
507
+ for (const identity of getCleanupResultClosedManagedSessionIdentities(result, fallbackNamespace)) {
508
+ identities.set(getSessionContextKey(identity.sessionName, identity.namespace) ?? identity.sessionName, identity);
509
+ }
398
510
  }
399
- return [...closedSessionNames];
511
+ return [...identities.values()];
400
512
  }
401
513
  function isElectronLaunchRecord(value) {
402
514
  if (!isRecord(value))
403
515
  return false;
404
516
  return value.version === 1
405
517
  && value.launchedByWrapper === true
518
+ && (value.namespace === undefined || typeof value.namespace === "string")
406
519
  && typeof value.launchId === "string"
407
520
  && typeof value.appName === "string"
408
521
  && typeof value.executablePath === "string"
@@ -460,18 +573,20 @@ function collectBranchManagedResourceEvents(branch) {
460
573
  eventRank += 1;
461
574
  const succeeded = getSuccessfulToolResult(details, message);
462
575
  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];
576
+ const command = typeof details.command === "string" ? details.command : extractUpstreamCommandTokens(args)[0];
464
577
  const sessionName = typeof details.sessionName === "string" ? details.sessionName : undefined;
465
578
  const namespace = typeof details.namespace === "string" ? details.namespace : undefined;
466
579
  const sessionMode = details.sessionMode === "fresh" || details.sessionMode === "auto" ? details.sessionMode : undefined;
467
580
  const usedImplicitSession = details.usedImplicitSession === true;
468
581
  const explicitSessionName = extractExplicitSessionName(args);
582
+ const batchCloseLifecycle = getSuccessfulBatchCloseLifecycle(details.batchSteps);
583
+ const closeAllApplied = detailsReportCloseAllApplied(details, succeeded);
469
584
  const outcome = getManagedSessionOutcome(details);
470
585
  const outcomeSucceeded = outcome?.succeeded === true;
471
586
  const outcomeStatus = typeof outcome?.status === "string" ? outcome.status : undefined;
472
587
  const outcomeCurrentSessionName = typeof outcome?.currentSessionName === "string" ? outcome.currentSessionName : undefined;
473
588
  const outcomeAttemptedSessionName = typeof outcome?.attemptedSessionName === "string" ? outcome.attemptedSessionName : undefined;
474
- if (outcomeSucceeded && outcome.activeAfter === true && (outcomeStatus === "created" || outcomeStatus === "replaced" || outcomeStatus === "unchanged")) {
589
+ if (outcome?.activeAfter === true && (outcomeStatus === "created" || outcomeStatus === "replaced" || outcomeStatus === "unchanged")) {
475
590
  setBranchManagedSessionActive(events, outcomeCurrentSessionName, namespace, eventRank);
476
591
  }
477
592
  if (outcomeSucceeded && outcomeStatus === "closed") {
@@ -487,6 +602,14 @@ function collectBranchManagedResourceEvents(branch) {
487
602
  if (succeeded && isCloseCommand(command)) {
488
603
  setBranchRankForString(events.managedSessionCloseRanks, getSessionContextKey(explicitSessionName ?? sessionName ?? outcomeAttemptedSessionName ?? outcomeCurrentSessionName, namespace), eventRank);
489
604
  }
605
+ if (closeAllApplied) {
606
+ const retainedSessionKey = batchCloseLifecycle?.endsClosed === false ? getSessionContextKey(sessionName, namespace) : undefined;
607
+ for (const sessionKey of events.managedSessionActiveIdentities.keys()) {
608
+ if (sessionKey !== retainedSessionKey && isAgentBrowserSessionIdentityKeyInNamespace(sessionKey, namespace)) {
609
+ events.managedSessionCloseRanks.set(sessionKey, eventRank);
610
+ }
611
+ }
612
+ }
490
613
  const electron = isRecord(details.electron) ? details.electron : undefined;
491
614
  const launch = electron && isElectronLaunchRecord(electron.launch) ? electron.launch : undefined;
492
615
  if (launch && getActiveElectronRecords(new Map([[launch.launchId, launch]])).length > 0) {
@@ -503,8 +626,8 @@ function collectBranchManagedResourceEvents(branch) {
503
626
  if (isRecord(cleanupResult) && isElectronLaunchRecord(cleanupResult.record)) {
504
627
  events.electronLaunchCleanupRanks.set(cleanupResult.record.launchId, eventRank);
505
628
  }
506
- for (const closedSessionName of getCleanupResultClosedManagedSessionNames(cleanupResult)) {
507
- events.managedSessionCloseRanks.set(closedSessionName, eventRank);
629
+ for (const identity of getCleanupResultClosedManagedSessionIdentities(cleanupResult, namespace)) {
630
+ events.managedSessionCloseRanks.set(getSessionContextKey(identity.sessionName, identity.namespace) ?? identity.sessionName, eventRank);
508
631
  }
509
632
  }
510
633
  }
@@ -523,23 +646,25 @@ function getCleanupResultsPreservedUserDataDirs(cleanupResults) {
523
646
  }
524
647
  return [...userDataDirs];
525
648
  }
526
- function syncElectronCleanupManagedSessions(sessions, cleanupResults) {
527
- for (const sessionName of getCleanupResultsClosedManagedSessionNames(cleanupResults)) {
528
- untrackOwnedManagedSession(sessions, sessionName);
649
+ function syncElectronCleanupManagedSessions(sessions, cleanupResults, fallbackNamespace) {
650
+ for (const identity of getCleanupResultsClosedManagedSessionIdentities(cleanupResults, fallbackNamespace)) {
651
+ untrackOwnedManagedSession(sessions, identity.sessionName, identity.namespace);
529
652
  }
530
653
  }
531
- async function closeOwnedManagedSessionsExcept(sessions, restoreState, keepSessionName, timeoutMs, attachedSessionKeys, keepNamespace) {
654
+ async function closeOwnedManagedSessionsExcept(sessions, restoreState, keepSessionName, timeoutMs, attachedSessionKeys, keepNamespace, onClosed) {
532
655
  const keepKey = getSessionContextKey(keepSessionName, keepNamespace);
533
656
  for (const [key, owner] of [...sessions]) {
534
657
  if (key === keepKey)
535
658
  continue;
536
659
  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)
660
+ if (!error) {
538
661
  sessions.delete(key);
662
+ onClosed?.(owner);
663
+ }
539
664
  }
540
665
  }
541
- async function closeOwnedManagedSessions(sessions, restoreState, timeoutMs, attachedSessionKeys) {
542
- await closeOwnedManagedSessionsExcept(sessions, restoreState, undefined, timeoutMs, attachedSessionKeys);
666
+ async function closeOwnedManagedSessions(sessions, restoreState, timeoutMs, attachedSessionKeys, onClosed) {
667
+ await closeOwnedManagedSessionsExcept(sessions, restoreState, undefined, timeoutMs, attachedSessionKeys, undefined, onClosed);
543
668
  }
544
669
  function getOffBranchOwnedElectronLaunchRecords(ownedRecords, branchRecords) {
545
670
  const activeBranchLaunchIds = new Set(getActiveElectronRecords(branchRecords).map((record) => record.launchId));
@@ -579,14 +704,19 @@ class AsyncExecutionQueue {
579
704
  })();
580
705
  }
581
706
  }
582
- class KeyedAsyncExecutionQueue {
707
+ export class KeyedAsyncExecutionQueue {
708
+ barriers = new Map();
583
709
  entries = new Map();
584
- async run(key, work) {
710
+ async run(key, namespace, work) {
585
711
  const entry = this.entries.get(key) ?? { queue: new AsyncExecutionQueue(), users: 0 };
712
+ const barrier = this.barriers.get(getAgentBrowserSessionIdentityKey("", namespace)) ?? Promise.resolve();
586
713
  entry.users += 1;
587
714
  this.entries.set(key, entry);
588
715
  try {
589
- return await entry.queue.run(work);
716
+ return await entry.queue.run(async () => {
717
+ await barrier;
718
+ return await work();
719
+ });
590
720
  }
591
721
  finally {
592
722
  entry.users -= 1;
@@ -594,6 +724,29 @@ class KeyedAsyncExecutionQueue {
594
724
  this.entries.delete(key);
595
725
  }
596
726
  }
727
+ async runExclusive(namespace, work) {
728
+ const namespaceKey = getAgentBrowserSessionIdentityKey("", namespace);
729
+ const previous = this.barriers.get(namespaceKey) ?? Promise.resolve();
730
+ let release;
731
+ const blocked = new Promise((resolve) => {
732
+ release = resolve;
733
+ });
734
+ const barrier = previous.then(() => blocked);
735
+ this.barriers.set(namespaceKey, barrier);
736
+ const drains = [...this.entries]
737
+ .filter(([key]) => isAgentBrowserSessionIdentityKeyInNamespace(key, namespace))
738
+ .map(([, { queue }]) => queue.run(async () => undefined));
739
+ await previous;
740
+ await Promise.all(drains);
741
+ try {
742
+ return await work();
743
+ }
744
+ finally {
745
+ release();
746
+ if (this.barriers.get(namespaceKey) === barrier)
747
+ this.barriers.delete(namespaceKey);
748
+ }
749
+ }
597
750
  }
598
751
  function mergeBrowserRunMap(current, initial, updated) {
599
752
  if (updated === initial)
@@ -609,11 +762,19 @@ function mergeBrowserRunMap(current, initial, updated) {
609
762
  }
610
763
  return merged;
611
764
  }
612
- function mergeBrowserRunArtifactManifest(current, initial, updated) {
765
+ export function mergeBrowserRunArtifactManifest(current, initial, updated) {
613
766
  if (!updated || updated === initial)
614
767
  return current;
768
+ if (current === initial)
769
+ return updated;
615
770
  const initialEntries = new Map((initial?.entries ?? []).map((entry) => [getSessionArtifactManifestEntryKey(entry), entry]));
616
- const changedEntries = updated.entries.filter((entry) => initialEntries.get(getSessionArtifactManifestEntryKey(entry)) !== entry);
771
+ const changedEntries = updated.entries
772
+ .map((entry, index) => ({ entry, index }))
773
+ .filter(({ entry }) => initialEntries.get(getSessionArtifactManifestEntryKey(entry)) !== entry)
774
+ .sort((left, right) => left.entry.createdAtMs - right.entry.createdAtMs
775
+ || Number(isPendingRecordingCommand(left.entry.command, left.entry.subcommand, left.entry.kind)) - Number(isPendingRecordingCommand(right.entry.command, right.entry.subcommand, right.entry.kind))
776
+ || left.index - right.index)
777
+ .map(({ entry }) => entry);
617
778
  return changedEntries.length === 0
618
779
  ? current
619
780
  : mergeSessionArtifactManifest({
@@ -681,7 +842,9 @@ export default function agentBrowserExtension(pi) {
681
842
  let sessionPageState = new SessionPageState();
682
843
  let traceOwners = new Map();
683
844
  let artifactManifest;
684
- let allowedDomainsBySession = new Map();
845
+ let activeRecordingReservations = new Map();
846
+ let recordingSessionTombstones = new Map();
847
+ let recordingSessionTombstonesToPersist = new Map();
685
848
  let attachedSessionKeys = new Set();
686
849
  let networkRoutesBySession = new Map();
687
850
  let electronLaunchRecords = new Map();
@@ -691,18 +854,211 @@ export default function agentBrowserExtension(pi) {
691
854
  const managedSessionRestoreState = new ManagedSessionRestoreState();
692
855
  const ownedManagedSessions = new Map();
693
856
  const managedSessionExecutionQueue = new AsyncExecutionQueue();
857
+ const artifactExecutionQueue = new AsyncExecutionQueue();
694
858
  const callerOwnedSessionExecutionQueues = new KeyedAsyncExecutionQueue();
859
+ const activeScriptControllers = new Set();
860
+ const activeScriptExecutions = new Set();
695
861
  let branchRestoreGeneration = 0;
696
862
  let branchStateGeneration = 0;
863
+ const validatedUpstreamPathKeys = new Set();
864
+ const appendRecordingTransitions = (transitions) => {
865
+ for (const transition of transitions) {
866
+ const key = getAgentBrowserSessionIdentityKey(transition.reservation.sessionName, transition.reservation.namespace);
867
+ if (transition.state === "active") {
868
+ recordingSessionTombstones.delete(key);
869
+ recordingSessionTombstonesToPersist.delete(key);
870
+ }
871
+ else {
872
+ recordingSessionTombstones.set(key, transition.reservation);
873
+ }
874
+ try {
875
+ appendRecordingReservationTransition(pi, transition);
876
+ recordingSessionTombstonesToPersist.delete(key);
877
+ }
878
+ catch {
879
+ if (transition.state === "closed")
880
+ recordingSessionTombstonesToPersist.set(key, transition.reservation);
881
+ }
882
+ }
883
+ };
884
+ const appendActiveRecordingCleanupAction = (result, reservation) => {
885
+ if (result.isError !== true)
886
+ return result;
887
+ const details = isRecord(result.details) ? result.details : {};
888
+ const nextActions = Array.isArray(details.nextActions) ? [...details.nextActions] : [];
889
+ if (nextActions.some((action) => action.id === "stop-pending-recording"))
890
+ return result;
891
+ const stopActions = applyNamespaceToNextActions(applySessionToNextActions([
892
+ buildNextToolAction({
893
+ args: ["record", "stop"],
894
+ id: "stop-pending-recording",
895
+ reason: "Stop the active recording so the requested video can be finalized and verified on disk.",
896
+ safety: "The file remains pending until record stop succeeds; verify details.artifactVerification afterward.",
897
+ }),
898
+ ], reservation.sessionName), reservation.namespace);
899
+ appendUniqueAgentBrowserNextActions(nextActions, stopActions);
900
+ const cleanupNotice = "An active recording remains open. Use the exact stop-pending-recording payload in details.nextActions before leaving this session.";
901
+ let noticeAppended = false;
902
+ const content = result.content.map((item) => {
903
+ if (noticeAppended || item.type !== "text")
904
+ return item;
905
+ noticeAppended = true;
906
+ return { ...item, text: `${item.text}\n\n${cleanupNotice}` };
907
+ });
908
+ if (!noticeAppended)
909
+ content.push({ type: "text", text: cleanupNotice });
910
+ return { ...result, content, details: { ...details, nextActions } };
911
+ };
912
+ const retireRecordingSession = (sessionName, namespace, retireManifest = true) => {
913
+ const reservation = retireRecordingReservation(activeRecordingReservations, sessionName, namespace);
914
+ const previousManifest = artifactManifest;
915
+ if (retireManifest && artifactManifest)
916
+ artifactManifest = retirePendingRecordingManifestEntries(artifactManifest, sessionName, namespace);
917
+ if (!reservation && artifactManifest === previousManifest)
918
+ return;
919
+ const terminalReservation = reservation ?? { absolutePath: "", cwd: managedSessionCwd, namespace, path: "", sessionName };
920
+ const terminalKey = getAgentBrowserSessionIdentityKey(sessionName, namespace);
921
+ recordingSessionTombstones.set(terminalKey, terminalReservation);
922
+ try {
923
+ appendRecordingReservationTransition(pi, {
924
+ reservation: terminalReservation,
925
+ state: "closed",
926
+ });
927
+ recordingSessionTombstonesToPersist.delete(terminalKey);
928
+ }
929
+ catch {
930
+ recordingSessionTombstonesToPersist.set(terminalKey, terminalReservation);
931
+ }
932
+ };
933
+ const syncRecordingReservationsFromResult = (result) => {
934
+ const handledClosedSessionKeys = new Set();
935
+ const details = isRecord(result.details) ? result.details : undefined;
936
+ const batchSteps = Array.isArray(details?.batchSteps) ? details.batchSteps : undefined;
937
+ const resultSessionName = typeof details?.sessionName === "string" ? details.sessionName : undefined;
938
+ const resultNamespace = typeof details?.namespace === "string" ? details.namespace : undefined;
939
+ if (!batchSteps) {
940
+ appendRecordingTransitions(applyRecordingArtifactsToReservations(activeRecordingReservations, getResultFileArtifacts(result)));
941
+ return handledClosedSessionKeys;
942
+ }
943
+ let sessionClosed = false;
944
+ for (const step of batchSteps) {
945
+ if (!isRecord(step))
946
+ continue;
947
+ const command = Array.isArray(step.command) && step.command.every((token) => typeof token === "string") ? step.command : undefined;
948
+ const commandTokens = command ? extractUpstreamCommandTokens(command) : [];
949
+ const commandName = commandTokens[0];
950
+ if (resultSessionName && batchStepReportsNoRecordingInProgress(step)) {
951
+ const sessionKey = getAgentBrowserSessionIdentityKey(resultSessionName, resultNamespace);
952
+ retireRecordingSession(resultSessionName, resultNamespace, false);
953
+ handledClosedSessionKeys.add(sessionKey);
954
+ continue;
955
+ }
956
+ if (step.success === true && commandName && isCloseCommand(commandName) && resultSessionName) {
957
+ const sessionKey = getAgentBrowserSessionIdentityKey(resultSessionName, resultNamespace);
958
+ retireRecordingSession(resultSessionName, resultNamespace, false);
959
+ handledClosedSessionKeys.add(sessionKey);
960
+ sessionClosed = true;
961
+ continue;
962
+ }
963
+ if (sessionClosed && commandName === "record")
964
+ continue;
965
+ if (step.success === true && sessionClosed)
966
+ sessionClosed = false;
967
+ const artifacts = Array.isArray(step.artifacts) ? step.artifacts.filter(isResultFileArtifact) : [];
968
+ appendRecordingTransitions(applyRecordingArtifactsToReservations(activeRecordingReservations, artifacts));
969
+ }
970
+ for (const sessionKey of handledClosedSessionKeys) {
971
+ if (!activeRecordingReservations.has(sessionKey) && artifactManifest && resultSessionName) {
972
+ artifactManifest = retirePendingRecordingManifestEntries(artifactManifest, resultSessionName, resultNamespace);
973
+ }
974
+ }
975
+ return handledClosedSessionKeys;
976
+ };
977
+ const validateUpstreamVersion = async (cwd, signal) => {
978
+ const processEnvironment = getAgentBrowserProcessEnvironment();
979
+ const pathKey = `${cwd}\0${processEnvironment.PATH ?? processEnvironment.Path ?? ""}`;
980
+ if (validatedUpstreamPathKeys.has(pathKey))
981
+ return undefined;
982
+ const probe = await runAgentBrowserProcess({ args: ["--version"], cwd, signal, timeoutMs: 5_000 });
983
+ if (probe.spawnError?.code === "ENOENT" || probe.exitCode === 127 || probe.aborted)
984
+ return undefined;
985
+ let error;
986
+ let observedVersion;
987
+ if (probe.spawnError || probe.exitCode !== 0) {
988
+ const detail = redactSensitiveText(probe.spawnError?.message ?? (probe.stderr.trim() || `exit ${probe.exitCode}`));
989
+ error = `agent-browser --version could not be validated (${detail}). Run pi-agent-browser-doctor before browser-backed calls.`;
990
+ }
991
+ else {
992
+ observedVersion = parseAgentBrowserVersionOutput(probe.stdout);
993
+ error = getAgentBrowserVersionValidationError(probe.stdout);
994
+ }
995
+ if (!error) {
996
+ validatedUpstreamPathKeys.add(pathKey);
997
+ return undefined;
998
+ }
999
+ return {
1000
+ content: [{ type: "text", text: error }],
1001
+ details: {
1002
+ expectedVersion: TARGET_AGENT_BROWSER_VERSION,
1003
+ failureCategory: "validation-error",
1004
+ observedVersion,
1005
+ resultCategory: "failure",
1006
+ minimumSupportedVersion: MINIMUM_AGENT_BROWSER_VERSION,
1007
+ versionValidation: { expected: SUPPORTED_AGENT_BROWSER_VERSION_LABEL, observed: observedVersion },
1008
+ },
1009
+ isError: true,
1010
+ };
1011
+ };
697
1012
  const clearSessionScopedBrowserState = (sessionName, namespace) => {
698
1013
  const key = getSessionContextKey(sessionName, namespace) ?? sessionName;
699
- allowedDomainsBySession = new Map(allowedDomainsBySession);
700
- allowedDomainsBySession.delete(key);
701
1014
  attachedSessionKeys.delete(key);
702
1015
  networkRoutesBySession = new Map(networkRoutesBySession);
703
1016
  networkRoutesBySession.delete(key);
1017
+ traceOwners.delete(key);
704
1018
  sessionPageState.clearSession(key);
705
1019
  };
1020
+ const closeScriptSessionLeaseWithinQueue = async (sessionName, cwd) => {
1021
+ const closeError = await withIsolatedAgentBrowserEnvironment(() => closeManagedSession({
1022
+ cwd,
1023
+ namespace: AGENT_BROWSER_SCRIPT_NAMESPACE,
1024
+ restoreState: managedSessionRestoreState,
1025
+ sessionName,
1026
+ timeoutMs: implicitSessionCloseTimeoutMs,
1027
+ }));
1028
+ if (closeError) {
1029
+ try {
1030
+ appendScriptSessionLease(pi, sessionName, "failed");
1031
+ }
1032
+ catch { }
1033
+ return redactSensitiveText(closeError);
1034
+ }
1035
+ try {
1036
+ appendScriptSessionLease(pi, sessionName, "closed");
1037
+ }
1038
+ catch {
1039
+ managedSessionRestoreState.disable(sessionName);
1040
+ return "The isolated session closed, but its durable cleanup record could not be saved.";
1041
+ }
1042
+ untrackOwnedManagedSession(ownedManagedSessions, sessionName, AGENT_BROWSER_SCRIPT_NAMESPACE);
1043
+ managedSessionRestoreState.clear(sessionName, AGENT_BROWSER_SCRIPT_NAMESPACE);
1044
+ retireRecordingSession(sessionName, AGENT_BROWSER_SCRIPT_NAMESPACE);
1045
+ clearSessionScopedBrowserState(sessionName, AGENT_BROWSER_SCRIPT_NAMESPACE);
1046
+ return undefined;
1047
+ };
1048
+ const recoverScriptSessionLeasesWithinQueue = async (ctx) => {
1049
+ const pendingSessionNames = new Set([...ownedManagedSessions.values()]
1050
+ .map((session) => session.sessionName)
1051
+ .filter(isAgentBrowserScriptSessionName));
1052
+ for (const lease of getScriptSessionLeasesFromBranch(ctx.sessionManager.getBranch()).values()) {
1053
+ if (lease.cleanup !== "closed")
1054
+ pendingSessionNames.add(lease.sessionName);
1055
+ }
1056
+ for (const sessionName of pendingSessionNames) {
1057
+ trackOwnedManagedSession(ownedManagedSessions, sessionName, ctx.cwd, { branchOwned: true, namespace: AGENT_BROWSER_SCRIPT_NAMESPACE });
1058
+ managedSessionRestoreState.disable(sessionName, AGENT_BROWSER_SCRIPT_NAMESPACE);
1059
+ await closeScriptSessionLeaseWithinQueue(sessionName, ctx.cwd);
1060
+ }
1061
+ };
706
1062
  const restoreBranchBackedState = (ctx, options) => {
707
1063
  branchRestoreGeneration += 1;
708
1064
  branchStateGeneration += 1;
@@ -750,13 +1106,28 @@ export default function agentBrowserExtension(pi) {
750
1106
  sessionPageState = SessionPageState.fromBranch(branch);
751
1107
  traceOwners = new Map();
752
1108
  artifactManifest = restoreArtifactManifestFromBranch(branch);
753
- allowedDomainsBySession = restoreAllowedDomainsBySessionFromBranch(branch);
1109
+ const restoredRecordingState = restoreRecordingReservationStateFromBranch(branch);
1110
+ for (const [key, reservation] of recordingSessionTombstones) {
1111
+ if (restoredRecordingState.terminal.has(key))
1112
+ recordingSessionTombstonesToPersist.delete(key);
1113
+ else
1114
+ recordingSessionTombstonesToPersist.set(key, reservation);
1115
+ }
1116
+ for (const [key, reservation] of restoredRecordingState.terminal) {
1117
+ if (!activeRecordingReservations.has(key))
1118
+ recordingSessionTombstones.set(key, reservation);
1119
+ }
1120
+ for (const key of recordingSessionTombstones.keys())
1121
+ restoredRecordingState.active.delete(key);
1122
+ for (const [key, reservation] of activeRecordingReservations)
1123
+ restoredRecordingState.active.set(key, reservation);
1124
+ activeRecordingReservations = restoredRecordingState.active;
754
1125
  attachedSessionKeys = restoreAttachedSessionKeysFromBranch(branch);
755
1126
  networkRoutesBySession = new Map();
756
1127
  electronLaunchRecords = restoreElectronLaunchRecordsFromBranch(branch);
757
1128
  for (const record of getActiveElectronRecords(electronLaunchRecords)) {
758
1129
  if (record.sessionName)
759
- attachedSessionKeys.add(getSessionContextKey(record.sessionName) ?? record.sessionName);
1130
+ attachedSessionKeys.add(getSessionContextKey(record.sessionName, record.namespace) ?? record.sessionName);
760
1131
  }
761
1132
  if (options.resetRuntimeOwnership) {
762
1133
  ownedManagedSessions.clear();
@@ -778,6 +1149,7 @@ export default function agentBrowserExtension(pi) {
778
1149
  continue;
779
1150
  trackOwnedManagedSession(ownedManagedSessions, identity.sessionName, ctx.cwd, {
780
1151
  branchOwned: true,
1152
+ compatibilityWorkaround: restoreManagedSessionCompatibilityWorkaroundFromBranch(branch, identity.sessionName, identity.namespace),
781
1153
  headedManagedAutosaveDisabled: restoreManagedSessionHeadedAutosaveDisabledFromBranch(branch, identity.sessionName, identity.namespace),
782
1154
  headedManagedAutosaveInterval: restoreManagedSessionHeadedAutosaveIntervalFromBranch(branch, identity.sessionName, identity.namespace),
783
1155
  namespace: identity.namespace,
@@ -786,6 +1158,7 @@ export default function agentBrowserExtension(pi) {
786
1158
  if (restoredState.active) {
787
1159
  trackOwnedManagedSession(ownedManagedSessions, restoredState.sessionName, ctx.cwd, {
788
1160
  branchOwned: true,
1161
+ compatibilityWorkaround: managedSessionCompatibilityWorkaround,
789
1162
  headedManagedAutosaveDisabled: managedSessionHeadedAutosaveDisabled,
790
1163
  headedManagedAutosaveInterval: managedSessionHeadedAutosaveInterval,
791
1164
  namespace: restoredState.namespace,
@@ -841,17 +1214,25 @@ export default function agentBrowserExtension(pi) {
841
1214
  cwd: ctx.cwd,
842
1215
  includeProjectConfig: shouldIncludeProjectConfig(ctx),
843
1216
  }));
1217
+ await artifactExecutionQueue.run(() => managedSessionExecutionQueue.run(() => recoverScriptSessionLeasesWithinQueue(ctx)));
844
1218
  });
845
1219
  pi.on("session_tree", async (_event, ctx) => {
846
- await managedSessionExecutionQueue.run(async () => {
1220
+ for (const controller of activeScriptControllers)
1221
+ controller.abort();
1222
+ await Promise.allSettled([...activeScriptExecutions]);
1223
+ await artifactExecutionQueue.run(() => managedSessionExecutionQueue.run(async () => {
847
1224
  restoreBranchBackedState(ctx, { resetRuntimeOwnership: false });
848
- });
1225
+ await recoverScriptSessionLeasesWithinQueue(ctx);
1226
+ }));
849
1227
  });
850
1228
  pi.on("session_shutdown", async (event, ctx) => {
1229
+ for (const controller of activeScriptControllers)
1230
+ controller.abort();
1231
+ await Promise.allSettled([...activeScriptExecutions]);
851
1232
  branchRestoreGeneration += 1;
852
1233
  branchStateGeneration += 1;
853
1234
  let preservedElectronProfileDirs = [];
854
- await managedSessionExecutionQueue.run(async () => {
1235
+ await artifactExecutionQueue.run(() => managedSessionExecutionQueue.run(async () => {
855
1236
  const shutdownCwd = ctx?.cwd ?? managedSessionCwd;
856
1237
  const quitting = event?.reason === "quit";
857
1238
  preservedElectronProfileDirs = quitting
@@ -874,22 +1255,38 @@ export default function agentBrowserExtension(pi) {
874
1255
  ...getCleanupResultsPreservedUserDataDirs(electronCleanupResults),
875
1256
  ])];
876
1257
  syncElectronCleanupManagedSessions(ownedManagedSessions, electronCleanupResults);
1258
+ for (const identity of getCleanupResultsClosedManagedSessionIdentities(electronCleanupResults))
1259
+ retireRecordingSession(identity.sessionName, identity.namespace);
877
1260
  if (quitting) {
878
- await closeOwnedManagedSessions(ownedManagedSessions, managedSessionRestoreState, implicitSessionCloseTimeoutMs, attachedSessionKeys);
1261
+ await closeOwnedManagedSessions(ownedManagedSessions, managedSessionRestoreState, implicitSessionCloseTimeoutMs, attachedSessionKeys, (owner) => retireRecordingSession(owner.sessionName, owner.namespace));
879
1262
  }
880
1263
  else {
881
- await closeOwnedManagedSessionsExcept(ownedManagedSessions, managedSessionRestoreState, managedSessionActive ? managedSessionName : undefined, implicitSessionCloseTimeoutMs, attachedSessionKeys, managedSessionActive ? managedSessionNamespace : undefined);
1264
+ await closeOwnedManagedSessionsExcept(ownedManagedSessions, managedSessionRestoreState, managedSessionActive ? managedSessionName : undefined, implicitSessionCloseTimeoutMs, attachedSessionKeys, managedSessionActive ? managedSessionNamespace : undefined, (owner) => retireRecordingSession(owner.sessionName, owner.namespace));
882
1265
  }
883
- });
1266
+ }));
884
1267
  managedSessionActive = false;
885
1268
  managedSessionCompatibilityWorkaround = undefined;
886
1269
  managedSessionHeadedAutosaveDisabled = false;
887
1270
  managedSessionHeadedAutosaveInterval = undefined;
888
1271
  managedSessionNamespace = undefined;
1272
+ for (const reservation of recordingSessionTombstonesToPersist.values()) {
1273
+ try {
1274
+ appendRecordingReservationTransition(pi, { reservation, state: "closed" });
1275
+ }
1276
+ catch { }
1277
+ }
1278
+ for (const reservation of activeRecordingReservations.values()) {
1279
+ try {
1280
+ appendRecordingReservationTransition(pi, { reservation, state: "active" });
1281
+ }
1282
+ catch { }
1283
+ }
889
1284
  sessionPageState.reset();
890
1285
  traceOwners = new Map();
891
1286
  artifactManifest = undefined;
892
- allowedDomainsBySession = new Map();
1287
+ activeRecordingReservations = new Map();
1288
+ recordingSessionTombstones = new Map();
1289
+ recordingSessionTombstonesToPersist = new Map();
893
1290
  attachedSessionKeys = new Set();
894
1291
  networkRoutesBySession = new Map();
895
1292
  electronLaunchRecords = new Map();
@@ -897,7 +1294,6 @@ export default function agentBrowserExtension(pi) {
897
1294
  branchOwnedElectronLaunchIds = new Set();
898
1295
  electronChildProcesses = new Map();
899
1296
  ownedManagedSessions.clear();
900
- cleanupManagedSessionRestoreConfig();
901
1297
  await cleanupSecureTempArtifacts({ preservePaths: preservedElectronProfileDirs });
902
1298
  });
903
1299
  pi.on("before_agent_start", async (event, ctx) => {
@@ -937,16 +1333,16 @@ export default function agentBrowserExtension(pi) {
937
1333
  }
938
1334
  });
939
1335
  pi.on("tool_result", async (event) => buildAgentBrowserToolResultPatch(event));
940
- pi.registerTool({
1336
+ const agentBrowserTool = {
941
1337
  name: "agent_browser",
942
1338
  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.",
1339
+ description: "Browse and interact with websites using agent-browser. Use this for reading live pages, opening known URLs, 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
1340
  promptSnippet: "Browse websites, read live docs, click and fill pages, extract browser content, take screenshots, and automate real web workflows.",
945
1341
  promptGuidelines: toolPromptGuidelines,
946
1342
  parameters: AGENT_BROWSER_PARAMS,
947
1343
  renderCall(args, theme, context) {
948
1344
  const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
949
- text.setText(formatAgentBrowserRenderCall(args, theme));
1345
+ text.setText(formatAgentBrowserRenderCall(args, theme, context.expanded));
950
1346
  return text;
951
1347
  },
952
1348
  renderResult(result, options, theme, context) {
@@ -956,20 +1352,138 @@ export default function agentBrowserExtension(pi) {
956
1352
  component.setState(formatAgentBrowserRenderResult(result, options, theme, context.isError), options.expanded, theme);
957
1353
  return component;
958
1354
  },
959
- async execute(_toolCallId, params, signal, onUpdate, ctx) {
1355
+ async execute(toolCallId, params, signal, onUpdate, ctx) {
960
1356
  const promptPolicy = buildPromptPolicy(getLatestUserPrompt(ctx.sessionManager.getBranch()));
961
1357
  const outputPath = isRecord(params) && typeof params.outputPath === "string" ? params.outputPath : undefined;
962
1358
  const resolvedInput = resolveAgentBrowserInput({
963
- getBatchPreflightValidationError,
1359
+ getBatchPreflightValidationError: (args, stdin) => getArtifactPreflightValidationError({ args, cwd: ctx.cwd, outputPath, stdin }),
964
1360
  managedSessionActive,
965
1361
  params,
966
1362
  });
967
1363
  if (resolvedInput.status === "invalid") {
968
1364
  return buildValidationFailureResult(resolvedInput);
969
1365
  }
970
- const outputPathValidationError = getAgentBrowserOutputPathValidationError(outputPath, ctx.cwd);
971
- if (outputPathValidationError) {
972
- return buildValidationFailureResult({ attemptedKind: resolvedInput.kind, kind: "invalid", redactedArgs: resolvedInput.redactedArgs, status: "invalid", toolArgs: resolvedInput.toolArgs, toolStdin: resolvedInput.toolStdin, validationError: outputPathValidationError });
1366
+ const applyUnserializedOutputPath = async (result, preserveTextContent = false) => {
1367
+ if (!outputPath || result.isError === true || (isRecord(result.details) && result.details.resultCategory === "failure"))
1368
+ return result;
1369
+ return artifactExecutionQueue.run(async () => {
1370
+ const reservationError = getArtifactPreflightValidationError({
1371
+ activeRecordingReservations: activeRecordingReservations.values(),
1372
+ args: [],
1373
+ cwd: ctx.cwd,
1374
+ outputPath,
1375
+ });
1376
+ if (reservationError) {
1377
+ return buildValidationFailureResult({ attemptedKind: resolvedInput.kind, kind: "invalid", redactedArgs: resolvedInput.redactedArgs, status: "invalid", toolArgs: resolvedInput.toolArgs, toolStdin: resolvedInput.toolStdin, validationError: reservationError });
1378
+ }
1379
+ return applyAgentBrowserOutputPath({ cwd: ctx.cwd, outputPath, preserveTextContent, result });
1380
+ });
1381
+ };
1382
+ const versionCheckCommand = extractUpstreamCommandTokens(resolvedInput.toolArgs)[0];
1383
+ const electronHostOnlyAction = resolvedInput.kind === "electron" && ["cleanup", "list", "status"].includes(resolvedInput.compiledElectron.action);
1384
+ const browserBackedVersionCheck = needsManagedSession(parseArgvDescriptor(resolvedInput.toolArgs));
1385
+ if (resolvedInput.kind !== "script" && !electronHostOnlyAction && browserBackedVersionCheck && !isPlainTextInspectionArgs(resolvedInput.toolArgs) && !isCloseCommand(versionCheckCommand) && signal?.aborted !== true) {
1386
+ const versionFailure = await validateUpstreamVersion(ctx.cwd, signal);
1387
+ if (versionFailure)
1388
+ return applyAgentBrowserOutputPath({ cwd: ctx.cwd, outputPath, result: versionFailure });
1389
+ }
1390
+ if (resolvedInput.kind === "script") {
1391
+ if (!ctx.sessionManager.getSessionFile()) {
1392
+ return buildValidationFailureResult({
1393
+ attemptedKind: "script",
1394
+ kind: "invalid",
1395
+ redactedArgs: [],
1396
+ status: "invalid",
1397
+ toolArgs: [],
1398
+ validationError: "script requires a persisted Pi session so its isolated browser-session cleanup lease survives restart; relaunch Pi without --no-session.",
1399
+ });
1400
+ }
1401
+ const sessionName = createAgentBrowserScriptSessionName();
1402
+ const innerResults = [];
1403
+ const scriptTimeoutMs = params.timeoutMs ?? AGENT_BROWSER_SCRIPT_DEFAULT_TIMEOUT_MS;
1404
+ const deadline = Date.now() + scriptTimeoutMs;
1405
+ let leased = false;
1406
+ let cleanupError;
1407
+ let run = {
1408
+ callCount: 0,
1409
+ emitCount: 0,
1410
+ error: "Script sandbox execution failed.",
1411
+ failureCategory: "upstream-error",
1412
+ ok: false,
1413
+ rejectedCallCount: 0,
1414
+ steps: [],
1415
+ };
1416
+ const scriptController = new AbortController();
1417
+ const abortScript = () => scriptController.abort();
1418
+ signal?.addEventListener("abort", abortScript, { once: true });
1419
+ if (signal?.aborted)
1420
+ scriptController.abort();
1421
+ activeScriptControllers.add(scriptController);
1422
+ let finishScriptExecution;
1423
+ const scriptExecution = new Promise((resolve) => {
1424
+ finishScriptExecution = resolve;
1425
+ });
1426
+ activeScriptExecutions.add(scriptExecution);
1427
+ try {
1428
+ // Keep preflight inside shutdown tracking so quit cannot race into starting the sandbox afterward.
1429
+ const versionFailure = await withIsolatedAgentBrowserEnvironment(() => validateUpstreamVersion(ctx.cwd, scriptController.signal));
1430
+ if (versionFailure)
1431
+ return applyAgentBrowserOutputPath({ cwd: ctx.cwd, outputPath, result: versionFailure });
1432
+ const pendingRun = runAgentBrowserScript({
1433
+ beforeFirstCall() {
1434
+ appendScriptSessionLease(pi, sessionName, "active");
1435
+ trackOwnedManagedSession(ownedManagedSessions, sessionName, ctx.cwd, { namespace: AGENT_BROWSER_SCRIPT_NAMESPACE });
1436
+ managedSessionRestoreState.disable(sessionName, AGENT_BROWSER_SCRIPT_NAMESPACE);
1437
+ leased = true;
1438
+ },
1439
+ code: resolvedInput.compiledScript.code,
1440
+ dispatch: async (innerParams, innerSignal) => {
1441
+ const remainingMs = Math.max(1, deadline - Date.now());
1442
+ const innerTimeoutMs = Math.min(innerParams.timeoutMs ?? remainingMs, remainingMs);
1443
+ const innerResult = await withIsolatedAgentBrowserEnvironment(() => agentBrowserTool.execute(`${toolCallId}:script:${innerResults.length + 1}`, {
1444
+ args: ["--namespace", AGENT_BROWSER_SCRIPT_NAMESPACE, "--session", sessionName, ...innerParams.args],
1445
+ stdin: innerParams.stdin,
1446
+ timeoutMs: innerTimeoutMs,
1447
+ }, innerSignal, undefined, ctx));
1448
+ innerResults.push(innerResult);
1449
+ return await buildScriptBrowserEnvelope(innerResult, innerParams.args, sessionName);
1450
+ },
1451
+ signal: scriptController.signal,
1452
+ timeoutMs: scriptTimeoutMs,
1453
+ });
1454
+ run = await pendingRun;
1455
+ }
1456
+ catch { }
1457
+ finally {
1458
+ activeScriptControllers.delete(scriptController);
1459
+ signal?.removeEventListener("abort", abortScript);
1460
+ if (leased) {
1461
+ try {
1462
+ cleanupError = await artifactExecutionQueue.run(() => managedSessionExecutionQueue.run(() => closeScriptSessionLeaseWithinQueue(sessionName, ctx.cwd)));
1463
+ }
1464
+ catch {
1465
+ cleanupError = "The isolated script session cleanup operation failed.";
1466
+ try {
1467
+ appendScriptSessionLease(pi, sessionName, "failed");
1468
+ }
1469
+ catch { }
1470
+ }
1471
+ }
1472
+ activeScriptExecutions.delete(scriptExecution);
1473
+ finishScriptExecution();
1474
+ }
1475
+ let scriptResult = buildScriptToolResult({ cleanupError, innerResults, run, sessionName: leased ? sessionName : undefined });
1476
+ if (artifactManifest) {
1477
+ scriptResult = {
1478
+ ...scriptResult,
1479
+ details: {
1480
+ ...(isRecord(scriptResult.details) ? scriptResult.details : {}),
1481
+ artifactManifest,
1482
+ artifactRetentionSummary: formatSessionArtifactRetentionSummary(artifactManifest),
1483
+ },
1484
+ };
1485
+ }
1486
+ return applyUnserializedOutputPath(scriptResult);
973
1487
  }
974
1488
  const { toolArgs } = resolvedInput;
975
1489
  const compiledElectron = resolvedInput.kind === "electron" ? resolvedInput.compiledElectron : undefined;
@@ -980,7 +1494,7 @@ export default function agentBrowserExtension(pi) {
980
1494
  compiledElectron,
981
1495
  ownedRecords: ownedElectronLaunchRecords,
982
1496
  });
983
- const electronHostResult = await handleElectronHostInput({
1497
+ let electronHostResult = await handleElectronHostInput({
984
1498
  attachedSessionKeys,
985
1499
  compiledElectron,
986
1500
  cwd: ctx.cwd,
@@ -1012,11 +1526,16 @@ export default function agentBrowserExtension(pi) {
1012
1526
  }
1013
1527
  replaceWithActiveElectronLaunchRecords(ownedElectronLaunchRecords, electronHostLaunchRecords, branchOwnedElectronLaunchIds, cleanedLaunchIds);
1014
1528
  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) {
1529
+ const cleanupNamespace = isRecord(electronHostResult.details) && typeof electronHostResult.details.namespace === "string"
1530
+ ? electronHostResult.details.namespace
1531
+ : undefined;
1532
+ const closedSessionIdentities = getCleanupResultsClosedManagedSessionIdentities(cleanupRecords, cleanupNamespace);
1533
+ syncElectronCleanupManagedSessions(ownedManagedSessions, cleanupRecords, cleanupNamespace);
1534
+ for (const identity of closedSessionIdentities) {
1535
+ retireRecordingSession(identity.sessionName, identity.namespace);
1536
+ const closedSessionKey = getSessionContextKey(identity.sessionName, identity.namespace) ?? identity.sessionName;
1537
+ clearSessionScopedBrowserState(closedSessionKey);
1538
+ if (closedSessionKey === (getSessionContextKey(managedSessionName, managedSessionNamespace) ?? managedSessionName)) {
1020
1539
  managedSessionActive = false;
1021
1540
  managedSessionCompatibilityWorkaround = undefined;
1022
1541
  managedSessionHeadedAutosaveDisabled = false;
@@ -1026,14 +1545,39 @@ export default function agentBrowserExtension(pi) {
1026
1545
  managedSessionName = createFreshSessionName(managedSessionBaseName, ephemeralSessionSeed, freshSessionOrdinal);
1027
1546
  }
1028
1547
  }
1548
+ if (artifactManifest) {
1549
+ electronHostResult = {
1550
+ ...electronHostResult,
1551
+ details: {
1552
+ ...(isRecord(electronHostResult.details) ? electronHostResult.details : {}),
1553
+ artifactManifest,
1554
+ artifactRetentionSummary: formatSessionArtifactRetentionSummary(artifactManifest),
1555
+ },
1556
+ };
1557
+ }
1029
1558
  }
1030
1559
  return electronHostResult;
1031
1560
  };
1032
- const electronHostResult = shouldSerializeElectronHostInput(compiledElectron)
1033
- ? await managedSessionExecutionQueue.run(runElectronHostInput)
1034
- : await runElectronHostInput();
1561
+ const runSerializedElectronHostInput = () => shouldSerializeElectronHostInput(compiledElectron)
1562
+ ? managedSessionExecutionQueue.run(runElectronHostInput)
1563
+ : runElectronHostInput();
1564
+ const electronHostResult = compiledElectron?.action === "cleanup"
1565
+ ? await artifactExecutionQueue.run(async () => {
1566
+ const reservationError = outputPath ? getArtifactPreflightValidationError({
1567
+ activeRecordingReservations: activeRecordingReservations.values(),
1568
+ args: [],
1569
+ cwd: ctx.cwd,
1570
+ outputPath,
1571
+ }) : undefined;
1572
+ if (reservationError) {
1573
+ return buildValidationFailureResult({ attemptedKind: resolvedInput.kind, kind: "invalid", redactedArgs: resolvedInput.redactedArgs, status: "invalid", toolArgs: resolvedInput.toolArgs, toolStdin: resolvedInput.toolStdin, validationError: reservationError });
1574
+ }
1575
+ const result = await runSerializedElectronHostInput();
1576
+ return result ? applyAgentBrowserOutputPath({ cwd: ctx.cwd, outputPath, result }) : result;
1577
+ })
1578
+ : await runSerializedElectronHostInput();
1035
1579
  if (electronHostResult) {
1036
- return applyAgentBrowserOutputPath({ cwd: ctx.cwd, outputPath, result: electronHostResult });
1580
+ return compiledElectron?.action === "cleanup" ? electronHostResult : applyUnserializedOutputPath(electronHostResult);
1037
1581
  }
1038
1582
  const explicitSessionName = extractExplicitSessionName(toolArgs);
1039
1583
  const explicitNamespace = extractExplicitNamespace(toolArgs);
@@ -1044,15 +1588,17 @@ export default function agentBrowserExtension(pi) {
1044
1588
  ownedElectronLaunchRecords,
1045
1589
  ownedManagedSessions,
1046
1590
  });
1591
+ const callerOwnedSessionNamespace = explicitSessionName
1592
+ ? resolveAgentBrowserNamespace(toolArgs, getAgentBrowserProcessEnvironment().AGENT_BROWSER_NAMESPACE)
1593
+ : undefined;
1047
1594
  const callerOwnedSessionQueueKey = !serializeBrowserCommand && explicitSessionName
1048
- ? getSessionContextKey(explicitSessionName, resolveAgentBrowserNamespace(toolArgs, process.env.AGENT_BROWSER_NAMESPACE)) ?? explicitSessionName
1595
+ ? getSessionContextKey(explicitSessionName, callerOwnedSessionNamespace) ?? explicitSessionName
1049
1596
  : undefined;
1050
1597
  const runBrowserCommand = async () => {
1051
1598
  const branchRestoreGenerationAtStart = branchRestoreGeneration;
1052
1599
  const generationAtStart = branchStateGeneration;
1053
1600
  const sessionPageStateUpdate = sessionPageState.beginUpdate();
1054
1601
  const browserRunState = {
1055
- allowedDomainsBySession,
1056
1602
  artifactManifest,
1057
1603
  attachedSessionKeys,
1058
1604
  closedManagedSessionNames: new Set(),
@@ -1074,7 +1620,6 @@ export default function agentBrowserExtension(pi) {
1074
1620
  sessionPageState,
1075
1621
  traceOwners,
1076
1622
  };
1077
- const initialAllowedDomainsBySession = browserRunState.allowedDomainsBySession;
1078
1623
  const initialArtifactManifest = browserRunState.artifactManifest;
1079
1624
  const initialNetworkRoutesBySession = browserRunState.networkRoutesBySession;
1080
1625
  const attachedSessionRequested = isAttachedBrowserInvocation(toolArgs)
@@ -1103,20 +1648,30 @@ export default function agentBrowserExtension(pi) {
1103
1648
  state: browserRunState,
1104
1649
  });
1105
1650
  const branchRestoreStillCurrent = branchRestoreGenerationAtStart === branchRestoreGeneration;
1651
+ const resultDetails = isRecord(result.details) ? result.details : undefined;
1652
+ const resultSessionName = typeof resultDetails?.sessionName === "string"
1653
+ ? resultDetails.sessionName
1654
+ : extractExplicitSessionName(toolArgs);
1655
+ const resultNamespace = typeof resultDetails?.namespace === "string"
1656
+ ? resultDetails.namespace
1657
+ : resolveAgentBrowserNamespace(toolArgs, getAgentBrowserProcessEnvironment().AGENT_BROWSER_NAMESPACE);
1106
1658
  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);
1659
+ const resultBatchCloseLifecycle = getSuccessfulBatchCloseLifecycle(resultDetails?.batchSteps);
1114
1660
  const resultSessionKey = getSessionContextKey(resultSessionName, resultNamespace) ?? resultSessionName;
1115
1661
  const managedSessionOutcome = isRecord(resultDetails?.managedSessionOutcome) ? resultDetails.managedSessionOutcome : undefined;
1662
+ const closeAllApplied = resultDetails?.closeAllApplied === true;
1116
1663
  const attachedSessionRemainsActive = result.isError !== true
1117
- || (attachedSessionRequested && managedSessionOutcome?.activeAfter === true);
1118
- const closesAttachedSession = result.isError !== true && isCloseCommand(extractCommandTokens(toolArgs)[0]);
1119
- if (resultSessionKey && closesAttachedSession)
1664
+ || ((attachedSessionRequested || attachedSessionKnown) && managedSessionOutcome?.activeAfter === true);
1665
+ const closesAttachedSession = (result.isError !== true && isCloseCommand(extractUpstreamCommandTokens(toolArgs)[0]))
1666
+ || resultBatchCloseLifecycle?.endsClosed === true;
1667
+ if (closeAllApplied) {
1668
+ deleteIdentityKeysInNamespace(attachedSessionKeys, resultNamespace);
1669
+ if (resultSessionKey && attachedSessionRemainsActive && (attachedSessionRequested || attachedSessionKnown) && resultBatchCloseLifecycle?.endsClosed === false) {
1670
+ attachedSessionKeys.add(resultSessionKey);
1671
+ result = { ...result, details: { ...(resultDetails ?? {}), attachedBrowserSession: true } };
1672
+ }
1673
+ }
1674
+ else if (resultSessionKey && closesAttachedSession)
1120
1675
  attachedSessionKeys.delete(resultSessionKey);
1121
1676
  else if (resultSessionKey && attachedSessionRemainsActive && (attachedSessionRequested || attachedSessionKnown)) {
1122
1677
  attachedSessionKeys.add(resultSessionKey);
@@ -1124,9 +1679,31 @@ export default function agentBrowserExtension(pi) {
1124
1679
  }
1125
1680
  }
1126
1681
  if (branchRestoreStillCurrent) {
1127
- allowedDomainsBySession = mergeBrowserRunMap(allowedDomainsBySession, initialAllowedDomainsBySession, browserRunState.allowedDomainsBySession);
1128
1682
  networkRoutesBySession = mergeBrowserRunMap(networkRoutesBySession, initialNetworkRoutesBySession, browserRunState.networkRoutesBySession);
1129
1683
  artifactManifest = mergeBrowserRunArtifactManifest(artifactManifest, initialArtifactManifest, browserRunState.artifactManifest);
1684
+ const handledBatchCloseKeys = syncRecordingReservationsFromResult(result);
1685
+ if (resultDetails?.closeAllApplied === true) {
1686
+ for (const [sessionKey, reservation] of [...activeRecordingReservations]) {
1687
+ if (isAgentBrowserSessionIdentityKeyInNamespace(sessionKey, resultNamespace)) {
1688
+ retireRecordingSession(reservation.sessionName, reservation.namespace);
1689
+ }
1690
+ }
1691
+ }
1692
+ for (const closedSessionKey of browserRunState.closedManagedSessionNames) {
1693
+ if (handledBatchCloseKeys.has(closedSessionKey))
1694
+ continue;
1695
+ const reservation = activeRecordingReservations.get(closedSessionKey);
1696
+ if (reservation)
1697
+ retireRecordingSession(reservation.sessionName, reservation.namespace);
1698
+ }
1699
+ if (resultSessionName && resultReportsNoRecordingInProgress(result)) {
1700
+ retireRecordingSession(resultSessionName, resultNamespace);
1701
+ }
1702
+ if (resultSessionName) {
1703
+ const reservation = activeRecordingReservations.get(getAgentBrowserSessionIdentityKey(resultSessionName, resultNamespace));
1704
+ if (reservation)
1705
+ result = appendActiveRecordingCleanupAction(result, reservation);
1706
+ }
1130
1707
  if (artifactManifest) {
1131
1708
  result = {
1132
1709
  ...result,
@@ -1155,7 +1732,7 @@ export default function agentBrowserExtension(pi) {
1155
1732
  mergeActiveElectronLaunchRecords(ownedElectronLaunchRecords, electronLaunchRecords, {
1156
1733
  branchOwnedLaunchIds: branchOwnedElectronLaunchIds,
1157
1734
  touchedLaunchIds: !result.isError
1158
- ? getTouchedElectronLaunchIds(explicitSessionName ?? browserRunState.managedSessionName, electronLaunchRecords)
1735
+ ? getTouchedElectronLaunchIds(explicitSessionName ?? browserRunState.managedSessionName, electronLaunchRecords, explicitSessionName ? resolveAgentBrowserNamespace(toolArgs, getAgentBrowserProcessEnvironment().AGENT_BROWSER_NAMESPACE) : browserRunState.managedSessionNamespace)
1159
1736
  : undefined,
1160
1737
  });
1161
1738
  if (serializeBrowserCommand)
@@ -1163,12 +1740,47 @@ export default function agentBrowserExtension(pi) {
1163
1740
  }
1164
1741
  return applyAgentBrowserOutputPath({ cwd: ctx.cwd, outputPath, preserveTextContent: Array.isArray(params.args) && params.args.includes("--json"), result });
1165
1742
  };
1166
- if (serializeBrowserCommand)
1167
- return managedSessionExecutionQueue.run(runBrowserCommand);
1168
- return callerOwnedSessionQueueKey
1169
- ? callerOwnedSessionExecutionQueues.run(callerOwnedSessionQueueKey, runBrowserCommand)
1170
- : runBrowserCommand();
1743
+ const closesAllSessions = commandClosesAllSessions(toolArgs, resolvedInput.toolStdin);
1744
+ const closeAllNamespace = closesAllSessions
1745
+ ? resolveAgentBrowserNamespace(toolArgs, getAgentBrowserProcessEnvironment().AGENT_BROWSER_NAMESPACE)
1746
+ : undefined;
1747
+ const runWithinSessionQueue = () => {
1748
+ if (closesAllSessions)
1749
+ return managedSessionExecutionQueue.run(() => callerOwnedSessionExecutionQueues.runExclusive(closeAllNamespace, runBrowserCommand));
1750
+ if (serializeBrowserCommand)
1751
+ return managedSessionExecutionQueue.run(runBrowserCommand);
1752
+ return callerOwnedSessionQueueKey
1753
+ ? callerOwnedSessionExecutionQueues.run(callerOwnedSessionQueueKey, callerOwnedSessionNamespace, runBrowserCommand)
1754
+ : runBrowserCommand();
1755
+ };
1756
+ if (!commandTouchesArtifactLifecycle(toolArgs, resolvedInput.toolStdin, outputPath))
1757
+ return runWithinSessionQueue();
1758
+ return artifactExecutionQueue.run(async () => {
1759
+ const artifactValidationError = getArtifactPreflightValidationError({
1760
+ activeRecordingReservations: activeRecordingReservations.values(),
1761
+ args: toolArgs,
1762
+ cwd: ctx.cwd,
1763
+ outputPath,
1764
+ stdin: resolvedInput.toolStdin,
1765
+ });
1766
+ if (!artifactValidationError)
1767
+ return runWithinSessionQueue();
1768
+ return applyAgentBrowserOutputPath({
1769
+ cwd: ctx.cwd,
1770
+ outputPath,
1771
+ result: buildValidationFailureResult({
1772
+ attemptedKind: resolvedInput.kind,
1773
+ kind: "invalid",
1774
+ redactedArgs: resolvedInput.redactedArgs,
1775
+ status: "invalid",
1776
+ toolArgs: resolvedInput.toolArgs,
1777
+ toolStdin: resolvedInput.toolStdin,
1778
+ validationError: artifactValidationError,
1779
+ }),
1780
+ });
1781
+ });
1171
1782
  },
1172
- });
1783
+ };
1784
+ pi.registerTool(agentBrowserTool);
1173
1785
  registerWebSearchToolIfAvailable(agentBrowserConfig);
1174
1786
  }