pi-agent-browser-native 0.2.77 → 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 (118) hide show
  1. package/CHANGELOG.md +154 -0
  2. package/README.md +69 -35
  3. package/dist/extensions/agent-browser/index.js +911 -121
  4. package/dist/extensions/agent-browser/lib/argv-descriptor.js +35 -8
  5. package/dist/extensions/agent-browser/lib/argv-grammar.js +48 -7
  6. package/dist/extensions/agent-browser/lib/batch-lifecycle.js +71 -0
  7. package/dist/extensions/agent-browser/lib/command-policy.js +1 -6
  8. package/dist/extensions/agent-browser/lib/command-taxonomy.js +35 -8
  9. package/dist/extensions/agent-browser/lib/config-policy.js +0 -5
  10. package/dist/extensions/agent-browser/lib/config.js +0 -6
  11. package/dist/extensions/agent-browser/lib/electron/cdp.js +6 -7
  12. package/dist/extensions/agent-browser/lib/electron/cleanup.js +0 -7
  13. package/dist/extensions/agent-browser/lib/electron/discovery.js +0 -7
  14. package/dist/extensions/agent-browser/lib/electron/launch.js +0 -7
  15. package/dist/extensions/agent-browser/lib/input-modes/electron.js +0 -5
  16. package/dist/extensions/agent-browser/lib/input-modes/job.js +61 -9
  17. package/dist/extensions/agent-browser/lib/input-modes/lookups.js +2 -7
  18. package/dist/extensions/agent-browser/lib/input-modes/params.js +23 -29
  19. package/dist/extensions/agent-browser/lib/input-modes/script.js +462 -0
  20. package/dist/extensions/agent-browser/lib/input-modes/semantic-action.js +51 -17
  21. package/dist/extensions/agent-browser/lib/input-modes/shared.js +0 -5
  22. package/dist/extensions/agent-browser/lib/input-modes/types.js +0 -5
  23. package/dist/extensions/agent-browser/lib/json-schema.js +0 -5
  24. package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +12 -5
  25. package/dist/extensions/agent-browser/lib/managed-session-capabilities.js +0 -2
  26. package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +3 -4
  27. package/dist/extensions/agent-browser/lib/managed-session-restore.js +55 -48
  28. package/dist/extensions/agent-browser/lib/managed-session-snapshots.js +2 -9
  29. package/dist/extensions/agent-browser/lib/managed-session-state-policy.js +48 -36
  30. package/dist/extensions/agent-browser/lib/managed-session-storage.js +50 -29
  31. package/dist/extensions/agent-browser/lib/navigation-policy.js +0 -5
  32. package/dist/extensions/agent-browser/lib/orchestration/batch-stdin.js +26 -5
  33. package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.js +110 -30
  34. package/dist/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.js +2 -1
  35. package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +27 -26
  36. package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +58 -33
  37. package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +2 -1
  38. package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +22 -14
  39. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/direct-anchor-download.js +1 -1
  40. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/network-page-filter.js +1 -1
  41. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/scroll-shims.js +1 -1
  42. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/snapshot-filter.js +1 -1
  43. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +148 -34
  44. package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +225 -61
  45. package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +74 -30
  46. package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +47 -26
  47. package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +40 -19
  48. package/dist/extensions/agent-browser/lib/orchestration/output-file.js +42 -1
  49. package/dist/extensions/agent-browser/lib/orchestration/script-mode.js +299 -0
  50. package/dist/extensions/agent-browser/lib/parsing.js +0 -7
  51. package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +36 -11
  52. package/dist/extensions/agent-browser/lib/playbook.js +20 -24
  53. package/dist/extensions/agent-browser/lib/process-environment.js +14 -0
  54. package/dist/extensions/agent-browser/lib/process-identity.js +4 -9
  55. package/dist/extensions/agent-browser/lib/process.js +131 -48
  56. package/dist/extensions/agent-browser/lib/prompt-policy.js +171 -27
  57. package/dist/extensions/agent-browser/lib/recording-reservations.js +183 -0
  58. package/dist/extensions/agent-browser/lib/results/action-recommendations.js +63 -13
  59. package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +68 -11
  60. package/dist/extensions/agent-browser/lib/results/categories.js +7 -9
  61. package/dist/extensions/agent-browser/lib/results/confirmation.js +0 -7
  62. package/dist/extensions/agent-browser/lib/results/contracts.js +0 -7
  63. package/dist/extensions/agent-browser/lib/results/editable-ref-evidence.js +0 -7
  64. package/dist/extensions/agent-browser/lib/results/envelope.js +0 -7
  65. package/dist/extensions/agent-browser/lib/results/network.js +0 -7
  66. package/dist/extensions/agent-browser/lib/results/next-actions.js +19 -35
  67. package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +85 -44
  68. package/dist/extensions/agent-browser/lib/results/presentation/batch.js +66 -11
  69. package/dist/extensions/agent-browser/lib/results/presentation/browser-profile-recovery.js +0 -5
  70. package/dist/extensions/agent-browser/lib/results/presentation/common.js +18 -5
  71. package/dist/extensions/agent-browser/lib/results/presentation/content.js +0 -5
  72. package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +7 -6
  73. package/dist/extensions/agent-browser/lib/results/presentation/errors.js +2 -1
  74. package/dist/extensions/agent-browser/lib/results/presentation/large-output.js +0 -5
  75. package/dist/extensions/agent-browser/lib/results/presentation/managed-list-filter.js +0 -5
  76. package/dist/extensions/agent-browser/lib/results/presentation/navigation.js +26 -16
  77. package/dist/extensions/agent-browser/lib/results/presentation/registry.js +58 -13
  78. package/dist/extensions/agent-browser/lib/results/presentation/semantic-action.js +1 -15
  79. package/dist/extensions/agent-browser/lib/results/presentation/skills.js +0 -5
  80. package/dist/extensions/agent-browser/lib/results/presentation.js +6 -8
  81. package/dist/extensions/agent-browser/lib/results/recovery-actions.js +2 -7
  82. package/dist/extensions/agent-browser/lib/results/recovery-next-actions.js +0 -7
  83. package/dist/extensions/agent-browser/lib/results/selector-recovery.js +51 -15
  84. package/dist/extensions/agent-browser/lib/results/snapshot-high-value-controls.js +13 -14
  85. package/dist/extensions/agent-browser/lib/results/snapshot-refs.js +0 -7
  86. package/dist/extensions/agent-browser/lib/results/snapshot-segments.js +0 -7
  87. package/dist/extensions/agent-browser/lib/results/snapshot-spill.js +0 -7
  88. package/dist/extensions/agent-browser/lib/results/snapshot.js +0 -7
  89. package/dist/extensions/agent-browser/lib/results/text.js +0 -7
  90. package/dist/extensions/agent-browser/lib/runtime.js +119 -52
  91. package/dist/extensions/agent-browser/lib/session-page-state.js +62 -17
  92. package/dist/extensions/agent-browser/lib/string-enum-schema.js +0 -5
  93. package/dist/extensions/agent-browser/lib/temp.js +0 -7
  94. package/dist/extensions/agent-browser/lib/upstream-version.js +14 -0
  95. package/dist/extensions/agent-browser/lib/web-search.js +0 -5
  96. package/dist/extensions/agent-browser/script-worker.js +169 -0
  97. package/dist/scripts/agent-browser-target.mjs +3 -0
  98. package/docs/ARCHITECTURE.md +50 -31
  99. package/docs/COMMAND_REFERENCE.md +97 -41
  100. package/docs/ELECTRON.md +3 -19
  101. package/docs/RELEASE.md +8 -20
  102. package/docs/REQUIREMENTS.md +8 -6
  103. package/docs/SUPPORT_MATRIX.md +32 -25
  104. package/docs/TOOL_CONTRACT.md +107 -64
  105. package/docs/platform-smoke.md +1 -1
  106. package/package.json +7 -6
  107. package/platform-smoke.config.mjs +2 -2
  108. package/scripts/agent-browser-capability-baseline.mjs +24 -6
  109. package/scripts/agent-browser-target.mjs +3 -0
  110. package/scripts/build.mjs +41 -0
  111. package/scripts/doctor.mjs +8 -7
  112. package/scripts/platform-smoke/browser-dogfood-windows.ps1 +9 -3
  113. package/scripts/platform-smoke/targets.mjs +12 -6
  114. package/dist/extensions/agent-browser/lib/electron/text.js +0 -13
  115. package/dist/extensions/agent-browser/lib/input-modes.js +0 -10
  116. package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-artifacts.js +0 -5
  117. package/dist/extensions/agent-browser/lib/results/artifact-state.js +0 -13
  118. package/dist/extensions/agent-browser/lib/results.js +0 -14
