pi-agent-browser-native 0.6.10 → 0.6.11

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 (45) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +49 -6
  3. package/dist/extensions/agent-browser/index.js +416 -430
  4. package/dist/extensions/agent-browser/lib/argv-grammar.js +1 -1
  5. package/dist/extensions/agent-browser/lib/command-policy.js +41 -2
  6. package/dist/extensions/agent-browser/lib/input-modes/params.js +1 -1
  7. package/dist/extensions/agent-browser/lib/input-modes/script.js +3 -2
  8. package/dist/extensions/agent-browser/lib/managed-session-restore.js +40 -10
  9. package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +3 -0
  10. package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +8 -2
  11. package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +1 -0
  12. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/wait-timeouts.js +3 -2
  13. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +25 -14
  14. package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +56 -10
  15. package/dist/extensions/agent-browser/lib/orchestration/browser-run/recording-recovery.js +161 -0
  16. package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +3 -3
  17. package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +2 -4
  18. package/dist/extensions/agent-browser/lib/orchestration/native-session-defaults.js +68 -0
  19. package/dist/extensions/agent-browser/lib/orchestration/output-file.js +41 -6
  20. package/dist/extensions/agent-browser/lib/page-target-validation.js +9 -5
  21. package/dist/extensions/agent-browser/lib/playbook.js +10 -9
  22. package/dist/extensions/agent-browser/lib/process-environment.js +26 -8
  23. package/dist/extensions/agent-browser/lib/process.js +8 -5
  24. package/dist/extensions/agent-browser/lib/read-confirmation.js +59 -0
  25. package/dist/extensions/agent-browser/lib/recording-reservations.js +11 -1
  26. package/dist/extensions/agent-browser/lib/results/action-recommendations.js +8 -0
  27. package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +6 -5
  28. package/dist/extensions/agent-browser/lib/results/categories.js +4 -2
  29. package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +60 -29
  30. package/dist/extensions/agent-browser/lib/results/presentation/batch.js +19 -8
  31. package/dist/extensions/agent-browser/lib/results/presentation/common.js +0 -20
  32. package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +40 -38
  33. package/dist/extensions/agent-browser/lib/results/presentation/errors.js +1 -0
  34. package/dist/extensions/agent-browser/lib/results/presentation/navigation.js +3 -3
  35. package/dist/extensions/agent-browser/lib/results/presentation.js +38 -9
  36. package/dist/extensions/agent-browser/lib/results/recording.js +50 -0
  37. package/dist/extensions/agent-browser/lib/runtime.js +72 -20
  38. package/dist/extensions/agent-browser/lib/session-page-state.js +23 -7
  39. package/dist/extensions/agent-browser/lib/temp.js +4 -0
  40. package/docs/ARCHITECTURE.md +26 -10
  41. package/docs/COMMAND_REFERENCE.md +35 -15
  42. package/docs/SUPPORT_MATRIX.md +7 -3
  43. package/docs/TOOL_CONTRACT.md +72 -20
  44. package/package.json +1 -1
  45. package/scripts/prepare.mjs +2 -4
