pi-agent-browser-native 0.8.2 → 0.8.3

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 (32) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +16 -7
  3. package/dist/extensions/agent-browser/index.js +430 -157
  4. package/dist/extensions/agent-browser/lib/browser-journal.js +418 -0
  5. package/dist/extensions/agent-browser/lib/browser-session-conversion.js +381 -0
  6. package/dist/extensions/agent-browser/lib/browser-transcript.js +119 -19
  7. package/dist/extensions/agent-browser/lib/input-modes/job.js +49 -2
  8. package/dist/extensions/agent-browser/lib/input-modes/params.js +32 -35
  9. package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +25 -5
  10. package/dist/extensions/agent-browser/lib/managed-session-restore.js +20 -5
  11. package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +97 -78
  12. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +23 -2
  13. package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +52 -17
  14. package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +1 -1
  15. package/dist/extensions/agent-browser/lib/orchestration/script-mode.js +13 -8
  16. package/dist/extensions/agent-browser/lib/playbook.js +9 -2
  17. package/dist/extensions/agent-browser/lib/prompt-policy.js +0 -14
  18. package/dist/extensions/agent-browser/lib/results/presentation/large-output.js +2 -1
  19. package/dist/extensions/agent-browser/lib/session-page-state.js +95 -157
  20. package/dist/extensions/agent-browser/lib/temp.js +14 -6
  21. package/dist/extensions/agent-browser/lib/tool-surface.js +12 -23
  22. package/dist/extensions/agent-browser/lib/web-search.js +0 -8
  23. package/docs/ARCHITECTURE.md +13 -11
  24. package/docs/COMMAND_REFERENCE.md +9 -7
  25. package/docs/RELEASE.md +3 -3
  26. package/docs/REQUIREMENTS.md +1 -1
  27. package/docs/SESSION_STORAGE.md +60 -0
  28. package/docs/SUPPORT_MATRIX.md +8 -5
  29. package/docs/TOOL_CONTRACT.md +17 -14
  30. package/package.json +9 -5
  31. package/scripts/build.mjs +4 -3
  32. package/scripts/convert-browser-session.mjs +22 -0