@@ -1,34 +1,38 @@
1
- /**
2
- * Purpose: Register the native agent_browser tool for pi so agents can invoke agent-browser without going through bash.
3
- * Responsibilities: Define the tool schema, inject thin wrapper behavior around the upstream CLI, manage extension-owned browser session convenience, and return pi-friendly content/details.
4
- * Scope: Native tool registration and orchestration only; the wrapper intentionally stays close to the upstream agent-browser CLI.
5
- * Usage: Loaded by pi through the package manifest in this package, or explicitly via `pi --no-extensions -e .` during local checkout development.
6
- * Invariants/Assumptions: agent-browser is installed separately on PATH, the wrapper targets the current locally installed upstream version only, and no backward-compatibility shims are provided.
7
- */
8
1
  import { existsSync, readFileSync } from "node:fs";
9
2
  import { dirname, join } from "node:path";
10
3
  import { fileURLToPath } from "node:url";
11
4
  import { Text } from "@earendil-works/pi-tui";
5
+ import { batchHasSuccessfulCloseAll, getSuccessfulBatchCloseLifecycle } from "./lib/batch-lifecycle.js";
12
6
  import { PROJECT_RULE_PROMPT, buildBrowserDefaultProfileGuideline, buildBrowserExecutablePathGuideline, buildToolPromptGuidelines, } from "./lib/playbook.js";
13
7
  import { SessionPageState } from "./lib/session-page-state.js";
14
- import { canUseHeadlessCompatibilityUserAgent, createEphemeralSessionSeed, createFreshSessionName, createImplicitSessionName, extractCommandTokens, getImplicitSessionCloseTimeoutMs, getImplicitSessionIdleTimeoutMs, restoreManagedSessionStateFromBranch, validateToolArgs, } from "./lib/runtime.js";
15
- import { extractExplicitNamespace, extractExplicitSessionName, 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";
16
12
  import { cleanupManagedSessionRestoreConfig, ManagedSessionRestoreState } from "./lib/managed-session-restore.js";
17
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";
18
17
  import { buildPromptPolicy, getLatestUserPrompt, shouldAppendBrowserSystemPrompt } from "./lib/prompt-policy.js";
19
- import { isCloseCommand } from "./lib/command-taxonomy.js";
18
+ import { isCloseAllCommand, isCloseCommand } from "./lib/command-taxonomy.js";
20
19
  import { hasLaunchScopedFlagToken } from "./lib/launch-scoped-flags.js";
21
20
  import { cleanupSecureTempArtifacts } from "./lib/temp.js";
22
- import { AGENT_BROWSER_PARAMS, } from "./lib/input-modes.js";
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";
23
23
  import { parseAllowedDomainsPolicyFromArgs } from "./lib/navigation-policy.js";
24
24
  import { closeManagedSession, getSessionContextKey, runAgentBrowserTool } from "./lib/orchestration/browser-run/index.js";
25
+ import { canonicalizeExplicitArtifactDestination, getExplicitArtifactDestination } from "./lib/orchestration/browser-run/artifact-paths.js";
25
26
  import { findElectronLaunchRecordForSession, getActiveElectronRecords } from "./lib/orchestration/browser-run/session-state.js";
26
- import { parseBatchStdinJsonArray } from "./lib/orchestration/batch-stdin.js";
27
+ import { parseBatchCommandArgument, parseUserBatchStdin } from "./lib/orchestration/batch-stdin.js";
27
28
  import { ELECTRON_POST_COMMAND_STATUS_SETTLE_MS, ELECTRON_PROFILE_ISOLATION_DETAILS, cleanupActiveElectronHostLaunches, handleElectronHostInput, restoreElectronLaunchRecordsFromBranch, } from "./lib/orchestration/electron-host/index.js";
28
29
  import { buildValidationFailureResult, resolveAgentBrowserInput } from "./lib/orchestration/input-plan.js";
29
- import { applyAgentBrowserOutputPath, getAgentBrowserOutputPathValidationError } from "./lib/orchestration/output-file.js";
30
- 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";
31
34
  import { canRegisterWebSearchTool, loadAgentBrowserConfigSync } from "./lib/config.js";
35
+ import { appendRecordingReservationTransition, applyRecordingArtifactsToReservations, restoreRecordingReservationStateFromBranch, retireRecordingReservation, } from "./lib/recording-reservations.js";
32
36
  import { createAgentBrowserWebSearchTool } from "./lib/web-search.js";
33
37
  import { isDirectAgentBrowserBashAllowed, isHarmlessAgentBrowserInspectionCommand, looksLikeDirectAgentBrowserBash, } from "./lib/bash-guard.js";
34
38
  import { AgentBrowserResultComponent, buildAgentBrowserToolResultPatch, formatAgentBrowserRenderCall, formatAgentBrowserRenderResult, } from "./lib/pi-tool-rendering.js";
@@ -37,22 +41,94 @@ function isBashToolCallEvent(event) {
37
41
  return false;
38
42
  return typeof event.input.command === "string";
39
43
  }
40
- function getBatchPreflightValidationError(args, stdin) {
41
- const commandTokens = extractCommandTokens(args);
42
- if (commandTokens[0] !== "batch" || stdin === undefined) {
43
- 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);
44
57
  }
45
- const parsed = parseBatchStdinJsonArray(stdin);
46
- if (parsed.error || parsed.steps === undefined) {
47
- 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
+ }
48
82
  }
