pi-agent-browser-native 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/CHANGELOG.md +127 -0
  2. package/README.md +63 -20
  3. package/dist/extensions/agent-browser/index.js +787 -105
  4. package/dist/extensions/agent-browser/lib/argv-descriptor.js +35 -3
  5. package/dist/extensions/agent-browser/lib/argv-grammar.js +44 -2
  6. package/dist/extensions/agent-browser/lib/batch-lifecycle.js +71 -0
  7. package/dist/extensions/agent-browser/lib/command-policy.js +1 -1
  8. package/dist/extensions/agent-browser/lib/command-taxonomy.js +35 -2
  9. package/dist/extensions/agent-browser/lib/input-modes/job.js +61 -4
  10. package/dist/extensions/agent-browser/lib/input-modes/lookups.js +2 -2
  11. package/dist/extensions/agent-browser/lib/input-modes/params.js +22 -23
  12. package/dist/extensions/agent-browser/lib/input-modes/script.js +462 -0
  13. package/dist/extensions/agent-browser/lib/input-modes/semantic-action.js +51 -12
  14. package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +8 -0
  15. package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +3 -1
  16. package/dist/extensions/agent-browser/lib/managed-session-restore.js +26 -36
  17. package/dist/extensions/agent-browser/lib/managed-session-snapshots.js +2 -4
  18. package/dist/extensions/agent-browser/lib/managed-session-state-policy.js +47 -29
  19. package/dist/extensions/agent-browser/lib/managed-session-storage.js +50 -24
  20. package/dist/extensions/agent-browser/lib/orchestration/batch-stdin.js +26 -5
  21. package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.js +110 -30
  22. package/dist/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.js +2 -1
  23. package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +26 -25
  24. package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +17 -3
  25. package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +2 -1
  26. package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +6 -4
  27. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +129 -32
  28. package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +191 -55
  29. package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +57 -29
  30. package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +15 -11
  31. package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +36 -18
  32. package/dist/extensions/agent-browser/lib/orchestration/script-mode.js +299 -0
  33. package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +32 -10
  34. package/dist/extensions/agent-browser/lib/playbook.js +18 -15
  35. package/dist/extensions/agent-browser/lib/process-environment.js +14 -0
  36. package/dist/extensions/agent-browser/lib/process-identity.js +4 -4
  37. package/dist/extensions/agent-browser/lib/process.js +131 -41
  38. package/dist/extensions/agent-browser/lib/recording-reservations.js +183 -0
  39. package/dist/extensions/agent-browser/lib/results/action-recommendations.js +62 -5
  40. package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +62 -4
  41. package/dist/extensions/agent-browser/lib/results/categories.js +6 -1
  42. package/dist/extensions/agent-browser/lib/results/next-actions.js +19 -5
  43. package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +85 -38
  44. package/dist/extensions/agent-browser/lib/results/presentation/batch.js +66 -11
  45. package/dist/extensions/agent-browser/lib/results/presentation/common.js +18 -0
  46. package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +7 -1
  47. package/dist/extensions/agent-browser/lib/results/presentation/errors.js +2 -1
  48. package/dist/extensions/agent-browser/lib/results/presentation/navigation.js +26 -11
  49. package/dist/extensions/agent-browser/lib/results/presentation/registry.js +58 -13
  50. package/dist/extensions/agent-browser/lib/results/presentation/semantic-action.js +1 -10
  51. package/dist/extensions/agent-browser/lib/results/presentation.js +6 -3
  52. package/dist/extensions/agent-browser/lib/results/recovery-actions.js +2 -0
  53. package/dist/extensions/agent-browser/lib/results/selector-recovery.js +51 -8
  54. package/dist/extensions/agent-browser/lib/results/snapshot-high-value-controls.js +13 -7
  55. package/dist/extensions/agent-browser/lib/runtime.js +116 -39
  56. package/dist/extensions/agent-browser/lib/session-page-state.js +62 -10
  57. package/dist/extensions/agent-browser/lib/upstream-version.js +14 -0
  58. package/dist/extensions/agent-browser/script-worker.js +169 -0
  59. package/dist/scripts/agent-browser-target.mjs +3 -0
  60. package/docs/ARCHITECTURE.md +40 -21
  61. package/docs/COMMAND_REFERENCE.md +90 -35
  62. package/docs/RELEASE.md +3 -3
  63. package/docs/REQUIREMENTS.md +4 -2
  64. package/docs/SUPPORT_MATRIX.md +26 -19
  65. package/docs/TOOL_CONTRACT.md +93 -52
  66. package/package.json +3 -1
  67. package/platform-smoke.config.mjs +2 -2
  68. package/scripts/agent-browser-capability-baseline.mjs +24 -6
  69. package/scripts/agent-browser-target.mjs +3 -0
  70. package/scripts/build.mjs +41 -0
  71. package/scripts/doctor.mjs +7 -6
  72. package/scripts/platform-smoke/browser-dogfood-windows.ps1 +9 -3
  73. package/scripts/platform-smoke/targets.mjs +12 -6
@@ -1,22 +1,25 @@
1
1
  import { rm } from "node:fs/promises";
2
- import { getAgentBrowserSessionIdentityKey } from "../../argv-grammar.js";
3
- import { isCloseCommand, isNavigationObservableCommandName, isOpenNavigationCommand } from "../../command-taxonomy.js";
2
+ import { getAgentBrowserSessionIdentityKey, isAgentBrowserSessionIdentityKeyInNamespace } from "../../argv-grammar.js";
3
+ import { batchHasSuccessfulCloseAll, getSuccessfulBatchCloseLifecycle } from "../../batch-lifecycle.js";
4
+ import { isCloseAllCommand, isCloseCommand, isNavigationObservableCommandName, isOpenNavigationCommand, isRecordPageTransitionCommand, isUnverifiedPageTransitionCommand } from "../../command-taxonomy.js";
4
5
  import { OPEN_RESULT_TAB_CORRECTION_FLAGS } from "../../launch-scoped-flags.js";
5
6
  import { cleanupElectronLaunchResources, inspectElectronLaunchStatus } from "../../electron/cleanup.js";
6
7
  import { getAllowedDomainsViolation, parseAllowedDomainsPolicyFromArgs } from "../../navigation-policy.js";
7
8
  import { getManagedSessionResultingPageState, getObservedBrowserPageValidationError, managedSessionCommandRequiresLivePageVerification } from "../../managed-session-state-policy.js";