@@ -79,41 +79,38 @@ export function createAgentBrowserQaParamsSchema(Type = JsonSchema, StringEnum =
79
79
  export function createAgentBrowserElectronParamsSchema(Type = JsonSchema, StringEnum = localStringEnum) {
80
80
  const common = { outputPath: outputProperties(Type).outputPath };
81
81
  const timeoutMs = Type.Optional(Type.Integer({ minimum: 1 }));
82
- return Type.Unsafe(Type.Union([
83
- Type.Object({
84
- action: StringEnum(["list"]),
85
- query: Type.Optional(Type.String({ description: "Case-insensitive app filter.", minLength: 1 })),
86
- maxResults: Type.Optional(Type.Integer({ description: `Result cap; default ${ELECTRON_DISCOVERY_DEFAULT_MAX_RESULTS}, clamped to ${ELECTRON_DISCOVERY_MAX_RESULTS}.`, minimum: 1 })),
87
- ...common,
88
- }, { additionalProperties: false }),
89
- Type.Object({
90
- action: StringEnum(["launch"]),
91
- appPath: Type.Optional(Type.String({ description: "macOS .app path.", minLength: 1 })),
92
- appName: Type.Optional(Type.String({ description: "Name from agent_browser_electron list.", minLength: 1 })),
93
- bundleId: Type.Optional(Type.String({ minLength: 1 })),
94
- executablePath: Type.Optional(Type.String({ minLength: 1 })),
95
- appArgs: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
96
- handoff: Type.Optional(StringEnum(AGENT_BROWSER_ELECTRON_HANDOFFS)),
97
- targetType: Type.Optional(StringEnum(AGENT_BROWSER_ELECTRON_TARGET_TYPES)),
98
- timeoutMs,
99
- allow: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
100
- deny: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
101
- ...common,
102
- }, { additionalProperties: false }),
103
- Type.Object({
104
- action: StringEnum(["status", "cleanup"]),
105
- launchId: Type.Optional(Type.String({ minLength: 1 })),
106
- all: Type.Optional(Type.Literal(true)),
107
- timeoutMs,
108
- ...common,
109
- }, { additionalProperties: false, not: { required: ["launchId", "all"] } }),
110
- Type.Object({
111
- action: StringEnum(["probe"]),
112
- launchId: Type.Optional(Type.String({ minLength: 1 })),
113
- timeoutMs,
114
- ...common,
115
- }, { additionalProperties: false }),
116
- ]));
82
+ const list = {
83
+ query: Type.Optional(Type.String({ description: "Case-insensitive app filter.", minLength: 1 })),
84
+ maxResults: Type.Optional(Type.Integer({ description: `Result cap; default ${ELECTRON_DISCOVERY_DEFAULT_MAX_RESULTS}, clamped to ${ELECTRON_DISCOVERY_MAX_RESULTS}.`, minimum: 1 })),
85
+ };
86
+ const launch = {
87
+ appPath: Type.Optional(Type.String({ description: "macOS .app path.", minLength: 1 })),
88
+ appName: Type.Optional(Type.String({ description: "Name from agent_browser_electron list.", minLength: 1 })),
89
+ bundleId: Type.Optional(Type.String({ minLength: 1 })),
90
+ executablePath: Type.Optional(Type.String({ minLength: 1 })),
91
+ appArgs: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
92
+ handoff: Type.Optional(StringEnum(AGENT_BROWSER_ELECTRON_HANDOFFS)),
93
+ targetType: Type.Optional(StringEnum(AGENT_BROWSER_ELECTRON_TARGET_TYPES)),
94
+ timeoutMs,
95
+ allow: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
96
+ deny: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
97
+ };
98
+ const probe = { launchId: Type.Optional(Type.String({ minLength: 1 })), timeoutMs };
99
+ const lifecycle = { ...probe, all: Type.Optional(Type.Literal(true)) };
100
+ // Strict providers need an object root; Pi needs root properties to normalize optional nulls.
101
+ // Each branch retains its original action-specific field boundary.
102
+ return Type.Unsafe(Type.Object({
103
+ action: StringEnum(["list", "launch", "status", "cleanup", "probe"]),
104
+ ...list, ...launch, ...lifecycle, ...common,
105
+ }, {
106
+ additionalProperties: false,
107
+ anyOf: [
108
+ Type.Object({ action: StringEnum(["list"]), ...list, ...common }, { additionalProperties: false }),
109
+ Type.Object({ action: StringEnum(["launch"]), ...launch, ...common }, { additionalProperties: false }),
110
+ Type.Object({ action: StringEnum(["status", "cleanup"]), ...lifecycle, ...common }, { additionalProperties: false, not: { required: ["launchId", "all"] } }),
111
+ Type.Object({ action: StringEnum(["probe"]), ...probe, ...common }, { additionalProperties: false }),
112
+ ],
113
+ }));
117
114
  }
118
115
  export function createAgentBrowserSourceParamsSchema(Type = JsonSchema, StringEnum = localStringEnum) {
119
116
  return Type.Unsafe(Type.Object({
@@ -1,5 +1,5 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
- import { createHash, randomUUID } from "node:crypto";
2
+ import { createHash, randomInt, randomUUID } from "node:crypto";
3
3
  import { lstat, mkdir, readFile, readdir, realpath, rename, rm, writeFile } from "node:fs/promises";
4
4
  import { tmpdir } from "node:os";
5
5
  import { basename, dirname, join } from "node:path";
@@ -11,6 +11,9 @@ const POLICY_LOCK_RETRY_MS = 10;
11
11
  const POLICY_LOCK_MAX_BYTES = 1_048_576; // One claim may cover an entire owned-browser cleanup set.
12
12
  const LOCK_OWNER_FILE = "owner.json";
13
13
  const LOCK_TICKET_FILE = "ticket.json";
14
+ const STALE_CLAIM_MIN_AGE_MS = 30 * 60 * 1_000;
15
+ const POLICY_GC_BATCH_SIZE = 8;
16
+ let lastPolicyGcName;
14
17
  function getCoordinationDirectory(platform = process.platform) {
15
18
  if (platform !== "win32") {
16
19
  const uid = typeof process.getuid === "function" ? process.getuid() : undefined;
@@ -175,6 +178,7 @@ function parseOwner(content) {
175
178
  && Number.isSafeInteger(parsed.pid) && (parsed.pid ?? 0) > 0
176
179
  && typeof parsed.startIdentity === "string" && parsed.startIdentity.length > 0
177
180
  && typeof parsed.token === "string" && parsed.token.length > 0
181
+ && !parsed.token.includes("/") && !parsed.token.includes("\\") && !parsed.token.includes("\0")
178
182
  ? parsed
179
183
  : undefined;
180
184
  }
@@ -323,14 +327,30 @@ async function cleanDeadPolicyArtifacts(directory, budget) {
323
327
  catch {
324
328
  return;
325
329
  }
326
- for (const name of names.filter((candidate) => candidate.startsWith(".pi-agent-browser-policy-remove-")
327
- || candidate.includes(".lock-v4.candidate-"))) {
330
+ // ponytail: list/sort all names; stream directories if enumeration becomes the bottleneck.
331
+ const candidates = names.filter((name) => name.startsWith(".pi-agent-browser-policy-remove-")
332
+ || /^\.pi-agent-browser-policy-[a-f0-9]{64}\.lock-v4\.(?:candidate|claim)-/.test(name)).sort();
333
+ if (candidates.length === 0)
334
+ return;
335
+ const start = lastPolicyGcName === undefined ? randomInt(candidates.length)
336
+ : Math.max(0, candidates.findIndex((name) => name > lastPolicyGcName));
337
+ const batch = Array.from({ length: Math.min(POLICY_GC_BATCH_SIZE, candidates.length) }, (_, offset) => candidates[(start + offset) % candidates.length]);
338
+ lastPolicyGcName = batch.at(-1);
339
+ for (const name of batch) {
328
340
  if (budget.signal?.aborted || Date.now() >= (budget.deadline ?? Infinity))
329
341
  return;
330
342
  const path = join(directory, name);
343
+ const published = name.includes(".lock-v4.claim-");
344
+ if (published) {
345
+ const entry = await lstat(path).catch(() => undefined);
346
+ if (!entry || entry.mtimeMs >= Date.now() - STALE_CLAIM_MIN_AGE_MS)
347
+ continue;
348
+ }
331
349
  const claim = await readClaim(path);
332
- if (claim && await ownerAlive(claim.owner, budget) === false)
333
- await rm(path, { force: true, recursive: true }).catch(() => undefined);
350
+ if (!claim || (published && !name.endsWith(`.claim-${claim.owner.token}`)))
351
+ continue;
352
+ if (await ownerAlive(claim.owner, budget) === false)
353
+ await removeClaimOwnedBy(path, claim.owner.token).catch(() => false);
334
354
  }
335
355
  }
336
356
  function claimPrecedes(left, right) {
@@ -4,6 +4,7 @@ import { canonicalizeAgentBrowserNamespace, extractExplicitNamespace, extractExp
4
4
  import { hasLaunchScopedFlagToken, MANAGED_RESTORE_INCOMPATIBLE_BOOLEAN_ENVS, MANAGED_RESTORE_INCOMPATIBLE_ENVS, MANAGED_RESTORE_INCOMPATIBLE_FLAGS, } from "./launch-scoped-flags.js";
5
5
  import { createManagedSessionRestoreKey, ensureManagedSessionRestoreStorageIsSecure, getManagedSessionRestoreScope, getManagedSessionRestoreProtectedStorageEnv, hasManagedSessionRestoreProjectIdentity, resolveManagedSessionRestoreHome, } from "./managed-session-storage.js";
6
6
  import { parseUserBatchStdin } from "./orchestration/batch-stdin.js";
7
+ import { isRecord } from "./parsing.js";
7
8
  import { getAgentBrowserProcessEnvironment } from "./process-environment.js";
8
9
  export { createManagedSessionRestoreKey, ensureManagedSessionRestoreStorageIsSecure, getManagedSessionRestoreScope } from "./managed-session-storage.js";
9
10
  export { pruneOwnedManagedSessionRestoreSnapshots } from "./managed-session-snapshots.js";
@@ -39,10 +40,11 @@ export class ManagedSessionRestoreState {
39
40
  this.#disabled.add(getAgentBrowserSessionIdentityKey(sessionName, namespace));
40
41
  }
41
42
  getDaemonRestoreKey(sessionName, namespace) {
42
- return typeof sessionName === "string" ? this.#daemonRestoreKeys.get(getAgentBrowserSessionIdentityKey(sessionName, namespace)) : undefined;
43
+ return typeof sessionName === "string" ? this.#daemonRestoreKeys.get(getAgentBrowserSessionIdentityKey(sessionName, namespace))?.restoreKey : undefined;
43
44
  }
44
45
  hasDaemonRestoreKey(sessionName, namespace) {
45
- return typeof sessionName === "string" && this.#daemonRestoreKeys.has(getAgentBrowserSessionIdentityKey(sessionName, namespace));
46
+ return typeof sessionName === "string" && this.#daemonRestoreKeys.get(getAgentBrowserSessionIdentityKey(sessionName, namespace))?.restored !== true
47
+ && this.#daemonRestoreKeys.has(getAgentBrowserSessionIdentityKey(sessionName, namespace));
46
48
  }
47
49
  forgetDaemonRestoreKey(sessionName, namespace) {
48
50
  if (sessionName)
@@ -51,9 +53,22 @@ export class ManagedSessionRestoreState {
51
53
  isDisabled(sessionName, namespace) {
52
54
  return typeof sessionName === "string" && this.#disabled.has(getAgentBrowserSessionIdentityKey(sessionName, namespace));
53
55
  }
54
- recordDaemonRestoreKey(sessionName, namespace, restoreKey) {
55
- if (sessionName)
56
- this.#daemonRestoreKeys.set(getAgentBrowserSessionIdentityKey(sessionName, namespace), restoreKey);
56
+ recordDaemonRestoreKey(sessionName, namespace, restoreKey, generation) {
57
+ if (sessionName) {
58
+ const key = getAgentBrowserSessionIdentityKey(sessionName, namespace);
59
+ const previous = this.#daemonRestoreKeys.get(key);
60
+ this.#daemonRestoreKeys.set(key, { restoreKey, generation: generation ?? (previous?.restoreKey === restoreKey ? previous.generation : undefined) });
61
+ }
62
+ }
63
+ getDaemonReceipt(sessionName, namespace) {
64
+ const receipt = sessionName ? this.#daemonRestoreKeys.get(getAgentBrowserSessionIdentityKey(sessionName, namespace)) : undefined;
65
+ return receipt?.generation ? { restoreKey: receipt.restoreKey, generation: receipt.generation } : undefined;
66
+ }
67
+ restoreDaemonReceipt(sessionName, namespace, receipt) {
68
+ if (isRecord(receipt) && typeof receipt.generation === "string" && receipt.generation.length > 0
69
+ && (receipt.restoreKey === null || typeof receipt.restoreKey === "string")) {
70
+ this.#daemonRestoreKeys.set(getAgentBrowserSessionIdentityKey(sessionName, namespace), { restoreKey: receipt.restoreKey, generation: receipt.generation, restored: true });
71
+ }
57
72
  }
58
73
  replace(identities = [], options = {}) {
59
74
  if (!options.preserveDaemonRestoreKeys)
@@ -5,7 +5,8 @@ import { acquireManagedSessionPolicyLock } from "../../managed-session-policy-lo
5
5
  import { pruneOwnedManagedSessionRestoreSnapshots, resolveExplicitAutosaveInterval, withOwnedManagedSessionContext, } from "../../managed-session-restore.js";
6
6
  import { isManagedSessionRestoreKey } from "../../managed-session-storage.js";
7
7
  import { isRecord } from "../../parsing.js";
8
- import { getAgentBrowserProcessEnvironment } from "../../process-environment.js";
8
+ import { readProcessStartIdentity } from "../../process-identity.js";
9
+ import { getAgentBrowserProcessEnvironment, withAgentBrowserProcessEnvironment } from "../../process-environment.js";
9
10
  import { runAgentBrowserProcess, withAttachedBrowserSessionContext } from "../../process.js";
10
11
  import { getAgentBrowserErrorText, parseAgentBrowserEnvelope } from "../../results/envelope.js";
11
12
  import { redactInvocationArgs } from "../../runtime.js";
@@ -43,9 +44,16 @@ export async function inspectManagedSessionDaemon(options) {
43
44
  return { status: "inactive" };
44
45
  if (!isRecord(data.runtime))
45
46
  return { status: "unknown" };
47
+ let generation;
48
+ if (options.includeGeneration && Number.isSafeInteger(data.runtime.backgroundPid) && Number(data.runtime.backgroundPid) > 0
49
+ && typeof data.runtime.socketDir === "string" && data.runtime.browserLaunched !== false) {
50
+ const start = await readProcessStartIdentity(Number(data.runtime.backgroundPid), process.platform, { signal: options.signal, deadline: Date.now() + (options.timeoutMs ?? 5_000) });
51
+ if (start)
52
+ generation = JSON.stringify({ pid: data.runtime.backgroundPid, start, socketDir: data.runtime.socketDir });
53
+ }
46
54
  if (typeof data.runtime.restoreKey === "string" && data.runtime.restoreKey.length > 0)
47
- return { restoreKey: data.runtime.restoreKey, status: "active" };
48
- return data.runtime.restoreKey === null ? { restoreKey: null, status: "active" } : { status: "unknown" };
55
+ return { restoreKey: data.runtime.restoreKey, status: "active", generation };
56
+ return data.runtime.restoreKey === null ? { restoreKey: null, status: "active", generation } : { status: "unknown" };
49
57
  }
50
58
  finally {
51
59
  if (processResult.stdoutSpillPath)
@@ -86,8 +94,10 @@ export async function acquireOwnedManagedSessionDaemonPolicy(options) {
86
94
  };
87
95
  }
88
96
  try {
97
+ const receipt = context.restoreState.getDaemonReceipt(context.sessionName, context.namespace);
89
98
  const daemon = await inspectManagedSessionDaemon({
90
99
  cwd: context.cwd,
100
+ includeGeneration: receipt !== undefined && !context.restoreState.hasDaemonRestoreKey(context.sessionName, context.namespace),
91
101
  headedManagedAutosaveInterval: context.headedManagedAutosaveInterval,
92
102
  namespace: context.namespace,
93
103
  sessionName: context.sessionName,
@@ -95,6 +105,13 @@ export async function acquireOwnedManagedSessionDaemonPolicy(options) {
95
105
  });
96
106
  if (daemon.status === "inactive")
97
107
  context.restoreState.forgetDaemonRestoreKey(context.sessionName, context.namespace);
108
+ if (daemon.status === "active" && receipt && !context.restoreState.hasDaemonRestoreKey(context.sessionName, context.namespace)) {
109
+ if (daemon.generation === receipt.generation && daemon.restoreKey === receipt.restoreKey) {
110
+ context.restoreState.recordDaemonRestoreKey(context.sessionName, context.namespace, daemon.restoreKey, daemon.generation);
111
+ }
112
+ else
113
+ context.restoreState.forgetDaemonRestoreKey(context.sessionName, context.namespace);
114
+ }
98
115
  if (options.mode === "close") {
99
116
  if (daemon.status === "active")
100
117
  context.restoreState.recordDaemonRestoreKey(context.sessionName, context.namespace, daemon.restoreKey);
@@ -141,87 +158,89 @@ export async function acquireOwnedManagedSessionDaemonPolicy(options) {
141
158
  }
142
159
  }
143
160
  export async function closeManagedSession(options) {
144
- const controller = new AbortController();
145
- let phase = "policy coordination";
146
- let phaseStartedAt = Date.now();
147
- let timeoutError;
148
- const timer = setTimeout(() => {
149
- timeoutError = `Managed-session cleanup timed out after ${options.timeoutMs} ms during ${phase} (${Date.now() - phaseStartedAt} ms in this phase).`;
150
- controller.abort();
151
- }, options.timeoutMs);
152
- let stdoutSpillPath;
153
- const closeArgs = [...(options.namespace !== undefined ? ["--namespace", options.namespace] : []), "--session", options.sessionName, "close"];
154
- const policyLock = options.policyLock ?? await acquireManagedSessionPolicyLock({
155
- namespace: options.namespace,
156
- sessionName: options.sessionName,
157
- signal: controller.signal,
158
- timeoutMs: Math.min(options.timeoutMs, 1_000),
159
- });
160
- if (!policyLock) {
161
- clearTimeout(timer);
162
- return timeoutError ?? "Managed-session policy coordination is unavailable or busy; cleanup did not run. Retry after the current operation finishes or repair the private policy-lock directory.";
163
- }
164
- try {
165
- phase = "daemon inspection";
166
- phaseStartedAt = Date.now();
167
- const daemon = await inspectManagedSessionDaemon({
168
- cwd: options.cwd,
169
- headedManagedAutosaveInterval: options.headedManagedAutosaveInterval,
161
+ return withAgentBrowserProcessEnvironment(options.socketDir ? { PI_AGENT_BROWSER_SOCKET_DIR: options.socketDir } : {}, async () => {
162
+ const controller = new AbortController();
163
+ let phase = "policy coordination";
164
+ let phaseStartedAt = Date.now();
165
+ let timeoutError;
166
+ const timer = setTimeout(() => {
167
+ timeoutError = `Managed-session cleanup timed out after ${options.timeoutMs} ms during ${phase} (${Date.now() - phaseStartedAt} ms in this phase).`;
168
+ controller.abort();
169
+ }, options.timeoutMs);
170
+ let stdoutSpillPath;
171
+ const closeArgs = [...(options.namespace !== undefined ? ["--namespace", options.namespace] : []), "--session", options.sessionName, "close"];
172
+ const policyLock = options.policyLock ?? await acquireManagedSessionPolicyLock({
170
173
  namespace: options.namespace,
171
- preserveAttachedBrowserSession: options.preserveAttachedBrowserSession,
172
174
  sessionName: options.sessionName,
173
175
  signal: controller.signal,
174
- timeoutMs: Math.min(options.timeoutMs, 2_000),
175
- });
176
- if (daemon.status === "active")
177
- options.restoreState.recordDaemonRestoreKey(options.sessionName, options.namespace, daemon.restoreKey);
178
- const daemonRestoreKey = options.restoreState.getDaemonRestoreKey(options.sessionName, options.namespace);
179
- const ownedRestoreKey = !options.restoreState.isDisabled(options.sessionName, options.namespace)
180
- && isManagedSessionRestoreKey(daemonRestoreKey) ? daemonRestoreKey : null;
181
- phase = "native close";
182
- phaseStartedAt = Date.now();
183
- const processResult = await runAgentBrowserProcess({
184
- args: closeArgs,
185
- cwd: options.cwd,
186
- env: { AGENT_BROWSER_JSON: "1", ...getHeadedManagedAutosaveEnv(options.headedManagedAutosaveInterval) },
187
- managedSessionRestoreState: options.restoreState,
188
- ownedManagedSession: true,
189
- preserveAttachedBrowserSession: options.preserveAttachedBrowserSession,
190
- signal: controller.signal,
176
+ timeoutMs: Math.min(options.timeoutMs, 1_000),
191
177
  });
192
- clearTimeout(timer);
193
- stdoutSpillPath = processResult.stdoutSpillPath;
194
- if (!processResult.aborted && !processResult.spawnError && processResult.exitCode === 0) {
195
- const parsed = await parseAgentBrowserEnvelope({ stdout: processResult.stdout, stdoutPath: processResult.stdoutSpillPath });
196
- const data = parsed.envelope?.success === true && isRecord(parsed.envelope.data) ? parsed.envelope.data : undefined;
197
- options.restoreState.clear(options.sessionName, options.namespace);
198
- pruneOwnedManagedSessionRestoreSnapshots({
178
+ if (!policyLock) {
179
+ clearTimeout(timer);
180
+ return timeoutError ?? "Managed-session policy coordination is unavailable or busy; cleanup did not run. Retry after the current operation finishes or repair the private policy-lock directory.";
181
+ }
182
+ try {
183
+ phase = "daemon inspection";
184
+ phaseStartedAt = Date.now();
185
+ const daemon = await inspectManagedSessionDaemon({
199
186
  cwd: options.cwd,
187
+ headedManagedAutosaveInterval: options.headedManagedAutosaveInterval,
200
188
  namespace: options.namespace,
201
- restoreKey: ownedRestoreKey,
202
- statePath: typeof data?.statePath === "string" ? data.statePath : undefined,
189
+ preserveAttachedBrowserSession: options.preserveAttachedBrowserSession,
190
+ sessionName: options.sessionName,
191
+ signal: controller.signal,
192
+ timeoutMs: Math.min(options.timeoutMs, 2_000),
193
+ });
194
+ if (daemon.status === "active")
195
+ options.restoreState.recordDaemonRestoreKey(options.sessionName, options.namespace, daemon.restoreKey);
196
+ const daemonRestoreKey = options.restoreState.getDaemonRestoreKey(options.sessionName, options.namespace);
197
+ const ownedRestoreKey = !options.restoreState.isDisabled(options.sessionName, options.namespace)
198
+ && isManagedSessionRestoreKey(daemonRestoreKey) ? daemonRestoreKey : null;
199
+ phase = "native close";
200
+ phaseStartedAt = Date.now();
201
+ const processResult = await runAgentBrowserProcess({
202
+ args: closeArgs,
203
+ cwd: options.cwd,
204
+ env: { AGENT_BROWSER_JSON: "1", ...getHeadedManagedAutosaveEnv(options.headedManagedAutosaveInterval) },
205
+ managedSessionRestoreState: options.restoreState,
206
+ ownedManagedSession: true,
207
+ preserveAttachedBrowserSession: options.preserveAttachedBrowserSession,
208
+ signal: controller.signal,
209
+ });
210
+ clearTimeout(timer);
211
+ stdoutSpillPath = processResult.stdoutSpillPath;
212
+ if (!processResult.aborted && !processResult.spawnError && processResult.exitCode === 0) {
213
+ const parsed = await parseAgentBrowserEnvelope({ stdout: processResult.stdout, stdoutPath: processResult.stdoutSpillPath });
214
+ const data = parsed.envelope?.success === true && isRecord(parsed.envelope.data) ? parsed.envelope.data : undefined;
215
+ options.restoreState.clear(options.sessionName, options.namespace);
216
+ pruneOwnedManagedSessionRestoreSnapshots({
217
+ cwd: options.cwd,
218
+ namespace: options.namespace,
219
+ restoreKey: ownedRestoreKey,
220
+ statePath: typeof data?.statePath === "string" ? data.statePath : undefined,
221
+ });
222
+ }
223
+ return timeoutError ?? getAgentBrowserErrorText({
224
+ aborted: processResult.aborted,
225
+ command: "close",
226
+ effectiveArgs: redactInvocationArgs(closeArgs),
227
+ exitCode: processResult.exitCode,
228
+ plainTextInspection: false,
229
+ spawnError: processResult.spawnError,
230
+ stderr: processResult.stderr,
231
+ timedOut: processResult.timedOut,
232
+ timeoutMs: processResult.timeoutMs,
203
233
  });
204
234
  }
205
- return timeoutError ?? getAgentBrowserErrorText({
206
- aborted: processResult.aborted,
207
- command: "close",
208
- effectiveArgs: redactInvocationArgs(closeArgs),
209
- exitCode: processResult.exitCode,
210
- plainTextInspection: false,
211
- spawnError: processResult.spawnError,
212
- stderr: processResult.stderr,
213
- timedOut: processResult.timedOut,
214
- timeoutMs: processResult.timeoutMs,
215
- });
216
- }
217
- catch (error) {
218
- return timeoutError ?? (error instanceof Error ? error.message : String(error));
219
- }
220
- finally {
221
- clearTimeout(timer);
222
- if (stdoutSpillPath)
223
- await rm(stdoutSpillPath, { force: true }).catch(() => undefined);
224
- if (!options.policyLock)
225
- await policyLock.release();
226
- }
235
+ catch (error) {
236
+ return timeoutError ?? (error instanceof Error ? error.message : String(error));
237
+ }
238
+ finally {
239
+ clearTimeout(timer);
240
+ if (stdoutSpillPath)
241
+ await rm(stdoutSpillPath, { force: true }).catch(() => undefined);
242
+ if (!options.policyLock)
243
+ await policyLock.release();
244
+ }
245
+ });
227
246
  }
@@ -392,6 +392,7 @@ export async function prepareBrowserRun(options) {
392
392
  }) };
393
393
  }
394
394
  electronLaunch = launchResult.value;
395
+ electronLaunch.record.ownerSessionId = options.ctx.sessionManager.getSessionId();
395
396
  runtimeToolArgs = ["connect", electronLaunch.connectArg];
396
397
  runtimeToolStdin = undefined;
397
398
  }
@@ -570,7 +571,18 @@ export async function prepareBrowserRun(options) {
570
571
  const sessionTabPinningReason = priorSessionPageState.pinningReason;
571
572
  let priorRefSnapshotState = priorSessionPageState.refSnapshot;
572
573
  let priorRefSnapshotInvalidation = priorSessionPageState.refSnapshotInvalidation;
573
- const coldManagedSession = !browserIndependent && (managedSessionDaemonInactive || priorSessionPageState.tabReopenPending === true)
574
+ let nativeGenerationChanged = false;
575
+ if (!browserIndependent && !isCloseCommand(executionPlan.commandInfo.command) && priorRefSnapshotState?.refIds.length && sessionStateKey && executionPlan.sessionName) {
576
+ const daemon = await inspectManagedSessionDaemon({ cwd, namespace: executionPlan.namespace, sessionName: executionPlan.sessionName, signal, includeGeneration: true, headedManagedAutosaveInterval: ownedManagedSession?.headedManagedAutosaveInterval ?? (ownedManagedSession?.headedManagedAutosaveDisabled ? "0" : undefined), timeoutMs: params.timeoutMs });
577
+ sessionPageState.bindSnapshotGeneration(sessionStateKey, daemon.status === "active" ? daemon.generation : undefined);
578
+ if (daemon.status !== "active" || daemon.generation === undefined || priorRefSnapshotState.generation !== daemon.generation) {
579
+ nativeGenerationChanged = daemon.status === "active" && daemon.generation !== undefined && priorRefSnapshotState.generation !== undefined && priorRefSnapshotState.generation !== daemon.generation;
580
+ priorRefSnapshotState = undefined;
581
+ priorRefSnapshotInvalidation = buildPageTransitionRefSnapshotInvalidation("The native browser generation changed or could not be verified. Take a new complete snapshot before using refs, even when the URL is unchanged.");
582
+ sessionPageState.applyRefSnapshotInvalidation({ invalidation: priorRefSnapshotInvalidation, sessionName: sessionStateKey, update: options.sessionPageStateUpdate });
583
+ }
584
+ }
585
+ const coldManagedSession = !browserIndependent && (managedSessionDaemonInactive || nativeGenerationChanged || priorSessionPageState.tabReopenPending === true)
574
586
  && recordedOwnedSession !== undefined
575
587
  && sessionTabPinningReason === "restore"
576
588
  && ownedManagedSession?.restoreDecision === "enabled"
@@ -960,8 +972,17 @@ export async function prepareBrowserRun(options) {
960
972
  signal,
961
973
  usedImplicitSession: executionPlan.usedImplicitSession,
962
974
  });
963
- if (snapshotFilter)
975
+ if (snapshotFilter) {
976
+ if (sessionStateKey && executionPlan.sessionName) {
977
+ const daemon = await inspectManagedSessionDaemon({ cwd, namespace: executionPlan.namespace, sessionName: executionPlan.sessionName, signal, includeGeneration: true, headedManagedAutosaveInterval: ownedManagedSession?.headedManagedAutosaveInterval ?? (ownedManagedSession?.headedManagedAutosaveDisabled ? "0" : undefined), timeoutMs: params.timeoutMs });
978
+ sessionPageState.bindSnapshotGeneration(sessionStateKey, daemon.status === "active" ? daemon.generation : undefined);
979
+ if (ownedManagedSession && daemon.status === "active" && daemon.generation)
980
+ state.managedSessionRestoreState.recordDaemonRestoreKey(executionPlan.sessionName, executionPlan.namespace, daemon.restoreKey, daemon.generation);
981
+ if (isRecord(snapshotFilter.result.details))
982
+ snapshotFilter.result.details.refSnapshot = sessionPageState.get(sessionStateKey).refSnapshot;
983
+ }
964
984
  return { kind: "early-result", statePatch: { ...statePatch, artifactManifest: snapshotFilter.artifactManifest ?? statePatch.artifactManifest }, result: snapshotFilter.result };
985
+ }
965
986
  const networkRequestsPageFilter = await tryNetworkRequestsPageFilter({
966
987
  commandTokens,
967
988
  compatibilityWorkaround,
@@ -15,7 +15,7 @@ import { buildToolPresentation } from "../../results/presentation.js";
15
15
  import { compactLargePresentationOutput } from "../../results/presentation/large-output.js";
16
16
  import { extractEnvelopeErrorText, getAgentBrowserErrorText, parseAgentBrowserEnvelope } from "../../results/envelope.js";
17
17
  import { detectConfirmationRequired } from "../../results/confirmation.js";
18
- import { omitUpstreamLifecycle } from "../../results/presentation/common.js";
18
+ import { extractAgentBrowserLifecycle, omitUpstreamLifecycle } from "../../results/presentation/common.js";
19
19
  import { getClipboardWritePayloadCandidates, redactClipboardPermissionEcho, redactClipboardPermissionErrorValue } from "../../results/presentation/errors.js";
20
20
  import { shouldCaptureSemanticActionNavigationSummary } from "../../results/presentation/semantic-action.js";
21
21
  import { buildPageTransitionRefSnapshotInvalidation, commandExplicitlyTargetsAboutBlank, getCommandRefSnapshotInvalidation, deriveSessionTabTarget, extractLatestRefSnapshotStateFromBatchResults, extractRefSnapshotFromData, extractSessionTabTargetFromBatchResults, extractSessionTabTargetFromCommandData, isAboutBlankSessionTabTarget, normalizeSessionTabTarget, } from "../../session-page-state.js";
@@ -23,10 +23,10 @@ import { isRecord } from "../../parsing.js";
23
23
  import { buildReadConfirmationNextActions, nextReadConfirmation } from "../../read-confirmation.js";
24
24
  import { pruneOwnedManagedSessionRestoreSnapshots } from "../../managed-session-restore.js";
25
25
  import { isManagedSessionRestoreKey } from "../../managed-session-storage.js";
26
- import { createFreshSessionName, extractUpstreamCommandTokens, resolveManagedSessionState } from "../../runtime.js";
26
+ import { createFreshSessionName, extractUpstreamCommandTokens, redactInvocationArgs, resolveManagedSessionState } from "../../runtime.js";
27
27
  import { getUpstreamEffectiveBatchSteps } from "../batch-stdin.js";
28
28
  import { closeManagedSession, inspectManagedSessionDaemon } from "./managed-session-daemon-policy.js";
29
- import { applyOpenResultTabCorrection, buildAboutBlankRecoveryHint, buildAboutBlankWarning, buildElectronPostCommandHealthDiagnostic, buildElectronRefFreshnessDiagnostic, buildElectronSessionMismatch, buildManagedSessionOutcome, collectOpenResultTabCorrection, collectSessionTabSelection, collectSessionTabTarget, commandChoosesSessionTabTarget, extractNavigationSummaryFromData, extractStringResultField, findElectronLaunchRecordForSession, formatElectronPostCommandHealthText, formatElectronSessionMismatchText, getSessionContextKey, getStaleRefArgs, mergeNavigationSummaryIntoData, runSessionCommandData, shouldCaptureNavigationSummary, shouldCorrectSessionTabAfterCommand, shouldInspectElectronPostCommandHealth, updateTraceOwnerState, } from "./session-state.js";
29
+ import { applyOpenResultTabCorrection, applyBrowserRunStatePatch, buildAboutBlankRecoveryHint, buildAboutBlankWarning, buildElectronPostCommandHealthDiagnostic, buildElectronRefFreshnessDiagnostic, buildElectronSessionMismatch, buildManagedSessionOutcome, collectOpenResultTabCorrection, collectSessionTabSelection, collectSessionTabTarget, commandChoosesSessionTabTarget, extractNavigationSummaryFromData, extractStringResultField, findElectronLaunchRecordForSession, formatElectronPostCommandHealthText, formatElectronSessionMismatchText, getSessionContextKey, getStaleRefArgs, mergeNavigationSummaryIntoData, runSessionCommandData, shouldCaptureNavigationSummary, shouldCorrectSessionTabAfterCommand, shouldInspectElectronPostCommandHealth, updateTraceOwnerState, } from "./session-state.js";
30
30
  import { collectClickDispatchDiagnostic } from "./click-dispatch.js";
31
31
  import { buildScrollNoopDiagnostic, collectComboboxFocusDiagnostic, collectElectronBroadGetTextScopeDiagnostics, collectElectronHandoff, collectFillVerificationDiagnostic, collectNavigationSummary, collectOverlayBlockerDiagnostic, collectQaAttachedTarget, collectSnapshotOverlayBlockerDiagnostic, collectRecordingDependencyWarning, collectScrollPositionSnapshot, collectSelectorTextVisibilityDiagnostics, collectTimeoutPartialProgress, sleepMs, formatQaAttachedTargetText, getArtifactCleanupGuidance, getEvalResultWarning, getEvalStdinHint, getSourceLookupElectronContext, } from "./diagnostics.js";
32
32
  import { repairScreenshotData } from "./prepare.js";
@@ -617,7 +617,7 @@ export async function processBrowserOutput(input) {
617
617
  networkRoutesBySession.delete(replacedSessionStateKey ?? replacedManagedSessionName);
618
618
  sessionPageState.clearSession(replacedSessionStateKey ?? replacedManagedSessionName);
619
619
  const replacedSessionKey = replacedSessionStateKey ?? replacedManagedSessionName;
620
- const replacedCloseError = await closeManagedSession({ cwd: priorManagedSessionCwd, headedManagedAutosaveInterval: priorManagedSessionHeadedAutosaveInterval, namespace: priorManagedSessionNamespace, preserveAttachedBrowserSession: state.attachedSessionKeys.has(replacedSessionKey), restoreState: state.managedSessionRestoreState, sessionName: replacedManagedSessionName, timeoutMs: implicitSessionCloseTimeoutMs });
620
+ const replacedCloseError = await closeManagedSession({ cwd: priorManagedSessionCwd, headedManagedAutosaveInterval: priorManagedSessionHeadedAutosaveInterval, namespace: priorManagedSessionNamespace, preserveAttachedBrowserSession: state.attachedSessionKeys.has(replacedSessionKey), restoreState: state.managedSessionRestoreState, sessionName: replacedManagedSessionName, socketDir: state.ownedManagedSessions.get(replacedSessionKey)?.socketDir, timeoutMs: implicitSessionCloseTimeoutMs });
621
621
  if (managedSessionOutcome) {
622
622
  managedSessionOutcome = {
623
623
  ...managedSessionOutcome,
@@ -709,6 +709,30 @@ export async function processBrowserOutput(input) {
709
709
  }
710
710
  if (plainTextUpgrade && errorText)
711
711
  presentationEnvelope = { ...presentationEnvelope, success: false, error: errorText };
712
+ const resultRetainsPreparedManagedSession = !managedSessionOutcome || (managedSessionOutcome.activeAfter
713
+ && managedSessionOutcome.attemptedSessionName === managedSessionOutcome.currentSessionName);
714
+ const resultHeadedManagedAutosaveDisabled = prepared.ownedManagedSessionContext?.headedManagedAutosaveDisabled === true
715
+ && resultRetainsPreparedManagedSession
716
+ && !(commandClosesSession && succeeded);
717
+ const resultHeadedManagedAutosaveInterval = resultRetainsPreparedManagedSession && !(commandClosesSession && succeeded)
718
+ ? prepared.ownedManagedSessionContext?.headedManagedAutosaveInterval
719
+ : undefined;
720
+ // Commit native lifecycle facts to the execution state before rendering/export can fail.
721
+ applyBrowserRunStatePatch(state, { artifactManifest, freshSessionOrdinal, managedSessionActive, managedSessionCompatibilityWorkaround, managedSessionHeadedAutosaveDisabled, managedSessionHeadedAutosaveInterval, managedSessionCwd, managedSessionName, managedSessionNamespace, networkRoutesBySession });
722
+ state.observedBrowserEffects = {
723
+ args: prepared.redactedArgs, command: prepared.executionPlan.commandInfo.command, subcommand: prepared.executionPlan.commandInfo.subcommand,
724
+ sessionName: prepared.executionPlan.sessionName, namespace: prepared.executionPlan.namespace, sessionMode: prepared.sessionMode,
725
+ usedImplicitSession: prepared.executionPlan.usedImplicitSession, agentBrowserStarted: processResult.agentBrowserStarted,
726
+ nativeSucceeded: succeeded, managedSessionOutcome, managedSessionCwd: prepared.ownedManagedSessionContext?.cwd ?? managedSessionCwd, compatibilityWorkaround: prepared.compatibilityWorkaround,
727
+ managedSessionHeadedAutosaveDisabled: resultHeadedManagedAutosaveDisabled, managedSessionHeadedAutosaveInterval: resultHeadedManagedAutosaveInterval,
728
+ managedSessionRestoreDisabled: state.managedSessionRestoreState.isDisabled(prepared.executionPlan.sessionName, prepared.executionPlan.namespace),
729
+ closeAllApplied, readConfirmation: readConfirmationEvent ?? prepared.readConfirmation,
730
+ electron: electronLaunchRecord ? { launch: electronLaunchRecord, cleanup: electronFailedConnectCleanup } : undefined,
731
+ batchSteps: Array.isArray(presentationEnvelope?.data) ? presentationEnvelope.data.filter(isRecord).map(row => ({
732
+ command: Array.isArray(row.command) && row.command.every(token => typeof token === "string") ? redactInvocationArgs(row.command) : undefined,
733
+ success: row.success, lifecycle: extractAgentBrowserLifecycle(row.result),
734
+ })) : undefined,
735
+ };
712
736
  let presentation = plainTextInspection ? { artifacts: undefined, batchFailure: undefined, batchSteps: undefined, content: [{ type: "text", text: inspectionText ?? "" }], data: undefined, fullOutputPath: undefined, fullOutputPaths: undefined, imagePath: undefined, imagePaths: undefined, savedFile: undefined, savedFilePath: undefined, summary: `${prepared.redactedArgs.join(" ")} completed` } : recordingStopRecovery && !recordingStopRecovery.batch ? recordingStopRecovery.presentation : await buildToolPresentation({ modelVisible: input.modelVisible, args: prepared.redactedProcessArgs, artifactManifest, artifactMaxUpdatedAtMs: Date.now(), artifactMinUpdatedAtMs: input.artifactRunStartedAtMs, artifactRequest: screenshotArtifactRequest, batchArtifactRequests: batchScreenshotArtifactRequests, commandInfo: prepared.executionPlan.commandInfo, compiledSemanticAction: prepared.compiledSemanticAction, cwd: operationCwd, envelope: presentationEnvelope, errorText, namespace: prepared.executionPlan.namespace, networkRouteDiagnostics, networkRoutes: activeNetworkRoutes, persistentArtifactStore, previousRecordingContactSheetPath: sessionStateKey ? state.activeRecordingReservations?.get(sessionStateKey)?.contactSheetPath : undefined, piCleanupOwnership: sessionStateKey && (state.ownedManagedSessions.has(sessionStateKey) || prepared.executionPlan.managedSessionName !== undefined) ? "wrapper-managed" : "caller-owned", sessionName: prepared.executionPlan.sessionName });
713
737
  if (recordingStopRecovery)
714
738
  presentation = mergeRecordingRecoveryPresentation(presentation, recordingStopRecovery);
@@ -794,13 +818,14 @@ export async function processBrowserOutput(input) {
794
818
  presentation.failureCategory = "qa-failure";
795
819
  presentation.summary = qaPreset.summary;
796
820
  const compactText = buildQaCompactFailureText({
797
- batchStepCount: presentation.batchSteps?.length ?? prepared.compiledQaPreset.steps.length,
798
- checks: prepared.compiledQaPreset.checks,
821
+ causalError: presentation.batchFailure?.failedStep.text,
822
+ executedStepCount: presentation.batchSteps?.length,
799
823
  page: extractQaPageContext({
800
824
  attachedTarget: qaAttachedTarget,
801
825
  batchData: presentationEnvelope?.data,
802
826
  compiled: prepared.compiledQaPreset,
803
827
  }),
828
+ plannedStepCount: prepared.compiledQaPreset.steps.length,
804
829
  qaPreset,
805
830
  });
806
831
  const nonTextContent = presentation.content.filter((item) => item.type !== "text");
@@ -829,7 +854,12 @@ export async function processBrowserOutput(input) {
829
854
  const qaAttachedBannerText = [qaAttachedTargetText, qaAttachedDiagnosticsText].filter((part) => typeof part === "string" && part.length > 0).join("\n");
830
855
  const skipAttachedTargetBanner = qaPreset?.passed && prepared.compiledQaPreset?.checks.attached;
831
856
  if (!skipAttachedTargetBanner && qaAttachedBannerText && presentation.content[0]?.type === "text")
832
- presentation.content[0] = { ...presentation.content[0], text: `${qaAttachedBannerText}\n\n${presentation.content[0].text}` };
857
+ presentation.content[0] = {
858
+ ...presentation.content[0],
859
+ text: qaPreset?.passed === false
860
+ ? `${presentation.content[0].text}\n\n${qaAttachedBannerText}`
861
+ : `${qaAttachedBannerText}\n\n${presentation.content[0].text}`,
862
+ };
833
863
  else if (!skipAttachedTargetBanner && qaAttachedBannerText)
834
864
  presentation.content.unshift({ type: "text", text: qaAttachedBannerText });
835
865
  if (managedSessionOutcome && managedSessionOutcome.succeeded !== succeeded)
@@ -856,19 +886,24 @@ export async function processBrowserOutput(input) {
856
886
  currentRefSnapshot = finalRecoveryState.currentRefSnapshot;
857
887
  currentRefSnapshotInvalidation = finalRecoveryState.currentRefSnapshotInvalidation;
858
888
  const authoritativePageState = sessionStateKey ? sessionPageState.get(sessionStateKey) : undefined;
889
+ const newSnapshot = authoritativePageState?.refSnapshot && authoritativePageState.refSnapshot.snapshotId !== prepared.priorRefSnapshotState?.snapshotId;
890
+ const needsDaemonReceipt = prepared.ownedManagedSessionContext && !prepared.ownedManagedSessionContext.reuseOnly && !commandClosesSession && processResult.agentBrowserStarted
891
+ && (succeeded || managedSessionOutcome?.activeAfter === true && managedSessionOutcome.currentSessionName === prepared.executionPlan.sessionName)
892
+ && !state.managedSessionRestoreState.getDaemonReceipt(prepared.executionPlan.sessionName, prepared.executionPlan.namespace);
893
+ if (sessionStateKey && prepared.executionPlan.sessionName && (newSnapshot || needsDaemonReceipt)) {
894
+ const daemon = await inspectManagedSessionDaemon({ cwd, namespace: prepared.executionPlan.namespace, sessionName: prepared.executionPlan.sessionName, signal, includeGeneration: true, headedManagedAutosaveInterval: prepared.ownedManagedSessionContext?.headedManagedAutosaveInterval ?? (prepared.ownedManagedSessionContext?.headedManagedAutosaveDisabled ? "0" : undefined), timeoutMs: prepared.processTimeoutMs });
895
+ if (newSnapshot) {
896
+ sessionPageState.bindSnapshotGeneration(sessionStateKey, daemon.status === "active" ? daemon.generation : undefined);
897
+ currentRefSnapshot = sessionPageState.get(sessionStateKey).refSnapshot;
898
+ }
899
+ if (prepared.ownedManagedSessionContext && daemon.status === "active" && daemon.generation) {
900
+ state.managedSessionRestoreState.recordDaemonRestoreKey(prepared.executionPlan.sessionName, prepared.executionPlan.namespace, daemon.restoreKey, daemon.generation);
901
+ }
902
+ }
859
903
  if (sessionStateKey)
860
904
  currentSessionTabTarget = authoritativePageState?.tabTarget;
861
905
  const currentSessionTabTargetUnknown = authoritativePageState?.tabTargetUnknown === true ? true : undefined;
862
- const resultRetainsPreparedManagedSession = !managedSessionOutcome || (managedSessionOutcome.activeAfter
863
- && managedSessionOutcome.attemptedSessionName === managedSessionOutcome.currentSessionName);
864
- const resultHeadedManagedAutosaveDisabled = prepared.ownedManagedSessionContext?.headedManagedAutosaveDisabled === true
865
- && !prepared.ownedManagedSessionContext.reuseOnly
866
- && resultRetainsPreparedManagedSession
867
- && !(commandClosesSession && succeeded);
868
- const resultHeadedManagedAutosaveInterval = resultRetainsPreparedManagedSession && !prepared.ownedManagedSessionContext?.reuseOnly && !(commandClosesSession && succeeded)
869
- ? prepared.ownedManagedSessionContext?.headedManagedAutosaveInterval
870
- : undefined;
871
- const result = buildFinalAgentBrowserToolResult({ modelVisible: input.modelVisible, aboutBlankSessionMismatch, artifactCleanup, categoryDetails: finalRecoveryState.categoryDetails, clickDispatchDiagnostic, commandTokens: prepared.commandTokens, comboboxFocusDiagnostic, compiledNetworkSourceLookup: prepared.compiledNetworkSourceLookup, compiledSemanticAction: prepared.compiledSemanticAction, compatibilityWorkaround: prepared.compatibilityWorkaround, currentRefSnapshot, currentRefSnapshotInvalidation, currentSessionTabTarget, currentSessionTabTargetUnknown, electronBroadGetTextScopeDiagnostics, electronFailedConnectCleanup, electronHandoff, electronLaunch: prepared.electronLaunch, electronLaunchRecord, electronLaunchRecords, electronPostCommandHealth, electronProfileIsolationDetails: input.electronProfileIsolationDetails, electronRefFreshnessDiagnostic, electronSessionMismatch, errorText, evalResultWarning, evalStdinHint, exactSensitiveValues: prepared.exactSensitiveValues, executionPlan: prepared.executionPlan, fillVerificationDiagnostic, headedLaunch: prepared.headedLaunch, inspectionText, preserveAttachedBrowserSession: input.preserveAttachedBrowserSession === true, providerLaunch: prepared.providerLaunch, managedSessionHeadedAutosaveDisabled: resultHeadedManagedAutosaveDisabled || undefined, managedSessionHeadedAutosaveInterval: resultHeadedManagedAutosaveInterval, managedSessionOutcome, managedSessionRestoreDisabled: state.managedSessionRestoreState.isDisabled(prepared.executionPlan.sessionName, prepared.executionPlan.namespace), navigationSummary, networkSourceLookup, noActivePageSnapshotFailure: finalRecoveryState.noActivePageSnapshotFailure, openResultTabCorrection, overlayBlockerDiagnostic, parseError, parseFailureOutput, parseSucceeded, plainTextInspection, presentation, presentationEnvelope, priorSessionTabTarget: prepared.priorSessionTabTarget, processResult, qaAttachedTarget, qaPreset, recordingDependencyWarning, redactedArgs: prepared.redactedArgs, redactedCompiledElectron: prepared.redactedCompiledElectron, redactedCompiledJob: prepared.redactedCompiledJob, redactedCompiledNetworkSourceLookup: prepared.redactedCompiledNetworkSourceLookup, redactedCompiledQaPreset: prepared.redactedCompiledQaPreset, redactedCompiledSemanticAction: prepared.redactedCompiledSemanticAction, redactedCompiledSourceLookup: prepared.redactedCompiledSourceLookup, redactedContent, redactedProcessArgs: prepared.redactedProcessArgs, redactedRecoveryHint: prepared.redactedRecoveryHint, resultArtifactManifest, richInputRecoveryDiagnostic: finalRecoveryState.richInputRecoveryDiagnostic, scrollNoopDiagnostic, selectorTextVisibilityDiagnostics, sessionMode: prepared.sessionMode, sessionTabCorrection, sourceLookup, succeeded, timeoutPartialProgress, unsettledWebMcpMutation, userRequestedJson: prepared.userRequestedJson, visibleRefFallbackDiagnostic: finalRecoveryState.visibleRefFallbackDiagnostic, visibleRefFallbackSessionName: finalRecoveryState.visibleRefFallbackSessionName });
906
+ const result = buildFinalAgentBrowserToolResult({ modelVisible: input.modelVisible, aboutBlankSessionMismatch, artifactCleanup, categoryDetails: finalRecoveryState.categoryDetails, clickDispatchDiagnostic, commandTokens: prepared.commandTokens, comboboxFocusDiagnostic, compiledNetworkSourceLookup: prepared.compiledNetworkSourceLookup, compiledSemanticAction: prepared.compiledSemanticAction, compatibilityWorkaround: prepared.compatibilityWorkaround, currentRefSnapshot, currentRefSnapshotInvalidation, currentSessionTabTarget, currentSessionTabTargetUnknown, electronBroadGetTextScopeDiagnostics, electronFailedConnectCleanup, electronHandoff, electronLaunch: prepared.electronLaunch, electronLaunchRecord, electronLaunchRecords, electronPostCommandHealth, electronProfileIsolationDetails: input.electronProfileIsolationDetails, electronRefFreshnessDiagnostic, electronSessionMismatch, errorText, evalResultWarning, evalStdinHint, exactSensitiveValues: prepared.exactSensitiveValues, executionPlan: prepared.executionPlan, fillVerificationDiagnostic, headedLaunch: prepared.headedLaunch, inspectionText, preserveAttachedBrowserSession: input.preserveAttachedBrowserSession === true, providerLaunch: prepared.providerLaunch, managedSessionHeadedAutosaveDisabled: !prepared.ownedManagedSessionContext?.reuseOnly && resultHeadedManagedAutosaveDisabled || undefined, managedSessionHeadedAutosaveInterval: prepared.ownedManagedSessionContext?.reuseOnly ? undefined : resultHeadedManagedAutosaveInterval, managedSessionOutcome, managedSessionRestoreDisabled: state.managedSessionRestoreState.isDisabled(prepared.executionPlan.sessionName, prepared.executionPlan.namespace), navigationSummary, networkSourceLookup, noActivePageSnapshotFailure: finalRecoveryState.noActivePageSnapshotFailure, openResultTabCorrection, overlayBlockerDiagnostic, parseError, parseFailureOutput, parseSucceeded, plainTextInspection, presentation, presentationEnvelope, priorSessionTabTarget: prepared.priorSessionTabTarget, processResult, qaAttachedTarget, qaPreset, recordingDependencyWarning, redactedArgs: prepared.redactedArgs, redactedCompiledElectron: prepared.redactedCompiledElectron, redactedCompiledJob: prepared.redactedCompiledJob, redactedCompiledNetworkSourceLookup: prepared.redactedCompiledNetworkSourceLookup, redactedCompiledQaPreset: prepared.redactedCompiledQaPreset, redactedCompiledSemanticAction: prepared.redactedCompiledSemanticAction, redactedCompiledSourceLookup: prepared.redactedCompiledSourceLookup, redactedContent, redactedProcessArgs: prepared.redactedProcessArgs, redactedRecoveryHint: prepared.redactedRecoveryHint, resultArtifactManifest, richInputRecoveryDiagnostic: finalRecoveryState.richInputRecoveryDiagnostic, scrollNoopDiagnostic, selectorTextVisibilityDiagnostics, sessionMode: prepared.sessionMode, sessionTabCorrection, sourceLookup, succeeded, timeoutPartialProgress, unsettledWebMcpMutation, userRequestedJson: prepared.userRequestedJson, visibleRefFallbackDiagnostic: finalRecoveryState.visibleRefFallbackDiagnostic, visibleRefFallbackSessionName: finalRecoveryState.visibleRefFallbackSessionName });
872
907
  const resultWithCloseAll = closeAllApplied
873
908
  ? { ...result, details: { ...(isRecord(result.details) ? result.details : {}), closeAllApplied: true } }
874
909
  : result;
@@ -603,7 +603,7 @@ export async function cleanupTrackedElectronHostLaunches(options) {
603
603
  const sessionKey = getSessionPageStateKey(record.sessionName, record.namespace) ?? record.sessionName;
604
604
  const managedSessionOwner = sessionKey ? options.ownedManagedSessions.get(sessionKey) : undefined;
605
605
  const managedSessionCloseError = record.sessionName
606
- ? await closeManagedSession({ cwd: options.cwd, headedManagedAutosaveInterval: managedSessionOwner?.headedManagedAutosaveInterval, namespace: record.namespace, preserveAttachedBrowserSession: options.attachedSessionKeys.has(sessionKey ?? record.sessionName), restoreState: options.managedSessionRestoreState, sessionName: record.sessionName, timeoutMs: options.timeoutMs })
606
+ ? await closeManagedSession({ cwd: options.cwd, headedManagedAutosaveInterval: managedSessionOwner?.headedManagedAutosaveInterval, namespace: record.namespace, preserveAttachedBrowserSession: options.attachedSessionKeys.has(sessionKey ?? record.sessionName), restoreState: options.managedSessionRestoreState, sessionName: record.sessionName, socketDir: managedSessionOwner?.socketDir, timeoutMs: options.timeoutMs })
607
607
  : undefined;
608
608
  const managedSessionStep = record.sessionName
609
609
  ? managedSessionCloseError