49
- for (const [index, step] of parsed.steps.entries()) {
50
- if (!Array.isArray(step) || !step.every((token) => typeof token === "string") || step.length === 0)
51
- continue;
52
- const stepValidationError = validateToolArgs(step);
53
- if (stepValidationError)
54
- return `Unsupported batch step ${index + 1}: ${stepValidationError}`;
55
- 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")) {
56
132
  return [
57
133
  `Unsupported batch screenshot annotation in step ${index + 1}: put --annotate in top-level args, not inside the batch step.`,
58
134
  `Use: { "args": ["--annotate", "batch"], "stdin": "[[\\"screenshot\\",\\"/path/to/image.png\\"]]" }`,
@@ -61,6 +137,51 @@ function getBatchPreflightValidationError(args, stdin) {
61
137
  }
62
138
  return undefined;
63
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
+ }
64
185
  function restoreArtifactManifestFromBranch(branch) {
65
186
  let restoredManifest;
66
187
  for (const entry of branch) {
@@ -76,6 +197,12 @@ function restoreArtifactManifestFromBranch(branch) {
76
197
  }
77
198
  return restoredManifest;
78
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
+ }
79
206
  function restoreManagedSessionCompatibilityWorkaroundFromBranch(branch, sessionName, namespace) {
80
207
  let restored;
81
208
  const targetKey = getSessionContextKey(sessionName, namespace);
@@ -88,12 +215,9 @@ function restoreManagedSessionCompatibilityWorkaroundFromBranch(branch, sessionN
88
215
  const details = isRecord(message.details) ? message.details : undefined;
89
216
  if (!details)
90
217
  continue;
91
- const workaround = isRecord(details.compatibilityWorkaround) ? details.compatibilityWorkaround : undefined;
92
218
  if (getSessionContextKey(typeof details.sessionName === "string" ? details.sessionName : undefined, typeof details.namespace === "string" ? details.namespace : undefined) !== targetKey)
93
219
  continue;
94
- const recognizedWorkaround = (workaround?.id === "chatgpt-headless-user-agent" || workaround?.id === "cloudflare-headless-user-agent") && typeof workaround.reason === "string"
95
- ? { id: workaround.id, reason: workaround.reason }
96
- : undefined;
220
+ const recognizedWorkaround = getRecognizedCompatibilityWorkaround(details.compatibilityWorkaround);
97
221
  const succeeded = getSuccessfulToolResult(details, message);
98
222
  const outcome = getManagedSessionOutcome(details);
99
223
  const activeAfterFailure = recognizedWorkaround
@@ -112,6 +236,57 @@ function restoreManagedSessionCompatibilityWorkaroundFromBranch(branch, sessionN
112
236
  }
113
237
  return restored;
114
238
  }
239
+ function restoreManagedSessionHeadedAutosaveDisabledFromBranch(branch, sessionName, namespace) {
240
+ let restored = false;
241
+ const targetKey = getSessionContextKey(sessionName, namespace);
242
+ for (const entry of branch) {
243
+ if (!isRecord(entry) || entry.type !== "message")
244
+ continue;
245
+ const message = isRecord(entry.message) ? entry.message : undefined;
246
+ if (!message || message.toolName !== "agent_browser")
247
+ continue;
248
+ const details = isRecord(message.details) ? message.details : undefined;
249
+ if (!details)
250
+ continue;
251
+ if (getSessionContextKey(typeof details.sessionName === "string" ? details.sessionName : undefined, typeof details.namespace === "string" ? details.namespace : undefined) !== targetKey)
252
+ continue;
253
+ const outcome = getManagedSessionOutcome(details);
254
+ const activeAfterFailure = outcome?.activeAfter === true
255
+ && typeof outcome.currentSessionName === "string"
256
+ && getSessionContextKey(outcome.currentSessionName, typeof outcome.currentSessionNamespace === "string" ? outcome.currentSessionNamespace : undefined) === targetKey;
257
+ if ((getSuccessfulToolResult(details, message) || activeAfterFailure) && typeof details.managedSessionHeadedAutosaveDisabled === "boolean") {
258
+ restored = details.managedSessionHeadedAutosaveDisabled;
259
+ }
260
+ }
261
+ return restored;
262
+ }
263
+ function restoreManagedSessionHeadedAutosaveIntervalFromBranch(branch, sessionName, namespace) {
264
+ let restored;
265
+ const targetKey = getSessionContextKey(sessionName, namespace);
266
+ for (const entry of branch) {
267
+ if (!isRecord(entry) || entry.type !== "message")
268
+ continue;
269
+ const message = isRecord(entry.message) ? entry.message : undefined;
270
+ if (!message || message.toolName !== "agent_browser")
271
+ continue;
272
+ const details = isRecord(message.details) ? message.details : undefined;
273
+ if (!details)
274
+ continue;
275
+ if (getSessionContextKey(typeof details.sessionName === "string" ? details.sessionName : undefined, typeof details.namespace === "string" ? details.namespace : undefined) !== targetKey)
276
+ continue;
277
+ const outcome = getManagedSessionOutcome(details);
278
+ const activeAfterFailure = outcome?.activeAfter === true
279
+ && typeof outcome.currentSessionName === "string"
280
+ && getSessionContextKey(outcome.currentSessionName, typeof outcome.currentSessionNamespace === "string" ? outcome.currentSessionNamespace : undefined) === targetKey;
281
+ if (!getSuccessfulToolResult(details, message) && !activeAfterFailure)
282
+ continue;
283
+ if (typeof details.managedSessionHeadedAutosaveInterval === "string")
284
+ restored = details.managedSessionHeadedAutosaveInterval;
285
+ else if (details.managedSessionHeadedAutosaveDisabled === true)
286
+ restored = "0";
287
+ }
288
+ return restored;
289
+ }
115
290
  function getToolResultArgs(details) {
116
291
  if (Array.isArray(details.args) && details.args.every((arg) => typeof arg === "string"))
117
292
  return details.args;
@@ -119,13 +294,25 @@ function getToolResultArgs(details) {
119
294
  return details.effectiveArgs;
120
295
  return [];
121
296
  }
122
- 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()) {
123
310
  const autoConnectEnv = env.AGENT_BROWSER_AUTO_CONNECT;
124
- return extractCommandTokens(args)[0] === "connect"
311
+ return extractUpstreamCommandTokens(args)[0] === "connect"
125
312
  || hasLaunchScopedFlagToken(args, "--cdp")
126
313
  || hasLaunchScopedFlagToken(args, "--auto-connect")
127
314
  || env.AGENT_BROWSER_CDP !== undefined
128
- || (autoConnectEnv !== undefined && !["", "0", "false", "no"].includes(autoConnectEnv.toLowerCase()));
315
+ || isUpstreamEnvFlagEnabled(autoConnectEnv);
129
316
  }
130
317
  function restoreAttachedSessionKeysFromBranch(branch) {
131
318
  const attachedSessionKeys = new Set();
@@ -140,15 +327,32 @@ function restoreAttachedSessionKeysFromBranch(branch) {
140
327
  continue;
141
328
  const managedSessionOutcome = isRecord(details.managedSessionOutcome) ? details.managedSessionOutcome : undefined;
142
329
  const retainedFailedAttachment = details.attachedBrowserSession === true && managedSessionOutcome?.activeAfter === true;
143
- if (!getSuccessfulToolResult(details, message) && !retainedFailedAttachment)
144
- continue;
330
+ const succeeded = getSuccessfulToolResult(details, message);
331
+ const batchCloseLifecycle = getSuccessfulBatchCloseLifecycle(details.batchSteps);
332
+ const terminalBatchClose = batchCloseLifecycle?.endsClosed === true;
145
333
  const args = getToolResultArgs(details);
334
+ const namespace = typeof details.namespace === "string" ? details.namespace : extractExplicitNamespace(args);
146
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;
147
352
  if (!sessionName)
148
353
  continue;
149
- const namespace = typeof details.namespace === "string" ? details.namespace : extractExplicitNamespace(args);
150
354
  const sessionKey = getSessionContextKey(sessionName, namespace) ?? sessionName;
151
- if (isCloseCommand(extractCommandTokens(args)[0]))
355
+ if ((succeeded && isCloseCommand(extractUpstreamCommandTokens(args)[0])) || terminalBatchClose)
152
356
  attachedSessionKeys.delete(sessionKey);
153
357
  else if (details.attachedBrowserSession === true || isAttachedBrowserInvocation(args, {}))
154
358
  attachedSessionKeys.add(sessionKey);
@@ -167,12 +371,15 @@ function restoreAllowedDomainsBySessionFromBranch(branch) {
167
371
  if (!details)
168
372
  continue;
169
373
  const succeeded = getSuccessfulToolResult(details, message);
374
+ const batchCloseLifecycle = getSuccessfulBatchCloseLifecycle(details.batchSteps);
170
375
  const args = getToolResultArgs(details);
171
- const command = typeof details.command === "string" ? details.command : extractCommandTokens(args)[0];
376
+ const command = typeof details.command === "string" ? details.command : extractUpstreamCommandTokens(args)[0];
172
377
  const sessionName = typeof details.sessionName === "string" ? details.sessionName : undefined;
173
378
  const namespace = typeof details.namespace === "string" ? details.namespace : undefined;
174
379
  const sessionKey = getSessionContextKey(sessionName, namespace);
175
380
  const explicitSessionName = extractExplicitSessionName(args);
381
+ if (detailsReportCloseAllApplied(details, succeeded))
382
+ deleteIdentityKeysInNamespace(restoredPolicies, namespace);
176
383
  const outcome = getManagedSessionOutcome(details);
177
384
  const outcomeSucceeded = outcome?.succeeded === true;
178
385
  const outcomeStatus = typeof outcome?.status === "string" ? outcome.status : undefined;
@@ -189,7 +396,7 @@ function restoreAllowedDomainsBySessionFromBranch(branch) {
189
396
  if (replacedSessionName)
190
397
  restoredPolicies.delete(getSessionContextKey(replacedSessionName, replacedSessionNamespace) ?? replacedSessionName);
191
398
  }
192
- if (succeeded && isCloseCommand(command)) {
399
+ if ((succeeded && isCloseCommand(command)) || batchCloseLifecycle) {
193
400
  const closedSessionName = explicitSessionName ?? sessionName ?? outcomeAttemptedSessionName ?? outcomeCurrentSessionName;
194
401
  if (closedSessionName)
195
402
  restoredPolicies.delete(getSessionContextKey(closedSessionName, namespace) ?? closedSessionName);
@@ -198,13 +405,14 @@ function restoreAllowedDomainsBySessionFromBranch(branch) {
198
405
  const cleanup = isRecord(electron?.cleanup) ? electron.cleanup : undefined;
199
406
  const cleanupResults = Array.isArray(cleanup?.results) ? cleanup.results : [];
200
407
  for (const cleanupResult of cleanupResults) {
201
- for (const closedSessionName of getCleanupResultClosedManagedSessionNames(cleanupResult))
202
- restoredPolicies.delete(closedSessionName);
408
+ for (const identity of getCleanupResultClosedManagedSessionIdentities(cleanupResult, namespace)) {
409
+ restoredPolicies.delete(getSessionContextKey(identity.sessionName, identity.namespace) ?? identity.sessionName);
410
+ }
203
411
  }
204
412
  const outcomeKeepsSessionCurrent = outcome?.activeAfter === true
205
413
  && (outcomeStatus === "created" || outcomeStatus === "replaced" || outcomeStatus === "unchanged")
206
414
  && outcomeCurrentSessionName === sessionName;
207
- const policy = (succeeded || outcomeKeepsSessionCurrent) && sessionKey && !isCloseCommand(command) ? parseAllowedDomainsPolicyFromArgs(args) : undefined;
415
+ const policy = (succeeded || outcomeKeepsSessionCurrent) && sessionKey && !isCloseCommand(command) && batchCloseLifecycle?.endsClosed !== true ? parseAllowedDomainsPolicyFromArgs(args) : undefined;
208
416
  if (policy && sessionKey)
209
417
  restoredPolicies.set(sessionKey, policy);
210
418
  }
@@ -216,7 +424,12 @@ function trackOwnedManagedSession(sessions, sessionName, cwd, options = {}) {
216
424
  const key = getSessionContextKey(sessionName, options.namespace) ?? sessionName;
217
425
  const existing = sessions.get(key);
218
426
  const branchOwned = existing && !existing.branchOwned ? false : options.branchOwned === true;
219
- sessions.set(key, { branchOwned, cwd, namespace: options.namespace, sessionName });
427
+ const compatibilityWorkaround = Object.hasOwn(options, "compatibilityWorkaround")
428
+ ? options.compatibilityWorkaround
429
+ : existing?.compatibilityWorkaround;
430
+ const headedManagedAutosaveDisabled = options.headedManagedAutosaveDisabled ?? existing?.headedManagedAutosaveDisabled;
431
+ const headedManagedAutosaveInterval = options.headedManagedAutosaveInterval ?? existing?.headedManagedAutosaveInterval;
432
+ sessions.set(key, { branchOwned, compatibilityWorkaround, cwd, headedManagedAutosaveDisabled, headedManagedAutosaveInterval, namespace: options.namespace, sessionName });
220
433
  }
221
434
  function untrackOwnedManagedSession(sessions, sessionName, namespace) {
222
435
  if (!sessionName)
@@ -245,16 +458,21 @@ function syncOwnedManagedSessionsFromResult(sessions, result, cwd) {
245
458
  const status = typeof outcome.status === "string" ? outcome.status : undefined;
246
459
  const currentSessionName = typeof outcome.currentSessionName === "string" ? outcome.currentSessionName : undefined;
247
460
  const attemptedSessionName = typeof outcome.attemptedSessionName === "string" ? outcome.attemptedSessionName : undefined;
461
+ const namespace = isRecord(details) && typeof details.namespace === "string" ? details.namespace : undefined;
248
462
  if (outcome.activeAfter === true && (status === "created" || status === "replaced" || status === "unchanged")) {
249
- const namespace = isRecord(details) && typeof details.namespace === "string" ? details.namespace : undefined;
250
- trackOwnedManagedSession(sessions, currentSessionName, cwd, { namespace });
463
+ trackOwnedManagedSession(sessions, currentSessionName, cwd, {
464
+ compatibilityWorkaround: getRecognizedCompatibilityWorkaround(details?.compatibilityWorkaround),
465
+ headedManagedAutosaveDisabled: details?.managedSessionHeadedAutosaveDisabled === true,
466
+ headedManagedAutosaveInterval: typeof details?.managedSessionHeadedAutosaveInterval === "string" ? details.managedSessionHeadedAutosaveInterval : undefined,
467
+ namespace,
468
+ });
251
469
  }
252
470
  if (succeeded && status === "closed") {
253
- untrackOwnedManagedSession(sessions, attemptedSessionName ?? currentSessionName);
471
+ untrackOwnedManagedSession(sessions, attemptedSessionName ?? currentSessionName, namespace);
254
472
  }
255
473
  }
256
- function getTouchedElectronLaunchIds(sessionName, records) {
257
- const record = findElectronLaunchRecordForSession(sessionName, records);
474
+ function getTouchedElectronLaunchIds(sessionName, records, namespace) {
475
+ const record = findElectronLaunchRecordForSession(sessionName, records, namespace);
258
476
  return record ? new Set([record.launchId]) : undefined;
259
477
  }
260
478
  function mergeActiveElectronLaunchRecords(target, source, options = {}) {
@@ -322,10 +540,10 @@ function getElectronHostLaunchRecordsForInput(options) {
322
540
  }
323
541
  return options.branchRecords;
324
542
  }
325
- function getCleanupResultClosedManagedSessionNames(result) {
543
+ function getCleanupResultClosedManagedSessionIdentities(result, fallbackNamespace) {
326
544
  if (!isRecord(result) || !Array.isArray(result.steps))
327
545
  return [];
328
- const closedSessionNames = new Set();
546
+ const identities = new Map();
329
547
  const record = isRecord(result.record) ? result.record : undefined;
330
548
  for (const step of result.steps) {
331
549
  if (!isRecord(step) || step.resource !== "managed-session")
@@ -335,24 +553,29 @@ function getCleanupResultClosedManagedSessionNames(result) {
335
553
  const sessionName = typeof step.sessionName === "string"
336
554
  ? step.sessionName
337
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;
338
559
  if (sessionName)
339
- closedSessionNames.add(sessionName);
560
+ identities.set(getSessionContextKey(sessionName, namespace) ?? sessionName, { namespace, sessionName });
340
561
  }
341
- return [...closedSessionNames];
562
+ return [...identities.values()];
342
563
  }
343
- function getCleanupResultsClosedManagedSessionNames(cleanupResults) {
344
- const closedSessionNames = new Set();
564
+ function getCleanupResultsClosedManagedSessionIdentities(cleanupResults, fallbackNamespace) {
565
+ const identities = new Map();
345
566
  for (const result of cleanupResults) {
346
- for (const sessionName of getCleanupResultClosedManagedSessionNames(result))
347
- closedSessionNames.add(sessionName);
567
+ for (const identity of getCleanupResultClosedManagedSessionIdentities(result, fallbackNamespace)) {
568
+ identities.set(getSessionContextKey(identity.sessionName, identity.namespace) ?? identity.sessionName, identity);
569
+ }
348
570
  }
349
- return [...closedSessionNames];
571
+ return [...identities.values()];
350
572
  }
351
573
  function isElectronLaunchRecord(value) {
352
574
  if (!isRecord(value))
353
575
  return false;
354
576
  return value.version === 1
355
577
  && value.launchedByWrapper === true
578
+ && (value.namespace === undefined || typeof value.namespace === "string")
356
579
  && typeof value.launchId === "string"
357
580
  && typeof value.appName === "string"
358
581
  && typeof value.executablePath === "string"
@@ -382,10 +605,18 @@ function setBranchRankForString(map, value, rank) {
382
605
  if (typeof value === "string" && value.length > 0)
383
606
  map.set(value, rank);
384
607
  }
608
+ function setBranchManagedSessionActive(events, sessionName, namespace, rank) {
609
+ if (typeof sessionName !== "string" || sessionName.length === 0)
610
+ return;
611
+ const key = getSessionContextKey(sessionName, namespace) ?? sessionName;
612
+ events.managedSessionActiveIdentities.set(key, { namespace, sessionName });
613
+ events.managedSessionActiveRanks.set(key, rank);
614
+ }
385
615
  function collectBranchManagedResourceEvents(branch) {
386
616
  const events = {
387
617
  electronLaunchActiveRanks: new Map(),
388
618
  electronLaunchCleanupRanks: new Map(),
619
+ managedSessionActiveIdentities: new Map(),
389
620
  managedSessionActiveRanks: new Map(),
390
621
  managedSessionCloseRanks: new Map(),
391
622
  };
@@ -402,33 +633,43 @@ function collectBranchManagedResourceEvents(branch) {
402
633
  eventRank += 1;
403
634
  const succeeded = getSuccessfulToolResult(details, message);
404
635
  const args = Array.isArray(details.args) && details.args.every((arg) => typeof arg === "string") ? details.args : [];
405
- const command = typeof details.command === "string" ? details.command : extractCommandTokens(args)[0];
636
+ const command = typeof details.command === "string" ? details.command : extractUpstreamCommandTokens(args)[0];
406
637
  const sessionName = typeof details.sessionName === "string" ? details.sessionName : undefined;
407
638
  const namespace = typeof details.namespace === "string" ? details.namespace : undefined;
408
639
  const sessionMode = details.sessionMode === "fresh" || details.sessionMode === "auto" ? details.sessionMode : undefined;
409
640
  const usedImplicitSession = details.usedImplicitSession === true;
410
641
  const explicitSessionName = extractExplicitSessionName(args);
642
+ const batchCloseLifecycle = getSuccessfulBatchCloseLifecycle(details.batchSteps);
643
+ const closeAllApplied = detailsReportCloseAllApplied(details, succeeded);
411
644
  const outcome = getManagedSessionOutcome(details);
412
645
  const outcomeSucceeded = outcome?.succeeded === true;
413
646
  const outcomeStatus = typeof outcome?.status === "string" ? outcome.status : undefined;
414
647
  const outcomeCurrentSessionName = typeof outcome?.currentSessionName === "string" ? outcome.currentSessionName : undefined;
415
648
  const outcomeAttemptedSessionName = typeof outcome?.attemptedSessionName === "string" ? outcome.attemptedSessionName : undefined;
416
- if (outcomeSucceeded && outcome.activeAfter === true && (outcomeStatus === "created" || outcomeStatus === "replaced" || outcomeStatus === "unchanged")) {
417
- setBranchRankForString(events.managedSessionActiveRanks, getSessionContextKey(outcomeCurrentSessionName, namespace), eventRank);
649
+ if (outcome?.activeAfter === true && (outcomeStatus === "created" || outcomeStatus === "replaced" || outcomeStatus === "unchanged")) {
650
+ setBranchManagedSessionActive(events, outcomeCurrentSessionName, namespace, eventRank);
418
651
  }
419
652
  if (outcomeSucceeded && outcomeStatus === "closed") {
420
653
  setBranchRankForString(events.managedSessionCloseRanks, getSessionContextKey(outcomeAttemptedSessionName ?? outcomeCurrentSessionName ?? sessionName, namespace), eventRank);
421
654
  }
422
- if (outcomeSucceeded && outcomeStatus === "replaced") {
655
+ if (outcome && outcomeStatus === "replaced" && outcome.replacedSessionClosed !== false) {
423
656
  const replacedSessionNamespace = typeof outcome.replacedSessionNamespace === "string" ? outcome.replacedSessionNamespace : namespace;
424
657
  setBranchRankForString(events.managedSessionCloseRanks, getSessionContextKey(typeof outcome.replacedSessionName === "string" ? outcome.replacedSessionName : undefined, replacedSessionNamespace), eventRank);
425
658
  }
426
- if (succeeded && !isCloseCommand(command) && sessionName && (usedImplicitSession || sessionMode === "fresh")) {
427
- events.managedSessionActiveRanks.set(getSessionContextKey(sessionName, namespace) ?? sessionName, eventRank);
659
+ if (succeeded && !isCloseCommand(command) && sessionName && (usedImplicitSession || sessionMode === "fresh" || details.managedSessionHeadedAutosaveDisabled === true || typeof details.managedSessionHeadedAutosaveInterval === "string")) {
660
+ setBranchManagedSessionActive(events, sessionName, namespace, eventRank);
428
661
  }
429
662
  if (succeeded && isCloseCommand(command)) {
430
663
  setBranchRankForString(events.managedSessionCloseRanks, getSessionContextKey(explicitSessionName ?? sessionName ?? outcomeAttemptedSessionName ?? outcomeCurrentSessionName, namespace), eventRank);
431
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
+ }
432
673
  const electron = isRecord(details.electron) ? details.electron : undefined;
433
674
  const launch = electron && isElectronLaunchRecord(electron.launch) ? electron.launch : undefined;
434
675
  if (launch && getActiveElectronRecords(new Map([[launch.launchId, launch]])).length > 0) {
@@ -445,8 +686,8 @@ function collectBranchManagedResourceEvents(branch) {
445
686
  if (isRecord(cleanupResult) && isElectronLaunchRecord(cleanupResult.record)) {
446
687
  events.electronLaunchCleanupRanks.set(cleanupResult.record.launchId, eventRank);
447
688
  }
448
- for (const closedSessionName of getCleanupResultClosedManagedSessionNames(cleanupResult)) {
449
- 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);
450
691
  }
451
692
  }
452
693
  }
@@ -465,23 +706,25 @@ function getCleanupResultsPreservedUserDataDirs(cleanupResults) {
465
706
  }
466
707
  return [...userDataDirs];
467
708
  }
468
- function syncElectronCleanupManagedSessions(sessions, cleanupResults) {
469
- for (const sessionName of getCleanupResultsClosedManagedSessionNames(cleanupResults)) {
470
- untrackOwnedManagedSession(sessions, sessionName);
709
+ function syncElectronCleanupManagedSessions(sessions, cleanupResults, fallbackNamespace) {
710
+ for (const identity of getCleanupResultsClosedManagedSessionIdentities(cleanupResults, fallbackNamespace)) {
711
+ untrackOwnedManagedSession(sessions, identity.sessionName, identity.namespace);
471
712
  }
472
713
  }
473
- async function closeOwnedManagedSessionsExcept(sessions, restoreState, keepSessionName, timeoutMs, attachedSessionKeys, keepNamespace) {
714
+ async function closeOwnedManagedSessionsExcept(sessions, restoreState, keepSessionName, timeoutMs, attachedSessionKeys, keepNamespace, onClosed) {
474
715
  const keepKey = getSessionContextKey(keepSessionName, keepNamespace);
475
716
  for (const [key, owner] of [...sessions]) {
476
717
  if (key === keepKey)
477
718
  continue;
478
- const error = await closeManagedSession({ cwd: owner.cwd, namespace: owner.namespace, preserveAttachedBrowserSession: attachedSessionKeys.has(key), restoreState, sessionName: owner.sessionName, timeoutMs });
479
- if (!error)
719
+ const error = await closeManagedSession({ cwd: owner.cwd, headedManagedAutosaveInterval: owner.headedManagedAutosaveInterval, namespace: owner.namespace, preserveAttachedBrowserSession: attachedSessionKeys.has(key), restoreState, sessionName: owner.sessionName, timeoutMs });
720
+ if (!error) {
480
721
  sessions.delete(key);
722
+ onClosed?.(owner);
723
+ }
481
724
  }
482
725
  }
483
- async function closeOwnedManagedSessions(sessions, restoreState, timeoutMs, attachedSessionKeys) {
484
- 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);
485
728
  }
486
729
  function getOffBranchOwnedElectronLaunchRecords(ownedRecords, branchRecords) {
487
730
  const activeBranchLaunchIds = new Set(getActiveElectronRecords(branchRecords).map((record) => record.launchId));
@@ -521,14 +764,19 @@ class AsyncExecutionQueue {
521
764
  })();
522
765
  }
523
766
  }
524
- class KeyedAsyncExecutionQueue {
767
+ export class KeyedAsyncExecutionQueue {
768
+ barriers = new Map();
525
769
  entries = new Map();
526
- async run(key, work) {
770
+ async run(key, namespace, work) {
527
771
  const entry = this.entries.get(key) ?? { queue: new AsyncExecutionQueue(), users: 0 };
772
+ const barrier = this.barriers.get(getAgentBrowserSessionIdentityKey("", namespace)) ?? Promise.resolve();
528
773
  entry.users += 1;
529
774
  this.entries.set(key, entry);
530
775
  try {
531
- return await entry.queue.run(work);
776
+ return await entry.queue.run(async () => {
777
+ await barrier;
778
+ return await work();
779
+ });
532
780
  }
533
781
  finally {
534
782
  entry.users -= 1;
@@ -536,6 +784,29 @@ class KeyedAsyncExecutionQueue {
536
784
  this.entries.delete(key);
537
785
  }
538
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
+ }
539
810
  }
540
811
  function mergeBrowserRunMap(current, initial, updated) {
541
812
  if (updated === initial)
@@ -551,11 +822,19 @@ function mergeBrowserRunMap(current, initial, updated) {
551
822
  }
552
823
  return merged;
553
824
  }
554
- function mergeBrowserRunArtifactManifest(current, initial, updated) {
825
+ export function mergeBrowserRunArtifactManifest(current, initial, updated) {
555
826
  if (!updated || updated === initial)
556
827
  return current;
828
+ if (current === initial)
829
+ return updated;
557
830
  const initialEntries = new Map((initial?.entries ?? []).map((entry) => [getSessionArtifactManifestEntryKey(entry), entry]));
558
- 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);
559
838
  return changedEntries.length === 0
560
839
  ? current
561
840
  : mergeSessionArtifactManifest({
@@ -614,6 +893,8 @@ export default function agentBrowserExtension(pi) {
614
893
  let managedSessionActive = false;
615
894
  let managedSessionBaseName = createImplicitSessionName(undefined, process.cwd(), ephemeralSessionSeed);
616
895
  let managedSessionCompatibilityWorkaround;
896
+ let managedSessionHeadedAutosaveDisabled = false;
897
+ let managedSessionHeadedAutosaveInterval;
617
898
  let managedSessionName = managedSessionBaseName;
618
899
  let managedSessionCwd = process.cwd();
619
900
  let managedSessionNamespace;
@@ -621,6 +902,9 @@ export default function agentBrowserExtension(pi) {
621
902
  let sessionPageState = new SessionPageState();
622
903
  let traceOwners = new Map();
623
904
  let artifactManifest;
905
+ let activeRecordingReservations = new Map();
906
+ let recordingSessionTombstones = new Map();
907
+ let recordingSessionTombstonesToPersist = new Map();
624
908
  let allowedDomainsBySession = new Map();
625
909
  let attachedSessionKeys = new Set();
626
910
  let networkRoutesBySession = new Map();
@@ -631,9 +915,160 @@ export default function agentBrowserExtension(pi) {
631
915
  const managedSessionRestoreState = new ManagedSessionRestoreState();
632
916
  const ownedManagedSessions = new Map();
633
917
  const managedSessionExecutionQueue = new AsyncExecutionQueue();
918
+ const artifactExecutionQueue = new AsyncExecutionQueue();
634
919
  const callerOwnedSessionExecutionQueues = new KeyedAsyncExecutionQueue();
920
+ const activeScriptControllers = new Set();
921
+ const activeScriptExecutions = new Set();
635
922
  let branchRestoreGeneration = 0;
636
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
+ };
637
1072
  const clearSessionScopedBrowserState = (sessionName, namespace) => {
638
1073
  const key = getSessionContextKey(sessionName, namespace) ?? sessionName;
639
1074
  allowedDomainsBySession = new Map(allowedDomainsBySession);
@@ -641,8 +1076,51 @@ export default function agentBrowserExtension(pi) {
641
1076
  attachedSessionKeys.delete(key);
642
1077
  networkRoutesBySession = new Map(networkRoutesBySession);
643
1078
  networkRoutesBySession.delete(key);
1079
+ traceOwners.delete(key);
644
1080
  sessionPageState.clearSession(key);
645
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
+ };
646
1124
  const restoreBranchBackedState = (ctx, options) => {
647
1125
  branchRestoreGeneration += 1;
648
1126
  branchStateGeneration += 1;
@@ -680,18 +1158,39 @@ export default function agentBrowserExtension(pi) {
680
1158
  managedSessionCompatibilityWorkaround = managedSessionActive
681
1159
  ? restoreManagedSessionCompatibilityWorkaroundFromBranch(branch, managedSessionName, managedSessionNamespace)
682
1160
  : undefined;
1161
+ managedSessionHeadedAutosaveDisabled = managedSessionActive
1162
+ && restoreManagedSessionHeadedAutosaveDisabledFromBranch(branch, managedSessionName, managedSessionNamespace);
1163
+ managedSessionHeadedAutosaveInterval = managedSessionActive
1164
+ ? restoreManagedSessionHeadedAutosaveIntervalFromBranch(branch, managedSessionName, managedSessionNamespace)
1165
+ : undefined;
683
1166
  managedSessionCwd = ctx.cwd;
684
1167
  freshSessionOrdinal = nextFreshSessionOrdinal;
685
1168
  sessionPageState = SessionPageState.fromBranch(branch);
686
1169
  traceOwners = new Map();
687
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;
688
1187
  allowedDomainsBySession = restoreAllowedDomainsBySessionFromBranch(branch);
689
1188
  attachedSessionKeys = restoreAttachedSessionKeysFromBranch(branch);
690
1189
  networkRoutesBySession = new Map();
691
1190
  electronLaunchRecords = restoreElectronLaunchRecordsFromBranch(branch);
692
1191
  for (const record of getActiveElectronRecords(electronLaunchRecords)) {
693
1192
  if (record.sessionName)
694
- attachedSessionKeys.add(getSessionContextKey(record.sessionName) ?? record.sessionName);
1193
+ attachedSessionKeys.add(getSessionContextKey(record.sessionName, record.namespace) ?? record.sessionName);
695
1194
  }
696
1195
  if (options.resetRuntimeOwnership) {
697
1196
  ownedManagedSessions.clear();
@@ -704,8 +1203,43 @@ export default function agentBrowserExtension(pi) {
704
1203
  }
705
1204
  removeInactiveOwnedElectronLaunchRecords(ownedElectronLaunchRecords, branchOwnedElectronLaunchIds, electronLaunchRecords, branchResourceEvents.electronLaunchActiveRanks, branchResourceEvents.electronLaunchCleanupRanks);
706
1205
  }
1206
+ for (const [sessionKey, identity] of branchResourceEvents.managedSessionActiveIdentities) {
1207
+ const activeRank = branchResourceEvents.managedSessionActiveRanks.get(sessionKey);
1208
+ const closeRank = branchResourceEvents.managedSessionCloseRanks.get(sessionKey);
1209
+ if (activeRank === undefined || (closeRank !== undefined && closeRank >= activeRank))
1210
+ continue;
1211
+ if (!isRestorableManagedSessionName(identity.sessionName, managedSessionBaseName))
1212
+ continue;
1213
+ trackOwnedManagedSession(ownedManagedSessions, identity.sessionName, ctx.cwd, {
1214
+ branchOwned: true,
1215
+ compatibilityWorkaround: restoreManagedSessionCompatibilityWorkaroundFromBranch(branch, identity.sessionName, identity.namespace),
1216
+ headedManagedAutosaveDisabled: restoreManagedSessionHeadedAutosaveDisabledFromBranch(branch, identity.sessionName, identity.namespace),
1217
+ headedManagedAutosaveInterval: restoreManagedSessionHeadedAutosaveIntervalFromBranch(branch, identity.sessionName, identity.namespace),
1218
+ namespace: identity.namespace,
1219
+ });
1220
+ }
707
1221
  if (restoredState.active) {
708
- trackOwnedManagedSession(ownedManagedSessions, restoredState.sessionName, ctx.cwd, { branchOwned: true, namespace: restoredState.namespace });
1222
+ trackOwnedManagedSession(ownedManagedSessions, restoredState.sessionName, ctx.cwd, {
1223
+ branchOwned: true,
1224
+ compatibilityWorkaround: managedSessionCompatibilityWorkaround,
1225
+ headedManagedAutosaveDisabled: managedSessionHeadedAutosaveDisabled,
1226
+ headedManagedAutosaveInterval: managedSessionHeadedAutosaveInterval,
1227
+ namespace: restoredState.namespace,
1228
+ });
1229
+ }
1230
+ for (const record of getActiveElectronRecords(electronLaunchRecords)) {
1231
+ if (!record.sessionName || !isRestorableManagedSessionName(record.sessionName, managedSessionBaseName))
1232
+ continue;
1233
+ const sessionKey = getSessionContextKey(record.sessionName) ?? record.sessionName;
1234
+ const activeRank = branchResourceEvents.managedSessionActiveRanks.get(sessionKey);
1235
+ const closeRank = branchResourceEvents.managedSessionCloseRanks.get(sessionKey);
1236
+ if (activeRank === undefined || (closeRank !== undefined && closeRank >= activeRank))
1237
+ continue;
1238
+ trackOwnedManagedSession(ownedManagedSessions, record.sessionName, ctx.cwd, {
1239
+ branchOwned: true,
1240
+ headedManagedAutosaveDisabled: restoreManagedSessionHeadedAutosaveDisabledFromBranch(branch, record.sessionName),
1241
+ headedManagedAutosaveInterval: restoreManagedSessionHeadedAutosaveIntervalFromBranch(branch, record.sessionName),
1242
+ });
709
1243
  }
710
1244
  mergeActiveElectronLaunchRecords(ownedElectronLaunchRecords, electronLaunchRecords, {
711
1245
  branchOwnedLaunchIds: branchOwnedElectronLaunchIds,
@@ -743,17 +1277,25 @@ export default function agentBrowserExtension(pi) {
743
1277
  cwd: ctx.cwd,
744
1278
  includeProjectConfig: shouldIncludeProjectConfig(ctx),
745
1279
  }));
1280
+ await artifactExecutionQueue.run(() => managedSessionExecutionQueue.run(() => recoverScriptSessionLeasesWithinQueue(ctx)));
746
1281
  });
747
1282
  pi.on("session_tree", async (_event, ctx) => {
748
- 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 () => {
749
1287
  restoreBranchBackedState(ctx, { resetRuntimeOwnership: false });
750
- });
1288
+ await recoverScriptSessionLeasesWithinQueue(ctx);
1289
+ }));
751
1290
  });
752
1291
  pi.on("session_shutdown", async (event, ctx) => {
1292
+ for (const controller of activeScriptControllers)
1293
+ controller.abort();
1294
+ await Promise.allSettled([...activeScriptExecutions]);
753
1295
  branchRestoreGeneration += 1;
754
1296
  branchStateGeneration += 1;
755
1297
  let preservedElectronProfileDirs = [];
756
- await managedSessionExecutionQueue.run(async () => {
1298
+ await artifactExecutionQueue.run(() => managedSessionExecutionQueue.run(async () => {
757
1299
  const shutdownCwd = ctx?.cwd ?? managedSessionCwd;
758
1300
  const quitting = event?.reason === "quit";
759
1301
  preservedElectronProfileDirs = quitting
@@ -768,6 +1310,7 @@ export default function agentBrowserExtension(pi) {
768
1310
  electronChildProcesses,
769
1311
  electronLaunchRecords: electronRecordsToCleanup,
770
1312
  managedSessionRestoreState,
1313
+ ownedManagedSessions,
771
1314
  timeoutMs: implicitSessionCloseTimeoutMs,
772
1315
  });
773
1316
  preservedElectronProfileDirs = [...new Set([
@@ -775,19 +1318,38 @@ export default function agentBrowserExtension(pi) {
775
1318
  ...getCleanupResultsPreservedUserDataDirs(electronCleanupResults),
776
1319
  ])];
777
1320
  syncElectronCleanupManagedSessions(ownedManagedSessions, electronCleanupResults);
1321
+ for (const identity of getCleanupResultsClosedManagedSessionIdentities(electronCleanupResults))
1322
+ retireRecordingSession(identity.sessionName, identity.namespace);
778
1323
  if (quitting) {
779
- await closeOwnedManagedSessions(ownedManagedSessions, managedSessionRestoreState, implicitSessionCloseTimeoutMs, attachedSessionKeys);
1324
+ await closeOwnedManagedSessions(ownedManagedSessions, managedSessionRestoreState, implicitSessionCloseTimeoutMs, attachedSessionKeys, (owner) => retireRecordingSession(owner.sessionName, owner.namespace));
780
1325
  }
781
1326
  else {
782
- 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));
783
1328
  }
784
- });
1329
+ }));
785
1330
  managedSessionActive = false;
786
1331
  managedSessionCompatibilityWorkaround = undefined;
1332
+ managedSessionHeadedAutosaveDisabled = false;
1333
+ managedSessionHeadedAutosaveInterval = undefined;
787
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
+ }
788
1347
  sessionPageState.reset();
789
1348
  traceOwners = new Map();
790
1349
  artifactManifest = undefined;
1350
+ activeRecordingReservations = new Map();
1351
+ recordingSessionTombstones = new Map();
1352
+ recordingSessionTombstonesToPersist = new Map();
791
1353
  allowedDomainsBySession = new Map();
792
1354
  attachedSessionKeys = new Set();
793
1355
  networkRoutesBySession = new Map();
@@ -836,16 +1398,16 @@ export default function agentBrowserExtension(pi) {
836
1398
  }
837
1399
  });
838
1400
  pi.on("tool_result", async (event) => buildAgentBrowserToolResultPatch(event));
839
- pi.registerTool({
1401
+ const agentBrowserTool = {
840
1402
  name: "agent_browser",
841
1403
  label: "Agent Browser",
842
- 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.",
843
1405
  promptSnippet: "Browse websites, read live docs, click and fill pages, extract browser content, take screenshots, and automate real web workflows.",
844
1406
  promptGuidelines: toolPromptGuidelines,
845
1407
  parameters: AGENT_BROWSER_PARAMS,
846
1408
  renderCall(args, theme, context) {
847
1409
  const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
848
- text.setText(formatAgentBrowserRenderCall(args, theme));
1410
+ text.setText(formatAgentBrowserRenderCall(args, theme, context.expanded));
849
1411
  return text;
850
1412
  },
851
1413
  renderResult(result, options, theme, context) {
@@ -855,11 +1417,11 @@ export default function agentBrowserExtension(pi) {
855
1417
  component.setState(formatAgentBrowserRenderResult(result, options, theme, context.isError), options.expanded, theme);
856
1418
  return component;
857
1419
  },
858
- async execute(_toolCallId, params, signal, onUpdate, ctx) {
1420
+ async execute(toolCallId, params, signal, onUpdate, ctx) {
859
1421
  const promptPolicy = buildPromptPolicy(getLatestUserPrompt(ctx.sessionManager.getBranch()));
860
1422
  const outputPath = isRecord(params) && typeof params.outputPath === "string" ? params.outputPath : undefined;
861
1423
  const resolvedInput = resolveAgentBrowserInput({
862
- getBatchPreflightValidationError,
1424
+ getBatchPreflightValidationError: (args, stdin) => getArtifactPreflightValidationError({ args, cwd: ctx.cwd, outputPath, stdin }),
863
1425
  managedSessionActive,
864
1426
  params,
865
1427
  });
@@ -870,6 +1432,126 @@ export default function agentBrowserExtension(pi) {
870
1432
  if (outputPathValidationError) {
871
1433
  return buildValidationFailureResult({ attemptedKind: resolvedInput.kind, kind: "invalid", redactedArgs: resolvedInput.redactedArgs, status: "invalid", toolArgs: resolvedInput.toolArgs, toolStdin: resolvedInput.toolStdin, validationError: outputPathValidationError });
872
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
+ }
873
1555
  const { toolArgs } = resolvedInput;
874
1556
  const compiledElectron = resolvedInput.kind === "electron" ? resolvedInput.compiledElectron : undefined;
875
1557
  const redactedCompiledElectron = resolvedInput.kind === "electron" ? resolvedInput.redactedCompiledElectron : undefined;
@@ -879,7 +1561,7 @@ export default function agentBrowserExtension(pi) {
879
1561
  compiledElectron,
880
1562
  ownedRecords: ownedElectronLaunchRecords,
881
1563
  });
882
- const electronHostResult = await handleElectronHostInput({
1564
+ let electronHostResult = await handleElectronHostInput({
883
1565
  attachedSessionKeys,
884
1566
  compiledElectron,
885
1567
  cwd: ctx.cwd,
@@ -890,6 +1572,7 @@ export default function agentBrowserExtension(pi) {
890
1572
  managedSessionName,
891
1573
  managedSessionNamespace,
892
1574
  managedSessionRestoreState,
1575
+ ownedManagedSessions,
893
1576
  redactedCompiledElectron,
894
1577
  sessionPageState,
895
1578
  signal,
@@ -910,26 +1593,58 @@ export default function agentBrowserExtension(pi) {
910
1593
  }
911
1594
  replaceWithActiveElectronLaunchRecords(ownedElectronLaunchRecords, electronHostLaunchRecords, branchOwnedElectronLaunchIds, cleanedLaunchIds);
912
1595
  mergeElectronCleanupRecords(electronLaunchRecords, cleanupRecords);
913
- const closedSessionNames = getCleanupResultsClosedManagedSessionNames(cleanupRecords);
914
- syncElectronCleanupManagedSessions(ownedManagedSessions, cleanupRecords);
915
- for (const closedSessionName of closedSessionNames) {
916
- clearSessionScopedBrowserState(closedSessionName);
917
- 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)) {
918
1606
  managedSessionActive = false;
919
1607
  managedSessionCompatibilityWorkaround = undefined;
1608
+ managedSessionHeadedAutosaveDisabled = false;
1609
+ managedSessionHeadedAutosaveInterval = undefined;
920
1610
  managedSessionNamespace = undefined;
921
1611
  freshSessionOrdinal += 1;
922
1612
  managedSessionName = createFreshSessionName(managedSessionBaseName, ephemeralSessionSeed, freshSessionOrdinal);
923
1613
  }
924
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
+ }
925
1625
  }
926
1626
  return electronHostResult;
927
1627
  };
928
- const electronHostResult = shouldSerializeElectronHostInput(compiledElectron)
929
- ? await managedSessionExecutionQueue.run(runElectronHostInput)
930
- : 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();
931
1646
  if (electronHostResult) {
932
- return applyAgentBrowserOutputPath({ cwd: ctx.cwd, outputPath, result: electronHostResult });
1647
+ return compiledElectron?.action === "cleanup" ? electronHostResult : applyUnserializedOutputPath(electronHostResult);
933
1648
  }
934
1649
  const explicitSessionName = extractExplicitSessionName(toolArgs);
935
1650
  const explicitNamespace = extractExplicitNamespace(toolArgs);
@@ -940,8 +1655,11 @@ export default function agentBrowserExtension(pi) {
940
1655
  ownedElectronLaunchRecords,
941
1656
  ownedManagedSessions,
942
1657
  });
1658
+ const callerOwnedSessionNamespace = explicitSessionName
1659
+ ? resolveAgentBrowserNamespace(toolArgs, getAgentBrowserProcessEnvironment().AGENT_BROWSER_NAMESPACE)
1660
+ : undefined;
943
1661
  const callerOwnedSessionQueueKey = !serializeBrowserCommand && explicitSessionName
944
- ? getSessionContextKey(explicitSessionName, resolveAgentBrowserNamespace(toolArgs, process.env.AGENT_BROWSER_NAMESPACE)) ?? explicitSessionName
1662
+ ? getSessionContextKey(explicitSessionName, callerOwnedSessionNamespace) ?? explicitSessionName
945
1663
  : undefined;
946
1664
  const runBrowserCommand = async () => {
947
1665
  const branchRestoreGenerationAtStart = branchRestoreGeneration;
@@ -959,6 +1677,8 @@ export default function agentBrowserExtension(pi) {
959
1677
  managedSessionActive,
960
1678
  managedSessionBaseName,
961
1679
  managedSessionCompatibilityWorkaround,
1680
+ managedSessionHeadedAutosaveDisabled,
1681
+ managedSessionHeadedAutosaveInterval,
962
1682
  managedSessionCwd,
963
1683
  managedSessionName,
964
1684
  managedSessionNamespace,
@@ -997,20 +1717,30 @@ export default function agentBrowserExtension(pi) {
997
1717
  state: browserRunState,
998
1718
  });
999
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);
1000
1727
  if (branchRestoreStillCurrent) {
1001
- const resultDetails = isRecord(result.details) ? result.details : undefined;
1002
- const resultSessionName = typeof resultDetails?.sessionName === "string"
1003
- ? resultDetails.sessionName
1004
- : extractExplicitSessionName(toolArgs);
1005
- const resultNamespace = typeof resultDetails?.namespace === "string"
1006
- ? resultDetails.namespace
1007
- : resolveAgentBrowserNamespace(toolArgs, process.env.AGENT_BROWSER_NAMESPACE);
1728
+ const resultBatchCloseLifecycle = getSuccessfulBatchCloseLifecycle(resultDetails?.batchSteps);
1008
1729
  const resultSessionKey = getSessionContextKey(resultSessionName, resultNamespace) ?? resultSessionName;
1009
1730
  const managedSessionOutcome = isRecord(resultDetails?.managedSessionOutcome) ? resultDetails.managedSessionOutcome : undefined;
1731
+ const closeAllApplied = resultDetails?.closeAllApplied === true;
1010
1732
  const attachedSessionRemainsActive = result.isError !== true
1011
- || (attachedSessionRequested && managedSessionOutcome?.activeAfter === true);
1012
- const closesAttachedSession = result.isError !== true && isCloseCommand(extractCommandTokens(toolArgs)[0]);
1013
- 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)
1014
1744
  attachedSessionKeys.delete(resultSessionKey);
1015
1745
  else if (resultSessionKey && attachedSessionRemainsActive && (attachedSessionRequested || attachedSessionKnown)) {
1016
1746
  attachedSessionKeys.add(resultSessionKey);
@@ -1021,6 +1751,29 @@ export default function agentBrowserExtension(pi) {
1021
1751
  allowedDomainsBySession = mergeBrowserRunMap(allowedDomainsBySession, initialAllowedDomainsBySession, browserRunState.allowedDomainsBySession);
1022
1752
  networkRoutesBySession = mergeBrowserRunMap(networkRoutesBySession, initialNetworkRoutesBySession, browserRunState.networkRoutesBySession);
1023
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
+ }
1024
1777
  if (artifactManifest) {
1025
1778
  result = {
1026
1779
  ...result,
@@ -1037,6 +1790,8 @@ export default function agentBrowserExtension(pi) {
1037
1790
  freshSessionOrdinal = Math.max(freshSessionOrdinal, browserRunState.freshSessionOrdinal);
1038
1791
  managedSessionActive = browserRunState.managedSessionActive;
1039
1792
  managedSessionCompatibilityWorkaround = browserRunState.managedSessionCompatibilityWorkaround;
1793
+ managedSessionHeadedAutosaveDisabled = browserRunState.managedSessionHeadedAutosaveDisabled === true;
1794
+ managedSessionHeadedAutosaveInterval = browserRunState.managedSessionHeadedAutosaveInterval;
1040
1795
  managedSessionCwd = browserRunState.managedSessionCwd;
1041
1796
  managedSessionName = browserRunState.managedSessionName;
1042
1797
  managedSessionNamespace = browserRunState.managedSessionNamespace;
@@ -1047,7 +1802,7 @@ export default function agentBrowserExtension(pi) {
1047
1802
  mergeActiveElectronLaunchRecords(ownedElectronLaunchRecords, electronLaunchRecords, {
1048
1803
  branchOwnedLaunchIds: branchOwnedElectronLaunchIds,
1049
1804
  touchedLaunchIds: !result.isError
1050
- ? getTouchedElectronLaunchIds(explicitSessionName ?? browserRunState.managedSessionName, electronLaunchRecords)
1805
+ ? getTouchedElectronLaunchIds(explicitSessionName ?? browserRunState.managedSessionName, electronLaunchRecords, explicitSessionName ? resolveAgentBrowserNamespace(toolArgs, getAgentBrowserProcessEnvironment().AGENT_BROWSER_NAMESPACE) : browserRunState.managedSessionNamespace)
1051
1806
  : undefined,
1052
1807
  });
1053
1808
  if (serializeBrowserCommand)
@@ -1055,12 +1810,47 @@ export default function agentBrowserExtension(pi) {
1055
1810
  }
1056
1811
  return applyAgentBrowserOutputPath({ cwd: ctx.cwd, outputPath, preserveTextContent: Array.isArray(params.args) && params.args.includes("--json"), result });
1057
1812
  };
1058
- if (serializeBrowserCommand)
1059
- return managedSessionExecutionQueue.run(runBrowserCommand);
1060
- return callerOwnedSessionQueueKey
1061
- ? callerOwnedSessionExecutionQueues.run(callerOwnedSessionQueueKey, runBrowserCommand)
1062
- : 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
+ });
1063
1852
  },
1064
- });
1853
+ };
1854
+ pi.registerTool(agentBrowserTool);
1065
1855
  registerWebSearchToolIfAvailable(agentBrowserConfig);
1066
1856
  }