8
9
  import { analyzeNetworkSourceLookupResults, analyzeSourceLookupResults, redactNetworkSourceLookupAnalysis } from "../../input-modes/lookups.js";
9
- import { analyzeQaPresetResults, analyzeQaPresetTimeout, buildQaCompactPassText, extractQaPageContext } from "../../input-modes/job.js";
10
+ import { analyzeQaPresetResults, analyzeQaPresetTimeout, buildQaCompactFailureText, buildQaCompactPassText, extractQaPageContext } from "../../input-modes/job.js";
10
11
  import { applyNetworkRouteRecords, buildNetworkRouteDiagnostics } from "../../results/network-routes.js";
11
12
  import { buildToolPresentation } from "../../results/presentation.js";
12
13
  import { getAgentBrowserErrorText, parseAgentBrowserEnvelope } from "../../results/envelope.js";
14
+ import { omitUpstreamLifecycle } from "../../results/presentation/common.js";
13
15
  import { getClipboardWritePayloadCandidates, redactClipboardPermissionEcho, redactClipboardPermissionErrorValue } from "../../results/presentation/errors.js";
14
16
  import { shouldCaptureSemanticActionNavigationSummary } from "../../results/presentation/semantic-action.js";
15
- import { commandExplicitlyTargetsAboutBlank, deriveSessionTabTarget, extractLatestRefSnapshotStateFromBatchResults, extractRefSnapshotFromData, extractSessionTabTargetFromBatchResults, extractSessionTabTargetFromCommandData, isAboutBlankSessionTabTarget, normalizeSessionTabTarget, } from "../../session-page-state.js";
17
+ import { buildPageTransitionRefSnapshotInvalidation, commandExplicitlyTargetsAboutBlank, deriveSessionTabTarget, extractLatestRefSnapshotStateFromBatchResults, extractRefSnapshotFromData, extractSessionTabTargetFromBatchResults, extractSessionTabTargetFromCommandData, isAboutBlankSessionTabTarget, normalizeSessionTabTarget, } from "../../session-page-state.js";
16
18
  import { isRecord } from "../../parsing.js";
17
19
  import { pruneOwnedManagedSessionRestoreSnapshots } from "../../managed-session-restore.js";
18
20
  import { isManagedSessionRestoreKey } from "../../managed-session-storage.js";
19
- import { createFreshSessionName, extractCommandTokens, resolveManagedSessionState } from "../../runtime.js";
21
+ import { createFreshSessionName, extractUpstreamCommandTokens, resolveManagedSessionState } from "../../runtime.js";
22
+ import { getUpstreamEffectiveBatchSteps } from "../batch-stdin.js";
20
23
  import { closeManagedSession, inspectManagedSessionDaemon } from "./managed-session-daemon-policy.js";
21
24
  import { applyOpenResultTabCorrection, buildAboutBlankRecoveryHint, buildAboutBlankWarning, buildElectronPostCommandHealthDiagnostic, buildElectronRefFreshnessDiagnostic, buildElectronSessionMismatch, buildManagedSessionOutcome, collectOpenResultTabCorrection, collectSessionTabSelection, extractNavigationSummaryFromData, extractStringResultField, findElectronLaunchRecordForSession, formatElectronPostCommandHealthText, formatElectronSessionMismatchText, getSessionContextKey, getStaleRefArgs, mergeNavigationSummaryIntoData, shouldCaptureNavigationSummary, shouldCorrectSessionTabAfterCommand, shouldInspectElectronPostCommandHealth, unwrapPinnedSessionBatchEnvelope, updateTraceOwnerState, } from "./session-state.js";
22
25
  import { collectClickDispatchDiagnostic } from "./click-dispatch.js";
@@ -71,6 +74,15 @@ function batchStartedManagedBrowser(data) {
71
74
  return command === "connect" || command === "goto" || command === "navigate" || isOpenNavigationCommand(command);
72
75
  });
73
76
  }