@@ -81,7 +81,7 @@ export const COMMAND_VALUE_FLAGS = [
81
81
  export const OPTIONAL_GLOBAL_VALUE_FLAGS = new Set(["--restore"]);
82
82
  export const VALUE_FLAGS = new Set([...GLOBAL_VALUE_FLAGS, ...COMMAND_VALUE_FLAGS]);
83
83
  export const PREVALIDATED_VALUE_FLAGS = new Set(GLOBAL_VALUE_FLAGS);
84
- export const GLOBAL_VALUE_FLAGS_ALLOWING_DASH_VALUE = new Set(["--args"]);
84
+ export const GLOBAL_VALUE_FLAGS_ALLOWING_DASH_VALUE = new Set(["--args", "--session"]);
85
85
  export const GLOBAL_BOOLEAN_FLAGS_WITH_OPTIONAL_VALUES = new Set([
86
86
  "--allow-file-access",
87
87
  "--annotate",
@@ -1,4 +1,5 @@
1
1
  import { hasOnlyBooleanFlags, hasOnlyOptionFlags, isNonFlagToken, stripSessionlessShapeGlobalFlags } from "./argv-grammar.js";
2
+ import { getUpstreamEffectiveBatchSteps } from "./orchestration/batch-stdin.js";
2
3
  const SESSIONLESS_AUTH_SUBCOMMANDS = new Set(["save", "list", "show", "delete", "remove"]);
3
4
  const PLUGIN_SESSIONLESS_SUBCOMMANDS = new Set(["list", "show", "add", "run"]);
4
5
  const EMPTY_BOOLEAN_FLAGS = new Set();
@@ -92,6 +93,44 @@ function isSessionlessCommand(commandTokens) {
92
93
  return isSessionlessStateCommand(normalizedTokens);
93
94
  return false;
94
95
  }
95
- export function needsManagedSession(descriptor) {
96
- return !isSessionlessCommand(descriptor.upstreamCommandTokens);
96
+ // undefined is a valid DOM read; null is invalid native syntax, which must not trigger page helpers.
97
+ export function getExplicitReadUrl(commandTokens) {
98
+ if (commandTokens[0] !== "read")
99
+ return undefined;
100
+ let url;
101
+ let llms = false;
102
+ let outline = false;
103
+ for (let index = 1; index < commandTokens.length; index += 1) {
104
+ const token = commandTokens[index];
105
+ if (["--filter", "--llms", "--timeout"].includes(token)) {
106
+ const value = commandTokens[++index];
107
+ if (value === undefined)
108
+ return null;
109
+ if (token === "--llms") {
110
+ if (!["index", "full"].includes(value))
111
+ return null;
112
+ llms = true;
113
+ }
114
+ if (token === "--timeout" && (!/^\+?\d+$/.test(value) || BigInt(value) === 0n || BigInt(value) > 18446744073709551615n))
115
+ return null;
116
+ }
117
+ else if (["--raw", "--require-md", "--outline", "--json"].includes(token)) {
118
+ if (token === "--outline")
119
+ outline = true;
120
+ }
121
+ else if (token.startsWith("--") || url !== undefined)
122
+ return null;
123
+ else
124
+ url = token;
125
+ }
126
+ return llms && outline ? null : url;
127
+ }
128
+ export function isBrowserIndependentRead(commandTokens, stdin) {
129
+ if (commandTokens[0] !== "batch")
130
+ return getExplicitReadUrl(commandTokens) !== undefined;
131
+ const steps = getUpstreamEffectiveBatchSteps(commandTokens, stdin);
132
+ return steps.length > 0 && steps.every((step) => getExplicitReadUrl(step) !== undefined);
133
+ }
134
+ export function needsManagedSession(descriptor, stdin) {
135
+ return !isSessionlessCommand(descriptor.upstreamCommandTokens) && !isBrowserIndependentRead(descriptor.upstreamCommandTokens, stdin);
97
136
  }
@@ -138,7 +138,7 @@ export function createAgentBrowserParamsSchema(Type = JsonSchema, StringEnum = l
138
138
  outputPath: Type.Optional(Type.String({ description: "Workspace-relative or absolute result-data path; keep it distinct from screenshot, download, recording, and other browser artifact destinations.", minLength: 1 })),
139
139
  timeoutMs: Type.Optional(Type.Integer({ description: "Wrapper timeout in ms; exceed explicit waits. electron.list has no configurable timeout; other Electron actions use electron.timeoutMs.", minimum: 1 })),
140
140
  sessionMode: Type.Optional(StringEnum(["auto", "fresh"], {
141
- description: "auto reuses the managed session; fresh starts one for launch-only flags, then makes it the managed session.",
141
+ description: "Native configured sessions win; otherwise auto reuses the managed session and fresh starts a new managed browser for launch-only flags.",
142
142
  default: DEFAULT_SESSION_MODE,
143
143
  })),
144
144
  }, {
@@ -5,7 +5,7 @@ import { dirname, join } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { parseArgvDescriptor } from "../argv-descriptor.js";
7
7
  import { getFlagName } from "../argv-grammar.js";
8
- import { needsManagedSession } from "../command-policy.js";
8
+ import { isBrowserIndependentRead, needsManagedSession } from "../command-policy.js";
9
9
  import { isCloseCommand } from "../command-taxonomy.js";
10
10
  import { LAUNCH_SCOPED_FLAGS, MANAGED_RESTORE_INCOMPATIBLE_FLAGS } from "../launch-scoped-flags.js";
11
11
  import { isRecord } from "../parsing.js";
@@ -44,6 +44,7 @@ const SCRIPT_FORBIDDEN_FLAGS = new Set([
44
44
  ...MANAGED_RESTORE_INCOMPATIBLE_FLAGS.filter((flag) => flag !== SCRIPT_ALLOWED_LAUNCH_FLAG),
45
45
  "--namespace",
46
46
  "--session",
47
+ "--config",
47
48
  ]);
48
49
  export function compileAgentBrowserScript(input) {
49
50
  if (typeof input !== "string")
@@ -71,7 +72,7 @@ function getScriptCallPolicyError(args) {
71
72
  return "script browser calls cannot close, quit, or exit their isolated session.";
72
73
  if (SCRIPT_FORBIDDEN_COMMANDS.has(command))
73
74
  return `script browser calls cannot use ${command}.`;
74
- if (!needsManagedSession(descriptor))
75
+ if (!needsManagedSession(descriptor) && !isBrowserIndependentRead(descriptor.upstreamCommandTokens))
75
76
  return `script browser calls cannot use sessionless/local command ${command}.`;
76
77
  for (const token of args) {
77
78
  const flag = getFlagName(token);
@@ -192,6 +192,12 @@ export function getOwnedManagedSessionCompatibilityEnv(options) {
192
192
  const explicitRawInterval = Object.hasOwn(callEnv, "AGENT_BROWSER_AUTOSAVE_INTERVAL_MS")
193
193
  ? callEnv.AGENT_BROWSER_AUTOSAVE_INTERVAL_MS
194
194
  : parentEnv.AGENT_BROWSER_AUTOSAVE_INTERVAL_MS;
195
+ if (ownedContext.reuseOnly) {
196
+ return explicitRawInterval === undefined && ownedContext.headedManagedAutosaveInterval !== undefined
197
+ && !agentBrowserExplicitConfigIsPresent({ ...parentEnv, ...callEnv }, options.args)
198
+ ? { AGENT_BROWSER_AUTOSAVE_INTERVAL_MS: ownedContext.headedManagedAutosaveInterval }
199
+ : {};
200
+ }
195
201
  const explicitIntervalMatches = resolveExplicitAutosaveInterval(explicitRawInterval) === ownedContext.headedManagedAutosaveInterval;
196
202
  return {
197
203
  ...(ownedContext.compatibilityUserAgent ? { AGENT_BROWSER_USER_AGENT: ownedContext.compatibilityUserAgent } : {}),
@@ -209,26 +215,33 @@ export function getManagedSessionRestoreProtectedEnv(options, restoreEnv) {
209
215
  }
210
216
  export function validateManagedSessionRestoreContextForSpawn(options) {
211
217
  const { namespace, ownedContext, parentEnv } = resolveManagedSessionRestorePolicy(options);
212
- if (closesBrowserSession(options.args) || ownedContext?.restoreDecision !== "enabled")
218
+ if (closesBrowserSession(options.args) || ownedContext?.restoreDecision !== "enabled" || (ownedContext.reuseOnly && ownedContext.restoreSuppressed))
213
219
  return true;
214
220
  const ownedCwd = ownedContext.cwd ?? options.cwd;
215
221
  if (!ownedContext.restoreKey || !ownedContext.restoreScope || createManagedSessionRestoreKey(ownedCwd, ownedContext.restoreScope) !== ownedContext.restoreKey || !hasManagedSessionRestoreProjectIdentity(ownedCwd))
216
222
  return false;
217
223
  const effectiveEnv = { ...parentEnv, ...options.env };
218
- if (isDisabledEnvFlag(effectiveEnv[MANAGED_SESSION_RESTORE_ENV]))
219
- return false;
220
- if (MANAGED_RESTORE_INCOMPATIBLE_ENVS.some((name) => !MANAGED_SESSION_RESTORE_SPAWN_PINNED_ENVS.has(name) && hasUpstreamEnvValue(effectiveEnv, name)))
221
- return false;
222
- if (MANAGED_RESTORE_INCOMPATIBLE_BOOLEAN_ENVS.some((name) => isUpstreamEnvFlagEnabled(effectiveEnv[name])))
223
- return false;
224
- if (options.env?.[AGENT_BROWSER_RESTORE_ENV] !== undefined && options.env[AGENT_BROWSER_RESTORE_ENV] !== ownedContext.restoreKey)
225
- return false;
224
+ if (!ownedContext.reuseOnly) {
225
+ if (isDisabledEnvFlag(effectiveEnv[MANAGED_SESSION_RESTORE_ENV]))
226
+ return false;
227
+ if (MANAGED_RESTORE_INCOMPATIBLE_ENVS.some((name) => !MANAGED_SESSION_RESTORE_SPAWN_PINNED_ENVS.has(name) && hasUpstreamEnvValue(effectiveEnv, name)))
228
+ return false;
229
+ if (MANAGED_RESTORE_INCOMPATIBLE_BOOLEAN_ENVS.some((name) => isUpstreamEnvFlagEnabled(effectiveEnv[name])))
230
+ return false;
231
+ if (options.env?.[AGENT_BROWSER_RESTORE_ENV] !== undefined && options.env[AGENT_BROWSER_RESTORE_ENV] !== ownedContext.restoreKey)
232
+ return false;
233
+ }
226
234
  return ensureManagedSessionRestoreStorageIsSecure({ ...effectiveEnv, ...ownedContext.protectedStorageEnv }, process.platform, namespace);
227
235
  }
228
236
  export function getManagedSessionRestoreEnv(options) {
229
237
  const { namespace, owned, ownedContext, parentEnv, restoreState, sessionName } = resolveManagedSessionRestorePolicy(options);
230
238
  if (!owned || !restoreState || closesBrowserSession(options.args))
231
239
  return {};
240
+ if (ownedContext?.reuseOnly) {
241
+ return ownedContext.restoreKey && !ownedContext.restoreSuppressed && validateManagedSessionRestoreContextForSpawn(options)
242
+ ? { [AGENT_BROWSER_RESTORE_ENV]: ownedContext.restoreKey }
243
+ : {};
244
+ }
232
245
  if (ownedContext?.restoreDecision) {
233
246
  if (ownedContext.restoreDecision !== "enabled" || restoreState.isDisabled(sessionName, namespace) || !sessionName || !ownedContext.restoreKey || !validateManagedSessionRestoreContextForSpawn(options))
234
247
  return {};
@@ -244,7 +257,7 @@ export function getManagedSessionRestoreEnv(options) {
244
257
  /** Commit sticky suppression only after an owned-context subprocess has actually started. */
245
258
  export function commitManagedSessionRestoreSuppression(options) {
246
259
  const { namespace, owned, ownedContext, parentEnv, restoreState, sessionName } = resolveManagedSessionRestorePolicy(options);
247
- if (!owned || !restoreState || closesBrowserSession(options.args))
260
+ if (!owned || !restoreState || ownedContext?.reuseOnly || closesBrowserSession(options.args))
248
261
  return;
249
262
  if (ownedContext?.restoreDecision) {
250
263
  const alreadyDisabled = restoreState.isDisabled(sessionName, namespace);
@@ -272,6 +285,23 @@ export function buildOwnedManagedSessionRestoreContext(options) {
272
285
  if (!owned)
273
286
  return undefined;
274
287
  const ownedCwd = owned.cwd ?? options.cwd;
288
+ if (options.reuseOnly) {
289
+ const env = { ...(options.parentEnv ?? getAgentBrowserProcessEnvironment()), ...options.env };
290
+ const enabled = !options.restoreState.isDisabled(owned.sessionName, owned.namespace);
291
+ const restoreScope = getManagedSessionRestoreScope(owned.sessionName);
292
+ const knownKey = options.restoreState.getDaemonRestoreKey(owned.sessionName, owned.namespace);
293
+ const restoreKey = knownKey === undefined && enabled && hasManagedSessionRestoreProjectIdentity(ownedCwd)
294
+ ? createManagedSessionRestoreKey(ownedCwd, restoreScope)
295
+ : knownKey;
296
+ return { ...owned, reuseOnly: true, restoreKey: restoreKey ?? undefined, restoreScope,
297
+ protectedStorageEnv: enabled ? getManagedSessionRestoreProtectedStorageEnv(true, env) : undefined,
298
+ restoreDecision: enabled ? "enabled" : undefined,
299
+ restoreSuppressed: managedSessionRestoreOptedOut(options) || agentBrowserExplicitConfigIsPresent(env, options.args)
300
+ || ["--restore", "--session-name"].some(flag => hasLaunchScopedFlagToken(options.args, flag))
301
+ || env.AGENT_BROWSER_RESTORE !== undefined || env.AGENT_BROWSER_SESSION_NAME !== undefined,
302
+ headedManagedAutosaveDisabled: options.headedManagedAutosaveDisabled,
303
+ headedManagedAutosaveInterval: options.headedManagedAutosaveInterval };
304
+ }
275
305
  const policyOptions = {
276
306
  args: options.args,
277
307
  cwd: ownedCwd,
@@ -1,6 +1,7 @@
1
1
  import { stat } from "node:fs/promises";
2
2
  import { isAbsolute, resolve } from "node:path";
3
3
  import { isCloseCommand, isOpenNavigationCommand } from "../../command-taxonomy.js";
4
+ import { isBrowserIndependentRead } from "../../command-policy.js";
4
5
  import { boundElectronProbeString } from "../../electron/cdp.js";
5
6
  import { executableExistsOnPath } from "../../executable-path.js";
6
7
  import { formatSessionArtifactRetentionSummary } from "../../results/artifact-manifest.js";
@@ -859,6 +860,8 @@ function buildTimeoutProgressSteps(options) {
859
860
  };
860
861
  }
861
862
  export async function collectTimeoutPartialProgress(options) {
863
+ if ((options.commandTokens[0] === "session" && options.commandTokens[1] === "info") || isBrowserIndependentRead(options.commandTokens, options.stdin))
864
+ return undefined;
862
865
  const rawSteps = getTimeoutProgressSteps(options.compiledJob, options.commandTokens, options.stdin);
863
866
  const artifacts = await collectTimeoutArtifactEvidence(options.cwd, rawSteps);
864
867
  const urlData = await runSessionCommandData({ args: ["get", "url"], cwd: options.cwd, namespace: options.namespace, sessionName: options.sessionName });
@@ -86,7 +86,7 @@ export function redactRecoveryHint(recoveryHint) {
86
86
  }
87
87
  export function buildJsonVisibleContent(options) {
88
88
  const { error, presentation, succeeded, warnings } = options;
89
- const payload = redactSensitiveValue({ artifacts: presentation.artifacts, data: presentation.data, error, success: succeeded, warnings: warnings && warnings.length > 0 ? warnings : undefined });
89
+ const payload = redactSensitiveValue({ artifacts: presentation.artifacts, data: presentation.data, error, recordingRecovery: presentation.recordingRecovery, readConfirmation: presentation.readConfirmation, success: succeeded, warnings: warnings && warnings.length > 0 ? warnings : undefined });
90
90
  if (isRecord(payload) && isRecord(payload.data) && isRecord(presentation.data) && typeof presentation.data.wsUrl === "string")
91
91
  payload.data.wsUrl = presentation.data.wsUrl;
92
92
  const images = presentation.content.filter((item) => item.type === "image");
@@ -205,6 +205,8 @@ export async function prepareFinalResultRecoveryState(options) {
205
205
  return { categoryDetails, currentRefSnapshot, currentRefSnapshotInvalidation, noActivePageSnapshotFailure, richInputRecoveryDiagnostic, visibleRefFallbackDiagnostic, visibleRefFallbackSessionName };
206
206
  }
207
207
  function buildTimeoutPartialProgressNextActions(options) {
208
+ if (options.executionPlan.commandInfo.command === "session" && options.executionPlan.commandInfo.subcommand === "info")
209
+ return [];
208
210
  const retry = options.timeoutPartialProgress?.retryStep?.retry;
209
211
  const stepIndex = options.timeoutPartialProgress?.retryStep?.index;
210
212
  const freshSessionAbandoned = options.sessionMode === "fresh" && options.timeoutPartialProgress?.liveUrlRecovered !== true;
@@ -271,6 +273,8 @@ function buildDialogTimeoutNextActions(options) {
271
273
  ];
272
274
  }
273
275
  function buildResultNextActions(options) {
276
+ if (options.presentation.recordingRecovery || options.presentation.readConfirmation)
277
+ return options.presentation.nextActions;
274
278
  let nextActions = options.presentation.nextActions ? [...options.presentation.nextActions] : [];
275
279
  const append = (actions) => {
276
280
  if (actions && actions.length > 0)
@@ -371,7 +375,7 @@ function formatReadExecutionText(options, lifecycle) {
371
375
  const source = getReadSource(options);
372
376
  if (!source)
373
377
  return undefined;
374
- return `Read execution: source ${source}; CLI started: ${options.processResult.agentBrowserStarted ? "yes" : "no"}; managed browser lifecycle active: ${lifecycle?.effectiveLaunch.browserLaunched === true ? "yes" : "no"}; managed session outcome: ${options.managedSessionOutcome?.status ?? "not managed"}.`;
378
+ return `Read execution: source ${source}; CLI started: ${options.processResult.agentBrowserStarted ? "yes" : "no"}; reported browserLaunched: ${lifecycle ? String(lifecycle.effectiveLaunch.browserLaunched) : "unknown"}; managed session outcome: ${options.managedSessionOutcome?.status ?? "not managed"}. An HTTP read does not establish shared-browser liveness; use session info for that.`;
375
379
  }
376
380
  function buildBrowserWindowStatus(options, lifecycle) {
377
381
  if (!options.headedLaunch || options.preserveAttachedBrowserSession || options.providerLaunch || !options.succeeded || lifecycle?.effectiveLaunch.browserLaunched !== true || !options.executionPlan.managedSessionName || !options.managedSessionOutcome || !["created", "replaced"].includes(options.managedSessionOutcome.status))
@@ -417,6 +421,8 @@ function buildAgentBrowserResultDetails(options, nextActions) {
417
421
  browserWindow,
418
422
  lifecycle,
419
423
  readSource: getReadSource(options),
424
+ recordingRecovery: options.presentation.recordingRecovery,
425
+ readConfirmation: options.presentation.readConfirmation,
420
426
  aboutBlankSessionMismatch: options.aboutBlankSessionMismatch,
421
427
  electronPostCommandHealth: options.electronPostCommandHealth,
422
428
  electronRefFreshness: options.electronRefFreshnessDiagnostic,
@@ -31,6 +31,7 @@ async function runAgentBrowserToolInContext(options) {
31
31
  const artifactRunStartedAtMs = Date.now();
32
32
  const processResult = await runAgentBrowserProcess({
33
33
  args: prepared.processArgs,
34
+ browserIndependentReadConfirmation: prepared.readConfirmation !== undefined,
34
35
  cwd: options.cwd,
35
36
  env: ownedManagedSession
36
37
  ? { AGENT_BROWSER_IDLE_TIMEOUT_MS: options.implicitSessionIdleTimeoutMs }
@@ -1,5 +1,6 @@
1
1
  import { GLOBAL_BOOLEAN_FLAGS_WITH_OPTIONAL_VALUES, VALUE_FLAGS } from "../../../argv-grammar.js";
2
2
  import { isOpenNavigationCommand } from "../../../command-taxonomy.js";
3
+ import { getExplicitReadUrl, isBrowserIndependentRead } from "../../../command-policy.js";
3
4
  import { getAgentBrowserProcessTimeoutMs } from "../../../process.js";
4
5
  import { getUpstreamEffectiveBatchSteps } from "../../batch-stdin.js";
5
6
  const POSITIONAL_VALUE_FLAGS = new Set([...VALUE_FLAGS, "--llms"]);
@@ -44,10 +45,10 @@ export function findFirstPositionalArgument(commandTokens) {
44
45
  return undefined;
45
46
  }
46
47
  function readUsesActivePageUrl(commandTokens) {
47
- return findFirstPositionalArgument(commandTokens) === undefined && commandTokens.some((token) => token === "--require-md" || token === "--llms" || token.startsWith("--llms="));
48
+ return !isBrowserIndependentRead(commandTokens) && commandTokens.some((token) => token === "--require-md" || token === "--llms" || token.startsWith("--llms="));
48
49
  }
49
50
  function readRequestBudget(commandTokens, activePageUrl) {
50
- const target = findFirstPositionalArgument(commandTokens) ?? activePageUrl;
51
+ const target = getExplicitReadUrl(commandTokens) ?? activePageUrl;
51
52
  if (target === undefined)
52
53
  return 1;
53
54
  let url;
@@ -1,7 +1,8 @@
1
1
  import { copyFile, mkdir } from "node:fs/promises";
2
2
  import { dirname, resolve } from "node:path";
3
- import { getBooleanFlagValue, isUpstreamEnvFlagEnabled, projectUpstreamGlobalFlags } from "../../argv-grammar.js";
3
+ import { extractExplicitSessionName, getBooleanFlagValue, isUpstreamEnvFlagEnabled, projectUpstreamGlobalFlags, resolveAgentBrowserNamespace } from "../../argv-grammar.js";
4
4
  import { isCloseCommand } from "../../command-taxonomy.js";
5
+ import { isBrowserIndependentRead } from "../../command-policy.js";
5
6
  import { cleanupElectronLaunchResources } from "../../electron/cleanup.js";
6
7
  import { launchElectronApp } from "../../electron/launch.js";
7
8
  import { pathExists } from "../../fs-utils.js";
@@ -421,6 +422,8 @@ export async function prepareBrowserRun(options) {
421
422
  } };
422
423
  }
423
424
  const userRequestedJson = runtimeToolArgs.includes("--json");
425
+ const routedReadConfirmation = state.sessionPageState.findReadConfirmation(preparedArgs.args, resolveAgentBrowserNamespace(preparedArgs.args, agentBrowserProcessEnv.AGENT_BROWSER_NAMESPACE));
426
+ const readConfirmation = routedReadConfirmation?.capabilities?.readRequiresConfirmation === true ? routedReadConfirmation : undefined;
424
427
  let executionPlan = buildExecutionPlan(preparedArgs.args, {
425
428
  freshSessionName,
426
429
  managedSessionActive: state.managedSessionActive,
@@ -428,13 +431,14 @@ export async function prepareBrowserRun(options) {
428
431
  managedSessionName: state.managedSessionName,
429
432
  managedSessionNamespace: state.managedSessionNamespace,
430
433
  sessionMode,
434
+ stdin: runtimeToolStdin,
435
+ browserIndependentReadConfirmation: readConfirmation !== undefined,
431
436
  });
432
- const idleTimeoutMismatch = getIdleTimeoutMismatch(preparedArgs.args, options.implicitSessionIdleTimeoutMs);
433
- if (idleTimeoutMismatch)
434
- executionPlan = { ...executionPlan, recoveryHint: undefined, validationError: idleTimeoutMismatch };
437
+ const browserIndependent = readConfirmation !== undefined || isBrowserIndependentRead(extractUpstreamCommandTokens(preparedArgs.args), runtimeToolStdin)
438
+ || (executionPlan.commandInfo.command === "session" && executionPlan.commandInfo.subcommand === "info");
435
439
  const ownedSessionKey = getSessionContextKey(executionPlan.sessionName, executionPlan.namespace);
436
440
  const plannedSessionPageState = sessionPageState.get(ownedSessionKey);
437
- const pageTargetError = getPageTargetValidationError({
441
+ const pageTargetError = readConfirmation ? undefined : getPageTargetValidationError({
438
442
  args: executionPlan.effectiveArgs,
439
443
  currentPageUrl: plannedSessionPageState.tabTarget?.url,
440
444
  pageUrlUnknown: plannedSessionPageState.tabTargetUnknown === true,
@@ -446,13 +450,18 @@ export async function prepareBrowserRun(options) {
446
450
  const targetsCurrentManagedSession = state.managedSessionActive
447
451
  && ownedSessionKey === getSessionContextKey(state.managedSessionName, state.managedSessionNamespace);
448
452
  const targetsOffCurrentOwnedSession = recordedOwnedSession !== undefined && !targetsCurrentManagedSession;
453
+ const idleTimeoutMismatch = !browserIndependent && (executionPlan.managedSessionName || recordedOwnedSession || targetsCurrentManagedSession || (state.managedSessionActive && extractExplicitSessionName(preparedArgs.args) === undefined))
454
+ ? getIdleTimeoutMismatch(preparedArgs.args, options.implicitSessionIdleTimeoutMs)
455
+ : undefined;
456
+ if (idleTimeoutMismatch)
457
+ executionPlan = { ...executionPlan, recoveryHint: undefined, validationError: idleTimeoutMismatch };
449
458
  const offCurrentLaunchScopedFlags = targetsOffCurrentOwnedSession
450
459
  ? executionPlan.startupScopedFlags.filter((flag) => flag !== "--namespace")
451
460
  : [];
452
461
  const offCurrentCompatibilityUpgrade = targetsOffCurrentOwnedSession
453
462
  && executionPlan.compatibilityWorkaround !== undefined
454
463
  && recordedOwnedSession.compatibilityWorkaround === undefined;
455
- if (targetsOffCurrentOwnedSession && canUseHeadlessCompatibilityUserAgent(preparedArgs.args, agentBrowserProcessEnv)) {
464
+ if (!browserIndependent && targetsOffCurrentOwnedSession && canUseHeadlessCompatibilityUserAgent(preparedArgs.args, agentBrowserProcessEnv)) {
456
465
  const compatibilityWorkaround = executionPlan.compatibilityWorkaround ?? recordedOwnedSession.compatibilityWorkaround;
457
466
  if (compatibilityWorkaround) {
458
467
  const userAgentIndex = executionPlan.effectiveArgs.indexOf("--user-agent");
@@ -471,7 +480,7 @@ export async function prepareBrowserRun(options) {
471
480
  ?? (targetsCurrentManagedSession ? state.managedSessionHeadedAutosaveInterval : undefined);
472
481
  const explicitAutosaveInterval = resolveExplicitAutosaveInterval(agentBrowserProcessEnv.AGENT_BROWSER_AUTOSAVE_INTERVAL_MS);
473
482
  const autosavePolicyChangeError = getRunningHeadedAutosavePolicyChangeError(retainedHeadedAutosaveInterval, isCloseCommand(executionPlan.commandInfo.command));
474
- if (!executionPlan.validationError && autosavePolicyChangeError) {
483
+ if (!browserIndependent && !executionPlan.validationError && autosavePolicyChangeError) {
475
484
  executionPlan = { ...executionPlan, recoveryHint: undefined, validationError: autosavePolicyChangeError };
476
485
  }
477
486
  const headedLaunch = getBooleanFlagValue(executionPlan.effectiveArgs, "--headed") ?? isUpstreamEnvFlagEnabled(agentBrowserProcessEnv.AGENT_BROWSER_HEADED);
@@ -481,13 +490,14 @@ export async function prepareBrowserRun(options) {
481
490
  const compatibilityUserAgent = executionPlan.compatibilityWorkaround ? getDefaultHeadlessCompatUserAgent() : undefined;
482
491
  const compatibilityUserAgentApplied = compatibilityUserAgent !== undefined
483
492
  && executionPlan.effectiveArgs.some((token, index) => token === "--user-agent" && executionPlan.effectiveArgs[index + 1] === compatibilityUserAgent);
484
- const ownedManagedSession = buildOwnedManagedSessionRestoreContext({
493
+ const ownedManagedSession = browserIndependent && !recordedOwnedSession && !targetsCurrentManagedSession ? undefined : buildOwnedManagedSessionRestoreContext({
485
494
  args: executionPlan.effectiveArgs,
495
+ reuseOnly: browserIndependent,
486
496
  cwd: recordedOwnedSession?.cwd ?? cwd,
487
497
  currentManagedSessionName: state.managedSessionName,
488
498
  currentManagedSessionNamespace: state.managedSessionNamespace,
489
- headedManagedAutosaveDisabled,
490
- headedManagedAutosaveInterval,
499
+ headedManagedAutosaveDisabled: browserIndependent ? retainedHeadedAutosaveDisabled : headedManagedAutosaveDisabled,
500
+ headedManagedAutosaveInterval: browserIndependent ? retainedHeadedAutosaveInterval : headedManagedAutosaveInterval,
491
501
  managedSessionName: executionPlan.managedSessionName,
492
502
  namespace: executionPlan.namespace,
493
503
  parentEnv: agentBrowserProcessEnv,
@@ -500,7 +510,7 @@ export async function prepareBrowserRun(options) {
500
510
  });
501
511
  let managedSessionDaemonInactive = false;
502
512
  let managedSessionCleanupOnlyReason;
503
- if (!executionPlan.validationError && ownedManagedSession) {
513
+ if (!browserIndependent && !executionPlan.validationError && ownedManagedSession) {
504
514
  const closeCommand = isCloseCommand(executionPlan.commandInfo.command);
505
515
  const policy = await acquireOwnedManagedSessionDaemonPolicy({
506
516
  context: ownedManagedSession,
@@ -550,7 +560,7 @@ export async function prepareBrowserRun(options) {
550
560
  const sessionTabPinningReason = priorSessionPageState.pinningReason;
551
561
  let priorRefSnapshotState = priorSessionPageState.refSnapshot;
552
562
  let priorRefSnapshotInvalidation = priorSessionPageState.refSnapshotInvalidation;
553
- const coldManagedSession = (managedSessionDaemonInactive || priorSessionPageState.tabReopenPending === true)
563
+ const coldManagedSession = !browserIndependent && (managedSessionDaemonInactive || priorSessionPageState.tabReopenPending === true)
554
564
  && recordedOwnedSession !== undefined
555
565
  && sessionTabPinningReason === "restore"
556
566
  && ownedManagedSession?.restoreDecision === "enabled"
@@ -573,7 +583,7 @@ export async function prepareBrowserRun(options) {
573
583
  command: executionPlan.commandInfo.command,
574
584
  commandTokens: plannedCommandTokens,
575
585
  // URL QA clears diagnostics before its explicit open; those clears do not need the old tab.
576
- pinningRequired: sessionTabPinningReason !== undefined && compiledQaPreset?.checks.url === undefined,
586
+ pinningRequired: !readConfirmation && sessionTabPinningReason !== undefined && compiledQaPreset?.checks.url === undefined,
577
587
  reopenPending: coldManagedSession,
578
588
  sessionName: executionPlan.sessionName,
579
589
  stdin: runtimeToolStdin,
@@ -613,7 +623,7 @@ export async function prepareBrowserRun(options) {
613
623
  const isCallerOwnedExplicitSession = () => executionPlan.sessionName !== undefined
614
624
  && executionPlan.usedImplicitSession === false
615
625
  && ownedManagedSession === undefined;
616
- const requiresLivePageVerification = () => isCallerOwnedExplicitSession() || options.preserveAttachedBrowserSession === true;
626
+ const requiresLivePageVerification = () => !readConfirmation && (isCallerOwnedExplicitSession() || options.preserveAttachedBrowserSession === true);
617
627
  const verifyLivePage = async (request) => {
618
628
  if (!request.requirement || !executionPlan.sessionName)
619
629
  return;
@@ -1050,6 +1060,7 @@ export async function prepareBrowserRun(options) {
1050
1060
  executionPlan,
1051
1061
  ownedManagedSessionContext: ownedManagedSession,
1052
1062
  preparedArgs,
1063
+ readConfirmation,
1053
1064
  priorRefSnapshotState,
1054
1065
  priorSessionTabTarget,
1055
1066
  priorSessionTabTargetUnknown,
@@ -1,6 +1,6 @@
1
1
  import { rm } from "node:fs/promises";
2
2
  import { parseArgvDescriptor } from "../../argv-descriptor.js";
3
- import { needsManagedSession } from "../../command-policy.js";
3
+ import { isBrowserIndependentRead, needsManagedSession } from "../../command-policy.js";
4
4
  import { deleteIdentityKeysInNamespace, getAgentBrowserSessionIdentityKey, isAgentBrowserSessionIdentityKeyInNamespace } from "../../argv-grammar.js";
5
5
  import { batchHasSuccessfulCloseAll, getSuccessfulBatchCloseLifecycle } from "../../batch-lifecycle.js";
6
6
  import { isCloseAllCommand, isCloseCommand, isOpenNavigationCommand, isRecordPageTransitionCommand, isUnverifiedPageTransitionCommand, isWindowOrDiffPageTransitionCommand } from "../../command-taxonomy.js";
@@ -13,11 +13,13 @@ import { applyNetworkRouteRecords, buildNetworkRouteDiagnostics } from "../../re
13
13
  import { buildToolPresentation } from "../../results/presentation.js";
14
14
  import { compactLargePresentationOutput } from "../../results/presentation/large-output.js";
15
15
  import { extractEnvelopeErrorText, getAgentBrowserErrorText, parseAgentBrowserEnvelope } from "../../results/envelope.js";
16
+ import { detectConfirmationRequired } from "../../results/confirmation.js";
16
17
  import { omitUpstreamLifecycle } from "../../results/presentation/common.js";
17
18
  import { getClipboardWritePayloadCandidates, redactClipboardPermissionEcho, redactClipboardPermissionErrorValue } from "../../results/presentation/errors.js";
18
19
  import { shouldCaptureSemanticActionNavigationSummary } from "../../results/presentation/semantic-action.js";
19
20
  import { buildPageTransitionRefSnapshotInvalidation, commandExplicitlyTargetsAboutBlank, getCommandRefSnapshotInvalidation, deriveSessionTabTarget, extractLatestRefSnapshotStateFromBatchResults, extractRefSnapshotFromData, extractSessionTabTargetFromBatchResults, extractSessionTabTargetFromCommandData, isAboutBlankSessionTabTarget, normalizeSessionTabTarget, } from "../../session-page-state.js";
20
21
  import { isRecord } from "../../parsing.js";
22
+ import { buildReadConfirmationNextActions, nextReadConfirmation } from "../../read-confirmation.js";
21
23
  import { pruneOwnedManagedSessionRestoreSnapshots } from "../../managed-session-restore.js";
22
24
  import { isManagedSessionRestoreKey } from "../../managed-session-storage.js";
23
25
  import { createFreshSessionName, extractUpstreamCommandTokens, resolveManagedSessionState } from "../../runtime.js";
@@ -27,6 +29,7 @@ import { applyOpenResultTabCorrection, buildAboutBlankRecoveryHint, buildAboutBl
27
29
  import { collectClickDispatchDiagnostic } from "./click-dispatch.js";
28
30
  import { buildScrollNoopDiagnostic, collectComboboxFocusDiagnostic, collectElectronBroadGetTextScopeDiagnostics, collectElectronHandoff, collectFillVerificationDiagnostic, collectNavigationSummary, collectOverlayBlockerDiagnostic, collectQaAttachedTarget, collectSnapshotOverlayBlockerDiagnostic, collectRecordingDependencyWarning, collectScrollPositionSnapshot, collectSelectorTextVisibilityDiagnostics, collectTimeoutPartialProgress, sleepMs, formatQaAttachedTargetText, getArtifactCleanupGuidance, getEvalResultWarning, getEvalStdinHint, getSourceLookupElectronContext, } from "./diagnostics.js";
29
31
  import { repairScreenshotData } from "./prepare.js";
32
+ import { mergeRecordingRecoveryPresentation, recoverRecordingStop } from "./recording-recovery.js";
30
33
  import { getPersistentSessionArtifactStore } from "./session-state.js";
31
34
  import { buildFinalAgentBrowserToolResult, buildRedactedPresentationContent, buildWrapperRecoveryHint, prepareFinalResultRecoveryState, redactExactSensitiveValue, } from "./final-result.js";
32
35
  async function repairScreenshotArtifact(options) {
@@ -161,7 +164,13 @@ export async function processBrowserOutput(input) {
161
164
  const plainTextUpgrade = !prepared.executionPlan.plainTextInspection && prepared.executionPlan.commandInfo.command === "upgrade" && !needsManagedSession(parseArgvDescriptor(prepared.runtimeToolArgs));
162
165
  const parsed = await parseAgentBrowserEnvelope({ stdout: processResult.stdout, stdoutPath: processResult.stdoutSpillPath, plainText: plainTextUpgrade });
163
166
  let parseError = parsed.parseError;
164
- let presentationEnvelope = parsed.envelope;
167
+ const recordingStopRecovery = await recoverRecordingStop({
168
+ artifactManifest, artifactRunStartedAtMs: input.artifactRunStartedAtMs, commandTokens: prepared.commandTokens, cwd,
169
+ envelope: parsed.envelope, namespace: prepared.executionPlan.namespace, parseError, processResult,
170
+ reservation: prepared.executionPlan.sessionName ? state.activeRecordingReservations?.get(getAgentBrowserSessionIdentityKey(prepared.executionPlan.sessionName, prepared.executionPlan.namespace)) : undefined,
171
+ sessionName: prepared.executionPlan.sessionName, signal, stdin: prepared.runtimeToolStdin,
172
+ });
173
+ let presentationEnvelope = recordingStopRecovery?.envelope ?? parsed.envelope;
165
174
  let navigationSummary = undefined;
166
175
  let failedTransitionReverification = false;
167
176
  const repairedScreenshot = await repairScreenshotArtifact({ cwd, envelope: presentationEnvelope, request: prepared.preparedArgs.screenshotPathRequest });
@@ -178,6 +187,25 @@ export async function processBrowserOutput(input) {
178
187
  : Array.isArray(presentationEnvelope?.data)
179
188
  ? presentationEnvelope.data.flatMap((row, index) => isRecord(row) ? [Array.isArray(row.command) && row.command.every((token) => typeof token === "string") ? row.command : batchCommandSteps[index] ?? []] : [])
180
189
  : batchCommandSteps;
190
+ const confirmationSessionName = prepared.executionPlan.sessionName ?? "default";
191
+ let readConfirmation = state.sessionPageState.getReadConfirmation(getAgentBrowserSessionIdentityKey(confirmationSessionName, prepared.executionPlan.namespace));
192
+ let readConfirmationEvent;
193
+ const confirmationRows = prepared.executionPlan.commandInfo.command === "batch" && Array.isArray(presentationEnvelope?.data)
194
+ ? presentationEnvelope.data.flatMap((row, index) => isRecord(row) ? [{ tokens: batchCommandSteps[index] ?? [], data: row.result, response: row, succeeded: row.success === true }] : [])
195
+ : [{ tokens: prepared.commandTokens, data: presentationEnvelope?.data, response: presentationEnvelope, succeeded: presentationEnvelope?.success === true }];
196
+ for (const row of confirmationRows) {
197
+ const transition = nextReadConfirmation({ commandTokens: row.tokens, current: readConfirmation, data: row.data, namespace: prepared.executionPlan.namespace, sessionName: confirmationSessionName, succeeded: row.succeeded });
198
+ if (transition) {
199
+ readConfirmationEvent = transition;
200
+ readConfirmation = transition;
201
+ }
202
+ if (row.tokens.length === 2 && row.tokens[0] === "confirm" && isRecord(row.data) && row.data.confirmed === true && row.data.action === "read" && isRecord(row.data.result) && row.data.result.success === false && row.response) {
203
+ row.response.success = false;
204
+ row.response.error = row.data.result.error;
205
+ if (presentationEnvelope)
206
+ presentationEnvelope.success = false;
207
+ }
208
+ }
181
209
  const destinationTransition = dispatchedCommands.some((step) => {
182
210
  const [command, subcommand] = extractUpstreamCommandTokens(step);
183
211
  return isWindowOrDiffPageTransitionCommand(command, subcommand);
@@ -200,6 +228,7 @@ export async function processBrowserOutput(input) {
200
228
  const parseFailureOutput = parseError && processResult.stdoutSpillPath
201
229
  ? { fullOutputUnavailable: "Malformed upstream output was discarded because it may contain sensitive browser data." }
202
230
  : {};
231
+ const browserIndependentRead = prepared.readConfirmation !== undefined || isBrowserIndependentRead(prepared.commandTokens, prepared.runtimeToolStdin);
203
232
  const processSucceeded = !processResult.timedOut && !processResult.aborted && !processResult.spawnError && processResult.exitCode === 0;
204
233
  const plainTextInspection = prepared.executionPlan.plainTextInspection && processSucceeded;
205
234
  const parseSucceeded = plainTextInspection || parseError === undefined;
@@ -207,7 +236,7 @@ export async function processBrowserOutput(input) {
207
236
  presentationEnvelope = { success: true, data: { alreadyEnabled: true, enabled: true, message: getEnvelopeErrorString(presentationEnvelope) ?? "Stream already enabled" } };
208
237
  }
209
238
  const envelopeSuccess = plainTextInspection ? true : presentationEnvelope?.success !== false;
210
- let succeeded = processSucceeded && parseSucceeded && envelopeSuccess;
239
+ let succeeded = (processSucceeded && parseSucceeded && envelopeSuccess) || recordingStopRecovery?.recovery.healed === true;
211
240
  const inspectionText = plainTextInspection ? processResult.stdout.trim() : undefined;
212
241
  const sessionStateKey = getSessionContextKey(prepared.executionPlan.sessionName, prepared.executionPlan.namespace);
213
242
  const closeAllApplied = nestedBatchClosesAll || (directCloseAllRequested && succeeded);
@@ -364,7 +393,7 @@ export async function processBrowserOutput(input) {
364
393
  let fillVerificationDiagnostic;
365
394
  let selectorTextVisibilityDiagnostics = [];
366
395
  let electronBroadGetTextScopeDiagnostics = [];
367
- const timeoutPartialProgress = processResult.timedOut ? await collectTimeoutPartialProgress({ commandTokens: prepared.commandTokens, compiledJob: prepared.compiledJob, cwd, namespace: prepared.executionPlan.namespace, sessionName: prepared.executionPlan.sessionName, stdin: prepared.runtimeToolStdin }) : undefined;
396
+ const timeoutPartialProgress = processResult.timedOut && !recordingStopRecovery && !prepared.readConfirmation ? await collectTimeoutPartialProgress({ commandTokens: prepared.commandTokens, compiledJob: prepared.compiledJob, cwd, namespace: prepared.executionPlan.namespace, sessionName: prepared.executionPlan.sessionName, stdin: prepared.runtimeToolStdin }) : undefined;
368
397
  if (!currentSessionTabTarget && timeoutPartialProgress?.currentPage?.source === "live") {
369
398
  currentSessionTabTarget = normalizeSessionTabTarget(timeoutPartialProgress.currentPage);
370
399
  }
@@ -396,7 +425,7 @@ export async function processBrowserOutput(input) {
396
425
  const batchRefSnapshotState = prepared.executionPlan.commandInfo.command === "batch" ? extractLatestRefSnapshotStateFromBatchResults(presentationEnvelope?.data) : undefined;
397
426
  let currentRefSnapshot;
398
427
  let currentRefSnapshotInvalidation;
399
- if (sessionStateKey) {
428
+ if (sessionStateKey && !browserIndependentRead) {
400
429
  const sessionClosed = (isCloseCommand(prepared.executionPlan.commandInfo.command) && succeeded) || nestedBatchClosed;
401
430
  if (sessionClosed) {
402
431
  state.attachedSessionKeys.delete(sessionStateKey);
@@ -427,7 +456,7 @@ export async function processBrowserOutput(input) {
427
456
  if (!tabUpdate.applied && succeeded)
428
457
  sessionPageState.markPinning(sessionStateKey, "drift");
429
458
  }
430
- else if (processResult.agentBrowserStarted && (resultingPageState.pageUrlUnknown || resultingPageState.pageTargetMayHaveChanged)) {
459
+ else if (processResult.agentBrowserStarted && (resultingPageState.pageUrlUnknown || resultingPageState.pageTargetMayHaveChanged) && !(prepared.commandTokens[0] === "session" && prepared.commandTokens[1] === "info")) {
431
460
  sessionPageState.markTabTargetUnknown({ sessionName: sessionStateKey, update: sessionPageStateUpdate });
432
461
  }
433
462
  const refSnapshot = unsettledWebMcpMutation
@@ -512,7 +541,7 @@ export async function processBrowserOutput(input) {
512
541
  managedSessionHeadedAutosaveDisabled = false;
513
542
  managedSessionHeadedAutosaveInterval = undefined;
514
543
  }
515
- else if (managedTransitionSucceeded && executionTargetsManagedSession) {
544
+ else if (managedTransitionSucceeded && executionTargetsManagedSession && prepared.ownedManagedSessionContext && !prepared.ownedManagedSessionContext.reuseOnly) {
516
545
  managedSessionCompatibilityWorkaround = prepared.compatibilityWorkaround;
517
546
  managedSessionHeadedAutosaveDisabled = prepared.ownedManagedSessionContext?.headedManagedAutosaveDisabled === true;
518
547
  managedSessionHeadedAutosaveInterval = prepared.ownedManagedSessionContext?.headedManagedAutosaveInterval;
@@ -630,7 +659,7 @@ export async function processBrowserOutput(input) {
630
659
  electronLaunchRecord = electronFailedConnectCleanup.record;
631
660
  }
632
661
  }
633
- let errorText = getAgentBrowserErrorText({ aborted: processResult.aborted, command: prepared.executionPlan.commandInfo.command, effectiveArgs: prepared.redactedProcessArgs, envelope: presentationEnvelope, exitCode: processResult.exitCode, parseError, plainTextInspection, staleRefArgs: getStaleRefArgs(prepared.commandTokens, prepared.runtimeToolStdin), spawnError: processResult.spawnError, stderr: processResult.stderr, timedOut: processResult.timedOut, timeoutMs: processResult.timeoutMs, wrapperRecoveryHint: buildWrapperRecoveryHint({ sessionTabCorrection }) });
662
+ let errorText = recordingStopRecovery?.recovery.healed ? undefined : getAgentBrowserErrorText({ aborted: processResult.aborted, command: prepared.executionPlan.commandInfo.command, effectiveArgs: prepared.redactedProcessArgs, envelope: presentationEnvelope, exitCode: processResult.exitCode, parseError, plainTextInspection, staleRefArgs: getStaleRefArgs(prepared.commandTokens, prepared.runtimeToolStdin), spawnError: processResult.spawnError, stderr: processResult.stderr, timedOut: processResult.timedOut, timeoutMs: processResult.timeoutMs, wrapperRecoveryHint: buildWrapperRecoveryHint({ sessionTabCorrection }) });
634
663
  if (errorText && presentationEnvelope?.success === false && extractEnvelopeErrorText(presentationEnvelope.error) === undefined)
635
664
  presentationEnvelope = { ...presentationEnvelope, error: errorText };
636
665
  if (errorText) {
@@ -641,7 +670,21 @@ export async function processBrowserOutput(input) {
641
670
  }
642
671
  if (plainTextUpgrade && errorText)
643
672
  presentationEnvelope = { ...presentationEnvelope, success: false, error: errorText };
644
- 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` } : await buildToolPresentation({ args: prepared.redactedProcessArgs, artifactManifest, artifactMaxUpdatedAtMs: Date.now(), artifactMinUpdatedAtMs: input.artifactRunStartedAtMs, artifactRequest: screenshotArtifactRequest, batchArtifactRequests: batchScreenshotArtifactRequests, commandInfo: prepared.executionPlan.commandInfo, compiledSemanticAction: prepared.compiledSemanticAction, cwd, envelope: presentationEnvelope, errorText, namespace: prepared.executionPlan.namespace, networkRouteDiagnostics, networkRoutes: activeNetworkRoutes, persistentArtifactStore, sessionName: prepared.executionPlan.sessionName });
673
+ 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({ args: prepared.redactedProcessArgs, artifactManifest, artifactMaxUpdatedAtMs: Date.now(), artifactMinUpdatedAtMs: input.artifactRunStartedAtMs, artifactRequest: screenshotArtifactRequest, batchArtifactRequests: batchScreenshotArtifactRequests, commandInfo: prepared.executionPlan.commandInfo, compiledSemanticAction: prepared.compiledSemanticAction, cwd, envelope: presentationEnvelope, errorText, namespace: prepared.executionPlan.namespace, networkRouteDiagnostics, networkRoutes: activeNetworkRoutes, persistentArtifactStore, piCleanupOwnership: sessionStateKey && (state.ownedManagedSessions.has(sessionStateKey) || prepared.executionPlan.managedSessionName !== undefined) ? "wrapper-managed" : "caller-owned", sessionName: prepared.executionPlan.sessionName });
674
+ if (recordingStopRecovery)
675
+ presentation = mergeRecordingRecoveryPresentation(presentation, recordingStopRecovery);
676
+ const confirmation = readConfirmationEvent ?? prepared.readConfirmation;
677
+ if (confirmation) {
678
+ presentation.readConfirmation = confirmation;
679
+ if (confirmation.state !== "cleared" || !detectConfirmationRequired(presentationEnvelope?.data)) {
680
+ presentation.nextActions = buildReadConfirmationNextActions(confirmation, readConfirmationEvent?.state === "pending");
681
+ }
682
+ if (readConfirmationEvent?.state === "pending") {
683
+ presentation.resultCategory = "failure";
684
+ presentation.failureCategory = "confirmation-required";
685
+ presentation.successCategory = undefined;
686
+ }
687
+ }
645
688
  if (plainTextUpgrade && !succeeded && typeof presentationEnvelope?.data === "string") {
646
689
  presentation.data = presentationEnvelope.data;
647
690
  const errorContent = presentation.content[0];
@@ -751,6 +794,8 @@ export async function processBrowserOutput(input) {
751
794
  const evalPageUrl = evalNavigationSummary?.url ?? currentSessionTabTarget?.url ?? prepared.priorSessionTabTarget?.url ?? evalSessionTabUrl;
752
795
  const evalStdinHint = getEvalStdinHint({ command: prepared.executionPlan.commandInfo.command, data: presentationEnvelope?.data, stdin: prepared.runtimeToolStdin });
753
796
  const evalResultWarning = getEvalResultWarning({ command: prepared.executionPlan.commandInfo.command, data: presentationEnvelope?.data, navigationSummary: evalNavigationSummary, pageUrl: evalPageUrl, stdin: prepared.runtimeToolStdin });
797
+ if (readConfirmationEvent)
798
+ state.sessionPageState.applyReadConfirmation(readConfirmationEvent, sessionPageStateUpdate);
754
799
  const resultArtifactManifest = presentation.artifactManifest ?? artifactManifest;
755
800
  const artifactCleanup = await getArtifactCleanupGuidance({ command: prepared.executionPlan.commandInfo.command, cwd, manifest: resultArtifactManifest, succeeded });
756
801
  const recordingTransitionReached = prepared.executionPlan.commandInfo.command === "batch"
@@ -772,9 +817,10 @@ export async function processBrowserOutput(input) {
772
817
  const resultRetainsPreparedManagedSession = !managedSessionOutcome || (managedSessionOutcome.activeAfter
773
818
  && managedSessionOutcome.attemptedSessionName === managedSessionOutcome.currentSessionName);
774
819
  const resultHeadedManagedAutosaveDisabled = prepared.ownedManagedSessionContext?.headedManagedAutosaveDisabled === true
820
+ && !prepared.ownedManagedSessionContext.reuseOnly
775
821
  && resultRetainsPreparedManagedSession
776
822
  && !(commandClosesSession && succeeded);
777
- const resultHeadedManagedAutosaveInterval = resultRetainsPreparedManagedSession && !(commandClosesSession && succeeded)
823
+ const resultHeadedManagedAutosaveInterval = resultRetainsPreparedManagedSession && !prepared.ownedManagedSessionContext?.reuseOnly && !(commandClosesSession && succeeded)
778
824
  ? prepared.ownedManagedSessionContext?.headedManagedAutosaveInterval
779
825
  : undefined;
780
826
  const result = buildFinalAgentBrowserToolResult({ 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 });