77
+ function withoutNamespaceEntries(entries, namespace) {
78
+ return new Map([...entries].filter(([key]) => !isAgentBrowserSessionIdentityKeyInNamespace(key, namespace)));
79
+ }
80
+ function deleteNamespaceEntries(entries, namespace) {
81
+ for (const key of entries.keys()) {
82
+ if (isAgentBrowserSessionIdentityKeyInNamespace(key, namespace))
83
+ entries.delete(key);
84
+ }
85
+ }
74
86
  function setNetworkRouteState(options) {
75
87
  if (!options.sessionName)
76
88
  return options.routesBySession;
@@ -95,7 +107,12 @@ function applyBatchNetworkRouteState(options) {
95
107
  for (const item of options.data) {
96
108
  if (!isRecord(item) || !Array.isArray(item.command) || !item.command.every((token) => typeof token === "string"))
97
109
  continue;
98
- routes = applyNetworkRouteRecords(routes, extractCommandTokens(item.command), item.success !== false);
110
+ const commandTokens = extractUpstreamCommandTokens(item.command);
111
+ const stepSucceeded = item.success !== false;
112
+ if (stepSucceeded && isCloseCommand(commandTokens[0]))
113
+ routes = undefined;
114
+ else
115
+ routes = applyNetworkRouteRecords(routes, commandTokens, stepSucceeded);
99
116
  }
100
117
  return setNetworkRouteState({ routes, routesBySession: options.routesBySession, sessionName: options.sessionName });
101
118
  }
@@ -120,6 +137,7 @@ export async function processBrowserOutput(input) {
120
137
  let parseError = parsed.parseError;
121
138
  let presentationEnvelope = parsed.envelope;
122
139
  let navigationSummary = undefined;
140
+ let failedTransitionReverification = false;
123
141
  if (prepared.pinnedBatchUnwrapMode) {
124
142
  const pinnedBatchResult = unwrapPinnedSessionBatchEnvelope({ envelope: parsed.envelope, includeNavigationSummary: prepared.includePinnedNavigationSummary, mode: prepared.pinnedBatchUnwrapMode });
125
143
  parseError = pinnedBatchResult.parseError ?? parseError;
@@ -132,11 +150,22 @@ export async function processBrowserOutput(input) {
132
150
  presentationEnvelope = repairedBatchScreenshots.envelope;
133
151
  const screenshotArtifactRequest = repairedScreenshot.request;
134
152
  const batchScreenshotArtifactRequests = repairedBatchScreenshots.requests;
153
+ const batchCommandSteps = prepared.executionPlan.commandInfo.command === "batch"
154
+ ? getUpstreamEffectiveBatchSteps(prepared.commandTokens, prepared.runtimeToolStdin)
155
+ : [];
156
+ const nestedBatchClose = prepared.executionPlan.commandInfo.command === "batch"
157
+ ? getSuccessfulBatchCloseLifecycle(presentationEnvelope?.data, batchCommandSteps)
158
+ : undefined;
159
+ const nestedBatchClosed = nestedBatchClose?.endsClosed === true;
160
+ const nestedBatchRemainsActive = nestedBatchClose?.endsClosed === false;
161
+ const nestedBatchClosesAll = prepared.executionPlan.commandInfo.command === "batch"
162
+ && batchHasSuccessfulCloseAll(presentationEnvelope?.data, batchCommandSteps);
163
+ const directCloseAllRequested = isCloseAllCommand(extractUpstreamCommandTokens(prepared.commandTokens));
135
164
  const rawCloseStatePath = isCloseCommand(prepared.executionPlan.commandInfo.command)
136
165
  && isRecord(presentationEnvelope?.data)
137
166
  && typeof presentationEnvelope.data.statePath === "string"
138
167
  ? presentationEnvelope.data.statePath
139
- : undefined;
168
+ : nestedBatchClose?.statePath;
140
169
  if (presentationEnvelope && prepared.exactSensitiveValues.length > 0)
141
170
  presentationEnvelope = redactExactSensitiveValue(presentationEnvelope, prepared.exactSensitiveValues);
142
171
  const parseFailureOutput = parseError && processResult.stdoutSpillPath
@@ -152,7 +181,44 @@ export async function processBrowserOutput(input) {
152
181
  let succeeded = processSucceeded && parseSucceeded && envelopeSuccess;
153
182
  const inspectionText = plainTextInspection ? processResult.stdout.trim() : undefined;
154
183
  const sessionStateKey = getSessionContextKey(prepared.executionPlan.sessionName, prepared.executionPlan.namespace);
155
- updateTraceOwnerState({ command: prepared.executionPlan.commandInfo.command, sessionName: sessionStateKey, subcommand: prepared.executionPlan.commandInfo.subcommand, succeeded, traceOwners });
184
+ const closeAllApplied = nestedBatchClosesAll || (directCloseAllRequested && succeeded);
185
+ if (closeAllApplied) {
186
+ allowedDomainsBySession = withoutNamespaceEntries(allowedDomainsBySession, prepared.executionPlan.namespace);
187
+ networkRoutesBySession = withoutNamespaceEntries(networkRoutesBySession, prepared.executionPlan.namespace);
188
+ deleteNamespaceEntries(state.attachedSessionKeys, prepared.executionPlan.namespace);
189
+ deleteNamespaceEntries(traceOwners, prepared.executionPlan.namespace);
190
+ sessionPageState.clearNamespace(prepared.executionPlan.namespace);
191
+ const retainedSessionKey = nestedBatchRemainsActive ? sessionStateKey : undefined;
192
+ for (const [key, owner] of state.ownedManagedSessions) {
193
+ if (!isAgentBrowserSessionIdentityKeyInNamespace(key, prepared.executionPlan.namespace) || key === retainedSessionKey)
194
+ continue;
195
+ state.closedManagedSessionNames.add(key);
196
+ state.managedSessionRestoreState.clear(owner.sessionName, owner.namespace);
197
+ }
198
+ }
199
+ else if (nestedBatchClose && sessionStateKey) {
200
+ allowedDomainsBySession = new Map(allowedDomainsBySession);
201
+ allowedDomainsBySession.delete(sessionStateKey);
202
+ networkRoutesBySession = new Map(networkRoutesBySession);
203
+ networkRoutesBySession.delete(sessionStateKey);
204
+ sessionPageState.clearSession(sessionStateKey);
205
+ }
206
+ if (prepared.executionPlan.commandInfo.command === "batch" && Array.isArray(presentationEnvelope?.data)) {
207
+ for (const [index, row] of presentationEnvelope.data.entries()) {
208
+ if (!isRecord(row))
209
+ continue;
210
+ const rowCommand = Array.isArray(row.command) && row.command.every((token) => typeof token === "string")
211
+ ? row.command
212
+ : batchCommandSteps[index];
213
+ if (!rowCommand)
214
+ continue;
215
+ const [command, subcommand] = extractUpstreamCommandTokens(rowCommand);
216
+ updateTraceOwnerState({ command, sessionName: sessionStateKey, subcommand, succeeded: row.success === true, traceOwners });
217
+ }
218
+ }
219
+ else {
220
+ updateTraceOwnerState({ command: prepared.executionPlan.commandInfo.command, sessionName: sessionStateKey, subcommand: prepared.executionPlan.commandInfo.subcommand, succeeded, traceOwners });
221
+ }
156
222
  let clickDispatchDiagnostic;
157
223
  if (succeeded && prepared.clickDispatchProbe) {
158
224
  clickDispatchDiagnostic = await collectClickDispatchDiagnostic({ cwd, namespace: prepared.executionPlan.namespace, probe: prepared.clickDispatchProbe, sessionName: prepared.executionPlan.sessionName, signal });
@@ -183,7 +249,24 @@ export async function processBrowserOutput(input) {
183
249
  shouldCaptureAllowedDomainNavigationSummary ||
184
250
  managedSessionCommandRequiresLivePageVerification(prepared.executionPlan.effectiveArgs, prepared.runtimeToolStdin) ||
185
251
  (prepared.executionPlan.commandInfo.command === "tab" && prepared.executionPlan.commandInfo.subcommand === "close"))) {
186
- navigationSummary = await collectNavigationSummary({ cwd, namespace: prepared.executionPlan.namespace, sessionName: prepared.executionPlan.sessionName, signal });
252
+ navigationSummary = await collectNavigationSummary({ cwd, namespace: prepared.executionPlan.namespace, priorTarget: prepared.priorSessionTabTarget, sessionName: prepared.executionPlan.sessionName, signal });
253
+ }
254
+ // A failed eval/back/forward/reload/state-load/tab would otherwise drop the page to unverified and force the
255
+ // agent through a manual get url round trip. Probe the live URL ourselves instead: an observed http(s) page
256
+ // stays verified, a file URL keeps the local-file gate closed through currentSessionTabTarget, and a failed
257
+ // probe preserves the existing unknown-target behavior.
258
+ if (succeeded === false &&
259
+ !navigationSummary &&
260
+ processResult.agentBrowserStarted &&
261
+ !processResult.aborted &&
262
+ !processResult.timedOut &&
263
+ prepared.executionPlan.commandInfo.command !== "batch" &&
264
+ isUnverifiedPageTransitionCommand(prepared.executionPlan.commandInfo.command, prepared.executionPlan.commandInfo.subcommand)) {
265
+ navigationSummary = await collectNavigationSummary({ cwd, namespace: prepared.executionPlan.namespace, priorTarget: prepared.priorSessionTabTarget, sessionName: prepared.executionPlan.sessionName, signal });
266
+ // A failed transition command can still have mutated or replaced the document before throwing, so
267
+ // keeping the verified URL must not keep the prior snapshot refs (markTabTargetUnknown dropped
268
+ // them before this probe existed); invalidate so the next ref use requires a fresh snapshot.
269
+ failedTransitionReverification = navigationSummary !== undefined;
187
270
  }
188
271
  if (navigationSummary && presentationEnvelope && prepared.executionPlan.commandInfo.command !== "eval" && !Array.isArray(presentationEnvelope.data))
189
272
  presentationEnvelope = { ...presentationEnvelope, data: mergeNavigationSummaryIntoData(presentationEnvelope.data, navigationSummary) };
@@ -207,15 +290,18 @@ export async function processBrowserOutput(input) {
207
290
  presentationEnvelope = { error: observedPageValidationError, success: false };
208
291
  }
209
292
  const safeObservedSessionTabTarget = observedPageValidationError ? normalizeSessionTabTarget({ url: observedSessionTabTarget?.url }) : observedSessionTabTarget;
210
- let currentSessionTabTarget = safeObservedSessionTabTarget ?? (resultingPageState.pageTargetMayHaveChanged
211
- ? succeeded ? normalizeSessionTabTarget({ url: resultingPageState.currentPageUrl }) : undefined
212
- : deriveSessionTabTarget({ command: prepared.executionPlan.commandInfo.command, data: presentationEnvelope?.data, navigationSummary, previousTarget: prepared.priorSessionTabTarget, subcommand: prepared.executionPlan.commandInfo.subcommand }));
293
+ let currentSessionTabTarget = safeObservedSessionTabTarget;
294
+ if (!currentSessionTabTarget && nestedBatchClose === undefined) {
295
+ currentSessionTabTarget = resultingPageState.pageTargetMayHaveChanged
296
+ ? succeeded ? normalizeSessionTabTarget({ url: resultingPageState.currentPageUrl }) : undefined
297
+ : deriveSessionTabTarget({ command: prepared.executionPlan.commandInfo.command, data: presentationEnvelope?.data, navigationSummary, previousTarget: prepared.priorSessionTabTarget, subcommand: prepared.executionPlan.commandInfo.subcommand });
298
+ }
213
299
  let aboutBlankSessionMismatch;
214
300
  let electronPostCommandHealth;
215
301
  let electronRefFreshnessDiagnostic;
216
302
  let electronSessionMismatch;
217
303
  let electronStatusAfterCommand;
218
- const shouldTreatAboutBlankAsMismatch = succeeded && prepared.priorSessionTabTarget !== undefined && !isAboutBlankSessionTabTarget(prepared.priorSessionTabTarget) && isAboutBlankSessionTabTarget(observedSessionTabTarget ?? currentSessionTabTarget) && !commandExplicitlyTargetsAboutBlank(prepared.commandTokens);
304
+ const shouldTreatAboutBlankAsMismatch = succeeded && nestedBatchClose === undefined && prepared.priorSessionTabTarget !== undefined && !isAboutBlankSessionTabTarget(prepared.priorSessionTabTarget) && isAboutBlankSessionTabTarget(observedSessionTabTarget ?? currentSessionTabTarget) && !commandExplicitlyTargetsAboutBlank(prepared.commandTokens);
219
305
  let sessionTabCorrection = prepared.sessionTabCorrection;
220
306
  if (shouldTreatAboutBlankAsMismatch && prepared.priorSessionTabTarget) {
221
307
  const aboutBlankObservedTarget = observedSessionTabTarget ?? currentSessionTabTarget;
@@ -228,7 +314,7 @@ export async function processBrowserOutput(input) {
228
314
  else
229
315
  currentSessionTabTarget = aboutBlankObservedTarget ?? normalizeSessionTabTarget({ url: "about:blank" });
230
316
  aboutBlankSessionMismatch = { activeUrl: "about:blank", recoveryApplied: appliedAboutBlankRecovery !== undefined, recoveryHint: buildAboutBlankRecoveryHint(), targetTitle: prepared.priorSessionTabTarget.title, targetUrl: prepared.priorSessionTabTarget.url };
231
- const electronRecord = findElectronLaunchRecordForSession(prepared.executionPlan.sessionName, electronLaunchRecords);
317
+ const electronRecord = findElectronLaunchRecordForSession(prepared.executionPlan.sessionName, electronLaunchRecords, prepared.executionPlan.namespace);
232
318
  if (electronRecord && prepared.executionPlan.sessionName) {
233
319
  electronStatusAfterCommand = await inspectElectronLaunchStatus(electronRecord);
234
320
  electronSessionMismatch = buildElectronSessionMismatch({ managedSession: { sessionName: prepared.executionPlan.sessionName, title: aboutBlankObservedTarget?.title, url: aboutBlankObservedTarget?.url ?? "about:blank" }, record: electronRecord, statusTargets: electronStatusAfterCommand.targets });
@@ -254,7 +340,7 @@ export async function processBrowserOutput(input) {
254
340
  succeeded = false;
255
341
  presentationEnvelope = { ...(presentationEnvelope ?? {}), error: allowedDomainsViolation.summary, success: false };
256
342
  }
257
- const electronRecordForCommand = findElectronLaunchRecordForSession(prepared.executionPlan.sessionName, electronLaunchRecords);
343
+ const electronRecordForCommand = findElectronLaunchRecordForSession(prepared.executionPlan.sessionName, electronLaunchRecords, prepared.executionPlan.namespace);
258
344
  if (succeeded && electronRecordForCommand && shouldInspectElectronPostCommandHealth(prepared.executionPlan.commandInfo.command)) {
259
345
  electronStatusAfterCommand ??= await inspectElectronLaunchStatus(electronRecordForCommand);
260
346
  electronPostCommandHealth = buildElectronPostCommandHealthDiagnostic({ command: prepared.executionPlan.commandInfo.command, record: electronRecordForCommand, status: electronStatusAfterCommand, target: observedSessionTabTarget ?? currentSessionTabTarget });
@@ -288,7 +374,7 @@ export async function processBrowserOutput(input) {
288
374
  if (succeeded) {
289
375
  selectorTextVisibilityDiagnostics = await collectSelectorTextVisibilityDiagnostics({ commandInfo: prepared.executionPlan.commandInfo, commandTokens: prepared.commandTokens, cwd, data: presentationEnvelope?.data, namespace: prepared.executionPlan.namespace, sessionName: prepared.executionPlan.sessionName, signal });
290
376
  if (electronRecordForCommand)
291
- electronBroadGetTextScopeDiagnostics = collectElectronBroadGetTextScopeDiagnostics({ commandInfo: prepared.executionPlan.commandInfo, commandTokens: prepared.commandTokens, currentTarget: currentSessionTabTarget, data: presentationEnvelope?.data, electronLaunchRecords, priorTarget: prepared.priorSessionTabTarget, sessionName: prepared.executionPlan.sessionName });
377
+ electronBroadGetTextScopeDiagnostics = collectElectronBroadGetTextScopeDiagnostics({ commandInfo: prepared.executionPlan.commandInfo, commandTokens: prepared.commandTokens, currentTarget: currentSessionTabTarget, data: presentationEnvelope?.data, electronLaunchRecords, namespace: prepared.executionPlan.namespace, priorTarget: prepared.priorSessionTabTarget, sessionName: prepared.executionPlan.sessionName });
292
378
  }
293
379
  const activeNetworkRoutes = sessionStateKey ? networkRoutesBySession.get(sessionStateKey) : undefined;
294
380
  const networkRouteDiagnostics = succeeded && prepared.executionPlan.commandInfo.command === "network" && prepared.executionPlan.commandInfo.subcommand === "requests" && prepared.executionPlan.sessionName
@@ -298,11 +384,13 @@ export async function processBrowserOutput(input) {
298
384
  const comboboxFocusDiagnostic = succeeded ? await collectComboboxFocusDiagnostic({ command: prepared.executionPlan.commandInfo.command, commandTokens: prepared.commandTokens, cwd, namespace: prepared.executionPlan.namespace, semanticAction: prepared.compiledSemanticAction, sessionName: prepared.executionPlan.sessionName, signal }) : undefined;
299
385
  const recordingDependencyWarning = await collectRecordingDependencyWarning({ command: prepared.executionPlan.commandInfo.command, commandTokens: prepared.commandTokens, succeeded });
300
386
  const scrollNoopDiagnostic = succeeded && prepared.shouldProbeScrollNoop ? buildScrollNoopDiagnostic(prepared.scrollPositionBefore, await collectScrollPositionSnapshot({ cwd, namespace: prepared.executionPlan.namespace, sessionName: prepared.executionPlan.sessionName, signal })) : undefined;
387
+ const batchRefSnapshotState = prepared.executionPlan.commandInfo.command === "batch" ? extractLatestRefSnapshotStateFromBatchResults(presentationEnvelope?.data) : undefined;
301
388
  let currentRefSnapshot;
302
389
  let currentRefSnapshotInvalidation;
303
- const batchRefSnapshotState = prepared.executionPlan.commandInfo.command === "batch" ? extractLatestRefSnapshotStateFromBatchResults(presentationEnvelope?.data) : undefined;
304
390
  if (sessionStateKey) {
305
- if (isCloseCommand(prepared.executionPlan.commandInfo.command) && succeeded) {
391
+ const sessionClosed = (isCloseCommand(prepared.executionPlan.commandInfo.command) && succeeded) || nestedBatchClosed;
392
+ if (sessionClosed) {
393
+ state.attachedSessionKeys.delete(sessionStateKey);
306
394
  allowedDomainsBySession = new Map(allowedDomainsBySession);
307
395
  allowedDomainsBySession.delete(sessionStateKey);
308
396
  networkRoutesBySession = new Map(networkRoutesBySession);
@@ -310,24 +398,43 @@ export async function processBrowserOutput(input) {
310
398
  sessionPageState.clearSession(sessionStateKey);
311
399
  state.closedManagedSessionNames.add(sessionStateKey);
312
400
  }
313
- else if (currentSessionTabTarget) {
314
- const tabUpdate = sessionPageState.applyTabTarget({ sessionName: sessionStateKey, target: currentSessionTabTarget, update: sessionPageStateUpdate });
315
- if (!tabUpdate.applied && succeeded)
316
- sessionPageState.markPinning(sessionStateKey, "drift");
317
- }
318
- else if (processResult.agentBrowserStarted && (resultingPageState.pageUrlUnknown || resultingPageState.pageTargetMayHaveChanged)) {
319
- sessionPageState.markTabTargetUnknown({ sessionName: sessionStateKey, update: sessionPageStateUpdate });
320
- }
321
- const refSnapshot = prepared.executionPlan.commandInfo.command === "batch" ? batchRefSnapshotState?.snapshot : succeeded ? prepared.executionPlan.commandInfo.command === "snapshot" ? extractRefSnapshotFromData(presentationEnvelope?.data) : prepared.resolvedSemanticActionRefSnapshot ?? overlayBlockerDiagnostic?.snapshot : undefined;
322
- if (refSnapshot) {
323
- const refUpdate = sessionPageState.applyRefSnapshot({ fallbackTarget: currentSessionTabTarget, sessionName: sessionStateKey, snapshot: refSnapshot, update: sessionPageStateUpdate });
324
- currentRefSnapshot = refUpdate.refSnapshot;
325
- currentRefSnapshotInvalidation = refUpdate.refSnapshotInvalidation;
326
- }
327
401
  else {
328
- const stateView = sessionPageState.get(sessionStateKey);
329
- currentRefSnapshot = stateView.refSnapshot;
330
- currentRefSnapshotInvalidation = stateView.refSnapshotInvalidation;
402
+ // A batch that times out or returns unparseable output yields no result rows, but the daemon
403
+ // may already have executed a recording page swap; fall back to the planned steps then. This
404
+ // also fires for wrapper-replaced envelopes and upstream pre-loop errors where nothing ran,
405
+ // which only over-invalidates (one extra snapshot), never under-invalidates.
406
+ const plannedBatchRecordSwap = !Array.isArray(presentationEnvelope?.data) && batchCommandSteps.some((step) => isRecordPageTransitionCommand(step));
407
+ const pageTransitionInvalidation = processResult.agentBrowserStarted && (isRecordPageTransitionCommand(prepared.commandTokens) || plannedBatchRecordSwap)
408
+ ? buildPageTransitionRefSnapshotInvalidation()
409
+ : failedTransitionReverification
410
+ ? buildPageTransitionRefSnapshotInvalidation("A failed eval/back/forward/reload/connect/state-load/tab command may still have changed the page, so the prior snapshot refs were invalidated. Run snapshot -i before using page-scoped refs.")
411
+ : batchRefSnapshotState?.invalidation?.reason === "page-transition"
412
+ ? batchRefSnapshotState.invalidation
413
+ : undefined;
414
+ if (currentSessionTabTarget) {
415
+ const tabUpdate = sessionPageState.applyTabTarget({ sessionName: sessionStateKey, target: currentSessionTabTarget, update: sessionPageStateUpdate });
416
+ if (!tabUpdate.applied && succeeded)
417
+ sessionPageState.markPinning(sessionStateKey, "drift");
418
+ }
419
+ else if (processResult.agentBrowserStarted && (resultingPageState.pageUrlUnknown || resultingPageState.pageTargetMayHaveChanged)) {
420
+ sessionPageState.markTabTargetUnknown({ sessionName: sessionStateKey, update: sessionPageStateUpdate });
421
+ }
422
+ const refSnapshot = prepared.executionPlan.commandInfo.command === "batch" ? batchRefSnapshotState?.snapshot : succeeded ? prepared.executionPlan.commandInfo.command === "snapshot" ? extractRefSnapshotFromData(presentationEnvelope?.data) : prepared.resolvedSemanticActionRefSnapshot ?? overlayBlockerDiagnostic?.snapshot : undefined;
423
+ if (refSnapshot) {
424
+ const refUpdate = sessionPageState.applyRefSnapshot({ fallbackTarget: currentSessionTabTarget, sessionName: sessionStateKey, snapshot: refSnapshot, update: sessionPageStateUpdate });
425
+ currentRefSnapshot = refUpdate.refSnapshot;
426
+ currentRefSnapshotInvalidation = refUpdate.refSnapshotInvalidation;
427
+ }
428
+ else if (pageTransitionInvalidation) {
429
+ const refUpdate = sessionPageState.applyRefSnapshotInvalidation({ invalidation: pageTransitionInvalidation, sessionName: sessionStateKey, update: sessionPageStateUpdate });
430
+ currentRefSnapshot = refUpdate.refSnapshot;
431
+ currentRefSnapshotInvalidation = refUpdate.refSnapshotInvalidation;
432
+ }
433
+ else {
434
+ const stateView = sessionPageState.get(sessionStateKey);
435
+ currentRefSnapshot = stateView.refSnapshot;
436
+ currentRefSnapshotInvalidation = stateView.refSnapshotInvalidation;
437
+ }
331
438
  }
332
439
  }
333
440
  const priorManagedSessionActive = managedSessionActive;
@@ -335,11 +442,22 @@ export async function processBrowserOutput(input) {
335
442
  const priorManagedSessionHeadedAutosaveInterval = managedSessionHeadedAutosaveInterval;
336
443
  const priorManagedSessionName = managedSessionName;
337
444
  const priorManagedSessionNamespace = managedSessionNamespace;
338
- const commandClosesSession = isCloseCommand(prepared.executionPlan.commandInfo.command);
445
+ const priorManagedSessionKey = getSessionContextKey(priorManagedSessionName, priorManagedSessionNamespace) ?? priorManagedSessionName;
446
+ const closeAllTargetsPriorManagedSession = closeAllApplied
447
+ && priorManagedSessionActive
448
+ && isAgentBrowserSessionIdentityKeyInNamespace(priorManagedSessionKey, prepared.executionPlan.namespace);
449
+ const closeAllRetainsPriorManagedSession = closeAllTargetsPriorManagedSession
450
+ && nestedBatchRemainsActive
451
+ && sessionStateKey === priorManagedSessionKey;
452
+ const closeAllClosesPriorManagedSession = closeAllTargetsPriorManagedSession && !closeAllRetainsPriorManagedSession;
453
+ const commandClosesSession = isCloseCommand(prepared.executionPlan.commandInfo.command) || nestedBatchClosed || closeAllClosesPriorManagedSession;
454
+ const closeCommandSucceeded = (isCloseCommand(prepared.executionPlan.commandInfo.command) && succeeded) || nestedBatchClosed || closeAllClosesPriorManagedSession;
339
455
  const closeTargetsPriorManagedNamespace = prepared.executionPlan.namespace === priorManagedSessionNamespace;
340
- const managedCloseSessionName = commandClosesSession && succeeded && prepared.executionPlan.sessionName === priorManagedSessionName && closeTargetsPriorManagedNamespace
341
- ? prepared.executionPlan.sessionName
342
- : prepared.executionPlan.managedSessionName;
456
+ const managedCloseSessionName = closeAllClosesPriorManagedSession
457
+ ? priorManagedSessionName
458
+ : closeCommandSucceeded && prepared.executionPlan.sessionName === priorManagedSessionName && closeTargetsPriorManagedNamespace
459
+ ? prepared.executionPlan.sessionName
460
+ : prepared.executionPlan.managedSessionName;
343
461
  const policyBlockedFreshManagedSession = allowedDomainsViolation !== undefined && prepared.sessionMode === "fresh" && prepared.executionPlan.managedSessionName === prepared.executionPlan.sessionName;
344
462
  const postLaunchBatchFailure = !succeeded && processSucceeded && parseSucceeded && prepared.sessionMode === "fresh" && prepared.executionPlan.commandInfo.command === "batch" && batchStartedManagedBrowser(presentationEnvelope?.data);
345
463
  const postLaunchTimeoutWithPage = !succeeded && processResult.timedOut && prepared.sessionMode === "fresh" && prepared.executionPlan.commandInfo.command === "batch" && timeoutPartialProgress?.liveUrlRecovered === true;
@@ -361,8 +479,8 @@ export async function processBrowserOutput(input) {
361
479
  }
362
480
  // Only a confirmed inactive daemon proves that a started fresh command did not establish browser ownership.
363
481
  const postLaunchFreshFailure = failedFreshDaemon !== undefined && failedFreshDaemon.status !== "inactive";
364
- const managedTransitionSucceeded = succeeded || policyBlockedFreshManagedSession || postLaunchBatchFailure || postLaunchTimeoutWithPage || postLaunchFreshFailure;
365
- const managedSessionState = resolveManagedSessionState({ command: prepared.executionPlan.commandInfo.command, managedSessionName: managedCloseSessionName, managedSessionNamespace: prepared.executionPlan.namespace, priorActive: priorManagedSessionActive, priorNamespace: priorManagedSessionNamespace, priorSessionName: priorManagedSessionName, succeeded: managedTransitionSucceeded });
482
+ const managedTransitionSucceeded = succeeded || nestedBatchClosed || nestedBatchRemainsActive || policyBlockedFreshManagedSession || postLaunchBatchFailure || postLaunchTimeoutWithPage || postLaunchFreshFailure;
483
+ const managedSessionState = resolveManagedSessionState({ command: commandClosesSession ? "close" : prepared.executionPlan.commandInfo.command, managedSessionName: managedCloseSessionName, managedSessionNamespace: prepared.executionPlan.namespace, priorActive: priorManagedSessionActive, priorNamespace: priorManagedSessionNamespace, priorSessionName: priorManagedSessionName, succeeded: managedTransitionSucceeded });
366
484
  if (!managedTransitionSucceeded && prepared.sessionMode === "fresh" && prepared.executionPlan.managedSessionName) {
367
485
  state.managedSessionRestoreState.clear(prepared.executionPlan.managedSessionName, prepared.executionPlan.namespace);
368
486
  }
@@ -383,7 +501,7 @@ export async function processBrowserOutput(input) {
383
501
  managedSessionHeadedAutosaveDisabled = prepared.ownedManagedSessionContext?.headedManagedAutosaveDisabled === true;
384
502
  managedSessionHeadedAutosaveInterval = prepared.ownedManagedSessionContext?.headedManagedAutosaveInterval;
385
503
  }
386
- if (commandClosesSession && succeeded && managedCloseSessionName === priorManagedSessionName && !managedSessionActive) {
504
+ if (closeCommandSucceeded && managedCloseSessionName === priorManagedSessionName && !managedSessionActive) {
387
505
  const daemonRestoreKey = state.managedSessionRestoreState.getDaemonRestoreKey(managedCloseSessionName, priorManagedSessionNamespace);
388
506
  const ownedRestoreKey = !state.managedSessionRestoreState.isDisabled(managedCloseSessionName, priorManagedSessionNamespace)
389
507
  && isManagedSessionRestoreKey(daemonRestoreKey) ? daemonRestoreKey : null;
@@ -398,7 +516,7 @@ export async function processBrowserOutput(input) {
398
516
  managedSessionName = createFreshSessionName(state.managedSessionBaseName, state.ephemeralSessionSeed, freshSessionOrdinal);
399
517
  managedSessionNamespace = undefined;
400
518
  }
401
- let managedSessionOutcome = buildManagedSessionOutcome({ activeAfter: managedSessionActive, activeBefore: priorManagedSessionActive, attemptedSessionName: managedCloseSessionName, command: prepared.executionPlan.commandInfo.command, currentSessionName: managedSessionName, currentSessionNamespace: managedSessionNamespace, previousSessionName: priorManagedSessionName, replacedSessionName: replacedManagedSessionName, replacedSessionNamespace: priorManagedSessionNamespace, sessionMode: prepared.sessionMode, succeeded: managedTransitionSucceeded });
519
+ let managedSessionOutcome = buildManagedSessionOutcome({ activeAfter: managedSessionActive, activeBefore: priorManagedSessionActive, attemptedSessionName: managedCloseSessionName, command: commandClosesSession ? "close" : prepared.executionPlan.commandInfo.command, currentSessionName: managedSessionName, currentSessionNamespace: managedSessionNamespace, previousSessionName: priorManagedSessionName, replacedSessionName: replacedManagedSessionName, replacedSessionNamespace: priorManagedSessionNamespace, sessionMode: prepared.sessionMode, succeeded: managedTransitionSucceeded });
402
520
  if (prepared.executionPlan.managedSessionName && managedTransitionSucceeded && managedSessionActive) {
403
521
  managedSessionCwd = cwd;
404
522
  managedSessionNamespace = prepared.executionPlan.namespace;
@@ -439,7 +557,7 @@ export async function processBrowserOutput(input) {
439
557
  if (succeeded && prepared.executionPlan.sessionName) {
440
558
  const electronSessionName = prepared.executionPlan.sessionName;
441
559
  const electronSessionStateKey = sessionStateKey ?? electronSessionName;
442
- electronLaunchRecord = { ...prepared.electronLaunch.record, sessionName: electronSessionName };
560
+ electronLaunchRecord = { ...prepared.electronLaunch.record, namespace: prepared.executionPlan.namespace, sessionName: electronSessionName };
443
561
  const electronHandoffMode = prepared.compiledElectron?.action === "launch" ? prepared.compiledElectron.handoff : "connect";
444
562
  try {
445
563
  electronHandoff = await collectElectronHandoff({ cwd, handoff: electronHandoffMode, namespace: prepared.executionPlan.namespace, sessionName: electronSessionName, signal });
@@ -507,7 +625,7 @@ export async function processBrowserOutput(input) {
507
625
  if (presentationEnvelope?.error !== undefined)
508
626
  presentationEnvelope = { ...presentationEnvelope, error: redactClipboardPermissionErrorValue(prepared.executionPlan.commandInfo, presentationEnvelope.error, clipboardWritePayloadCandidates) };
509
627
  }
510
- const 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, 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 });
628
+ const 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 });
511
629
  if (observedPageValidationError)
512
630
  presentation.failureCategory = "validation-error";
513
631
  if (electronHandoff?.error && electronHandoff.failureCategory)
@@ -518,13 +636,20 @@ export async function processBrowserOutput(input) {
518
636
  presentationEnvelope = { ...(presentationEnvelope ?? {}), error: presentation.summary, success: false };
519
637
  }
520
638
  if (scrollNoopDiagnostic) {
639
+ succeeded = false;
640
+ presentation.resultCategory = "failure";
641
+ presentation.failureCategory = "upstream-error";
642
+ presentationEnvelope = { ...(presentationEnvelope ?? {}), error: "Scroll completed with no observed movement.", success: false };
521
643
  presentation.summary = "Scroll completed with no observed movement.";
522
644
  if (isRecord(presentation.data))
523
645
  presentation.data = { ...presentation.data, noMovement: true, scrolled: false };
524
- if (presentation.content[0]?.type === "text")
525
- presentation.content[0] = { ...presentation.content[0], text: `Scroll completed with no observed movement.\n\n${presentation.content[0].text}` };
526
- else
646
+ if (presentation.content[0]?.type === "text") {
647
+ const details = isRecord(presentation.data) ? JSON.stringify(omitUpstreamLifecycle(presentation.data), null, 2) : presentation.content[0].text;
648
+ presentation.content[0] = { ...presentation.content[0], text: `Scroll completed with no observed movement.\n\n${details}` };
649
+ }
650
+ else {
527
651
  presentation.content.unshift({ type: "text", text: "Scroll completed with no observed movement." });
652
+ }
528
653
  }
529
654
  if (parseFailureOutput.artifactManifest) {
530
655
  presentation.artifactManifest = parseFailureOutput.artifactManifest;
@@ -544,7 +669,7 @@ export async function processBrowserOutput(input) {
544
669
  let qaAttachedTarget = prepared.compiledQaPreset?.checks.attached
545
670
  ? await collectQaAttachedTarget({ currentTarget: currentSessionTabTarget ?? prepared.priorSessionTabTarget, cwd, namespace: prepared.executionPlan.namespace, sessionName: prepared.executionPlan.sessionName, signal })
546
671
  : undefined;
547
- const sourceLookupElectronContext = prepared.compiledSourceLookup ? getSourceLookupElectronContext({ currentTarget: currentSessionTabTarget, electronLaunchRecords, priorTarget: prepared.priorSessionTabTarget, sessionName: prepared.executionPlan.sessionName }) : undefined;
672
+ const sourceLookupElectronContext = prepared.compiledSourceLookup ? getSourceLookupElectronContext({ currentTarget: currentSessionTabTarget, electronLaunchRecords, namespace: prepared.executionPlan.namespace, priorTarget: prepared.priorSessionTabTarget, sessionName: prepared.executionPlan.sessionName }) : undefined;
548
673
  const sourceLookup = prepared.compiledSourceLookup ? await analyzeSourceLookupResults(presentationEnvelope?.data, prepared.compiledSourceLookup, cwd, { electronContext: sourceLookupElectronContext, workspaceRoot: cwd }) : undefined;
549
674
  const networkSourceLookup = prepared.compiledNetworkSourceLookup ? redactNetworkSourceLookupAnalysis(await analyzeNetworkSourceLookupResults(presentationEnvelope?.data, prepared.compiledNetworkSourceLookup, cwd)) : undefined;
550
675
  if (networkSourceLookup && presentation.content[0]?.type === "text")
@@ -555,14 +680,22 @@ export async function processBrowserOutput(input) {
555
680
  presentation.content[0] = { ...presentation.content[0], text: `${sourceLookup.summary}\n\n${presentation.content[0].text}` };
556
681
  else if (sourceLookup)
557
682
  presentation.content.unshift({ type: "text", text: sourceLookup.summary });
558
- if (qaPreset && !qaPreset.passed && presentation.failureCategory !== "artifact-missing") {
683
+ if (qaPreset && !qaPreset.passed && prepared.compiledQaPreset && presentation.failureCategory !== "artifact-missing") {
559
684
  succeeded = false;
560
685
  presentation.failureCategory = "qa-failure";
561
686
  presentation.summary = qaPreset.summary;
562
- if (presentation.content[0]?.type === "text")
563
- presentation.content[0] = { ...presentation.content[0], text: `${qaPreset.summary}\n\n${presentation.content[0].text}` };
564
- else
565
- presentation.content.unshift({ type: "text", text: qaPreset.summary });
687
+ const compactText = buildQaCompactFailureText({
688
+ batchStepCount: presentation.batchSteps?.length ?? prepared.compiledQaPreset.steps.length,
689
+ checks: prepared.compiledQaPreset.checks,
690
+ page: extractQaPageContext({
691
+ attachedTarget: qaAttachedTarget,
692
+ batchData: presentationEnvelope?.data,
693
+ compiled: prepared.compiledQaPreset,
694
+ }),
695
+ qaPreset,
696
+ });
697
+ const nonTextContent = presentation.content.filter((item) => item.type !== "text");
698
+ presentation.content = [{ type: "text", text: compactText }, ...nonTextContent];
566
699
  }
567
700
  else if (qaPreset?.passed && prepared.compiledQaPreset && succeeded) {
568
701
  const compactText = buildQaCompactPassText({
@@ -617,8 +750,11 @@ export async function processBrowserOutput(input) {
617
750
  ? prepared.ownedManagedSessionContext?.headedManagedAutosaveInterval
618
751
  : undefined;
619
752
  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, inspectionText, 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, userRequestedJson: prepared.userRequestedJson, visibleRefFallbackDiagnostic: finalRecoveryState.visibleRefFallbackDiagnostic, visibleRefFallbackSessionName: finalRecoveryState.visibleRefFallbackSessionName });
753
+ const resultWithCloseAll = closeAllApplied
754
+ ? { ...result, details: { ...(isRecord(result.details) ? result.details : {}), closeAllApplied: true } }
755
+ : result;
620
756
  const statePatch = { allowedDomainsBySession, artifactManifest, freshSessionOrdinal, managedSessionActive, managedSessionCompatibilityWorkaround, managedSessionHeadedAutosaveDisabled, managedSessionHeadedAutosaveInterval, managedSessionCwd, managedSessionName, managedSessionNamespace, networkRoutesBySession };
621
- return { result, statePatch };
757
+ return { result: resultWithCloseAll, statePatch };
622
758
  }
623
759
  finally {
624
760
  if (processResult.stdoutSpillPath)