pi-agent-browser-native 0.3.0 → 0.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/CHANGELOG.md +265 -0
  2. package/README.md +130 -54
  3. package/dist/extensions/agent-browser/index.js +781 -169
  4. package/dist/extensions/agent-browser/lib/argv-descriptor.js +35 -3
  5. package/dist/extensions/agent-browser/lib/argv-grammar.js +50 -2
  6. package/dist/extensions/agent-browser/lib/batch-lifecycle.js +71 -0
  7. package/dist/extensions/agent-browser/lib/command-policy.js +5 -8
  8. package/dist/extensions/agent-browser/lib/command-taxonomy.js +53 -12
  9. package/dist/extensions/agent-browser/lib/config-policy.js +25 -1
  10. package/dist/extensions/agent-browser/lib/config.js +1 -1
  11. package/dist/extensions/agent-browser/lib/input-modes/job.js +61 -13
  12. package/dist/extensions/agent-browser/lib/input-modes/lookups.js +2 -2
  13. package/dist/extensions/agent-browser/lib/input-modes/params.js +23 -24
  14. package/dist/extensions/agent-browser/lib/input-modes/script.js +462 -0
  15. package/dist/extensions/agent-browser/lib/input-modes/semantic-action.js +51 -12
  16. package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +26 -4
  17. package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +6 -139
  18. package/dist/extensions/agent-browser/lib/managed-session-restore.js +26 -116
  19. package/dist/extensions/agent-browser/lib/managed-session-snapshots.js +2 -4
  20. package/dist/extensions/agent-browser/lib/managed-session-storage.js +54 -25
  21. package/dist/extensions/agent-browser/lib/orchestration/batch-stdin.js +26 -5
  22. package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.js +110 -30
  23. package/dist/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.js +2 -1
  24. package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +54 -48
  25. package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +71 -5
  26. package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +2 -1
  27. package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +6 -7
  28. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/snapshot-filter.js +119 -2
  29. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/wait-timeouts.js +7 -6
  30. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +152 -64
  31. package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +244 -102
  32. package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +63 -37
  33. package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +20 -21
  34. package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +36 -18
  35. package/dist/extensions/agent-browser/lib/orchestration/output-file.js +41 -21
  36. package/dist/extensions/agent-browser/lib/orchestration/script-mode.js +299 -0
  37. package/dist/extensions/agent-browser/lib/page-target-validation.js +270 -0
  38. package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +32 -10
  39. package/dist/extensions/agent-browser/lib/playbook.js +29 -25
  40. package/dist/extensions/agent-browser/lib/process-environment.js +14 -0
  41. package/dist/extensions/agent-browser/lib/process-identity.js +5 -12
  42. package/dist/extensions/agent-browser/lib/process.js +130 -104
  43. package/dist/extensions/agent-browser/lib/recording-reservations.js +116 -0
  44. package/dist/extensions/agent-browser/lib/results/action-recommendations.js +63 -6
  45. package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +62 -4
  46. package/dist/extensions/agent-browser/lib/results/categories.js +6 -1
  47. package/dist/extensions/agent-browser/lib/results/next-actions.js +19 -5
  48. package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +85 -38
  49. package/dist/extensions/agent-browser/lib/results/presentation/batch.js +86 -18
  50. package/dist/extensions/agent-browser/lib/results/presentation/common.js +38 -2
  51. package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +18 -17
  52. package/dist/extensions/agent-browser/lib/results/presentation/errors.js +2 -1
  53. package/dist/extensions/agent-browser/lib/results/presentation/navigation.js +38 -20
  54. package/dist/extensions/agent-browser/lib/results/presentation/registry.js +60 -15
  55. package/dist/extensions/agent-browser/lib/results/presentation/semantic-action.js +1 -10
  56. package/dist/extensions/agent-browser/lib/results/presentation.js +36 -6
  57. package/dist/extensions/agent-browser/lib/results/recovery-actions.js +3 -1
  58. package/dist/extensions/agent-browser/lib/results/recovery-next-actions.js +9 -0
  59. package/dist/extensions/agent-browser/lib/results/selector-recovery.js +54 -11
  60. package/dist/extensions/agent-browser/lib/results/snapshot-high-value-controls.js +13 -7
  61. package/dist/extensions/agent-browser/lib/results/snapshot-spill.js +2 -1
  62. package/dist/extensions/agent-browser/lib/results/snapshot.js +4 -4
  63. package/dist/extensions/agent-browser/lib/runtime.js +186 -108
  64. package/dist/extensions/agent-browser/lib/session-page-state.js +71 -10
  65. package/dist/extensions/agent-browser/lib/temp.js +1 -2
  66. package/dist/extensions/agent-browser/lib/upstream-version.js +14 -0
  67. package/dist/extensions/agent-browser/lib/web-search.js +108 -24
  68. package/dist/extensions/agent-browser/script-worker.js +169 -0
  69. package/dist/scripts/agent-browser-target.mjs +21 -0
  70. package/docs/ARCHITECTURE.md +57 -34
  71. package/docs/COMMAND_REFERENCE.md +255 -68
  72. package/docs/ELECTRON.md +2 -2
  73. package/docs/RELEASE.md +12 -10
  74. package/docs/REQUIREMENTS.md +11 -8
  75. package/docs/SUPPORT_MATRIX.md +36 -24
  76. package/docs/TOOL_CONTRACT.md +169 -95
  77. package/package.json +3 -1
  78. package/platform-smoke.config.mjs +2 -2
  79. package/scripts/agent-browser-capability-baseline.mjs +87 -9
  80. package/scripts/agent-browser-target.mjs +21 -0
  81. package/scripts/build.mjs +41 -0
  82. package/scripts/config.mjs +1 -0
  83. package/scripts/doctor.mjs +16 -9
  84. package/scripts/platform-smoke/browser-dogfood-windows.ps1 +9 -3
  85. package/scripts/platform-smoke/targets.mjs +12 -6
  86. package/dist/extensions/agent-browser/lib/managed-session-capabilities.js +0 -20
  87. package/dist/extensions/agent-browser/lib/managed-session-state-policy.js +0 -583
  88. package/dist/extensions/agent-browser/lib/navigation-policy.js +0 -78
  89. package/dist/extensions/agent-browser/lib/results/presentation/managed-list-filter.js +0 -37
@@ -1,22 +1,24 @@
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, isOpenNavigationCommand, 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
- import { getAllowedDomainsViolation, parseAllowedDomainsPolicyFromArgs } from "../../navigation-policy.js";
7
- import { getManagedSessionResultingPageState, getObservedBrowserPageValidationError, managedSessionCommandRequiresLivePageVerification } from "../../managed-session-state-policy.js";
7
+ import { getResultingPageTargetState, commandRequiresLivePageVerification } from "../../page-target-validation.js";
8
8
  import { analyzeNetworkSourceLookupResults, analyzeSourceLookupResults, redactNetworkSourceLookupAnalysis } from "../../input-modes/lookups.js";
9
- import { analyzeQaPresetResults, analyzeQaPresetTimeout, buildQaCompactPassText, extractQaPageContext } from "../../input-modes/job.js";
9
+ import { analyzeQaPresetResults, analyzeQaPresetTimeout, buildQaCompactFailureText, buildQaCompactPassText, extractQaPageContext } from "../../input-modes/job.js";
10
10
  import { applyNetworkRouteRecords, buildNetworkRouteDiagnostics } from "../../results/network-routes.js";
11
11
  import { buildToolPresentation } from "../../results/presentation.js";
12
12
  import { getAgentBrowserErrorText, parseAgentBrowserEnvelope } from "../../results/envelope.js";
13
+ import { omitUpstreamLifecycle } from "../../results/presentation/common.js";
13
14
  import { getClipboardWritePayloadCandidates, redactClipboardPermissionEcho, redactClipboardPermissionErrorValue } from "../../results/presentation/errors.js";
14
15
  import { shouldCaptureSemanticActionNavigationSummary } from "../../results/presentation/semantic-action.js";
15
- import { commandExplicitlyTargetsAboutBlank, deriveSessionTabTarget, extractLatestRefSnapshotStateFromBatchResults, extractRefSnapshotFromData, extractSessionTabTargetFromBatchResults, extractSessionTabTargetFromCommandData, isAboutBlankSessionTabTarget, normalizeSessionTabTarget, } from "../../session-page-state.js";
16
+ import { buildPageTransitionRefSnapshotInvalidation, commandExplicitlyTargetsAboutBlank, getCommandRefSnapshotInvalidation, deriveSessionTabTarget, extractLatestRefSnapshotStateFromBatchResults, extractRefSnapshotFromData, extractSessionTabTargetFromBatchResults, extractSessionTabTargetFromCommandData, isAboutBlankSessionTabTarget, normalizeSessionTabTarget, } from "../../session-page-state.js";
16
17
  import { isRecord } from "../../parsing.js";
17
18
  import { pruneOwnedManagedSessionRestoreSnapshots } from "../../managed-session-restore.js";
18
19
  import { isManagedSessionRestoreKey } from "../../managed-session-storage.js";
19
- import { createFreshSessionName, extractCommandTokens, resolveManagedSessionState } from "../../runtime.js";
20
+ import { createFreshSessionName, extractUpstreamCommandTokens, resolveManagedSessionState } from "../../runtime.js";
21
+ import { getUpstreamEffectiveBatchSteps } from "../batch-stdin.js";
20
22
  import { closeManagedSession, inspectManagedSessionDaemon } from "./managed-session-daemon-policy.js";
21
23
  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
24
  import { collectClickDispatchDiagnostic } from "./click-dispatch.js";
@@ -61,6 +63,35 @@ function isStreamEnableAlreadyEnabledNoop(options) {
61
63
  const message = (getEnvelopeErrorString(options.envelope) ?? "").trim().replace(/[.!]+$/, "").toLowerCase();
62
64
  return message === "streaming is already enabled for this session" || message === "streaming is already enabled" || message === "stream already enabled";
63
65
  }
66
+ function isPendingWebMcpMutation(command, subcommand, data) {
67
+ return command === "webmcp"
68
+ && ["invoke", "result"].includes(subcommand ?? "")
69
+ && isRecord(data)
70
+ && data.status === "pending";
71
+ }
72
+ function isWebMcpSettlementCommand(command, subcommand) {
73
+ return command === "webmcp" && ["result", "cancel"].includes(subcommand ?? "");
74
+ }
75
+ function batchHasPendingWebMcpMutation(data) {
76
+ if (!Array.isArray(data))
77
+ return false;
78
+ return data.some((row) => {
79
+ if (!isRecord(row) || row.success === false || !Array.isArray(row.command) || !row.command.every((token) => typeof token === "string"))
80
+ return false;
81
+ const [command, subcommand] = extractUpstreamCommandTokens(row.command);
82
+ return isPendingWebMcpMutation(command, subcommand, row.result);
83
+ });
84
+ }
85
+ function batchHasFailedWebMcpSettlement(data) {
86
+ if (!Array.isArray(data))
87
+ return false;
88
+ return data.some((row) => {
89
+ if (!isRecord(row) || row.success !== false || !Array.isArray(row.command) || !row.command.every((token) => typeof token === "string"))
90
+ return false;
91
+ const [command, subcommand] = extractUpstreamCommandTokens(row.command);
92
+ return isWebMcpSettlementCommand(command, subcommand);
93
+ });
94
+ }
64
95
  function batchStartedManagedBrowser(data) {
65
96
  if (!Array.isArray(data))
66
97
  return false;
@@ -71,6 +102,15 @@ function batchStartedManagedBrowser(data) {
71
102
  return command === "connect" || command === "goto" || command === "navigate" || isOpenNavigationCommand(command);
72
103
  });
73
104
  }
105
+ function withoutNamespaceEntries(entries, namespace) {
106
+ return new Map([...entries].filter(([key]) => !isAgentBrowserSessionIdentityKeyInNamespace(key, namespace)));
107
+ }
108
+ function deleteNamespaceEntries(entries, namespace) {
109
+ for (const key of entries.keys()) {
110
+ if (isAgentBrowserSessionIdentityKeyInNamespace(key, namespace))
111
+ entries.delete(key);
112
+ }
113
+ }
74
114
  function setNetworkRouteState(options) {
75
115
  if (!options.sessionName)
76
116
  return options.routesBySession;
@@ -95,7 +135,12 @@ function applyBatchNetworkRouteState(options) {
95
135
  for (const item of options.data) {
96
136
  if (!isRecord(item) || !Array.isArray(item.command) || !item.command.every((token) => typeof token === "string"))
97
137
  continue;
98
- routes = applyNetworkRouteRecords(routes, extractCommandTokens(item.command), item.success !== false);
138
+ const commandTokens = extractUpstreamCommandTokens(item.command);
139
+ const stepSucceeded = item.success !== false;
140
+ if (stepSucceeded && isCloseCommand(commandTokens[0]))
141
+ routes = undefined;
142
+ else
143
+ routes = applyNetworkRouteRecords(routes, commandTokens, stepSucceeded);
99
144
  }
100
145
  return setNetworkRouteState({ routes, routesBySession: options.routesBySession, sessionName: options.sessionName });
101
146
  }
@@ -103,7 +148,6 @@ export async function processBrowserOutput(input) {
103
148
  const { ctx, cwd, electronPostCommandStatusSettleMs, implicitSessionCloseTimeoutMs, sessionPageStateUpdate, signal, state } = input;
104
149
  const { prepared, processResult } = input;
105
150
  const { electronChildProcesses, electronLaunchRecords, sessionPageState, traceOwners } = state;
106
- let allowedDomainsBySession = state.allowedDomainsBySession;
107
151
  let artifactManifest = state.artifactManifest;
108
152
  let freshSessionOrdinal = state.freshSessionOrdinal;
109
153
  let managedSessionActive = state.managedSessionActive;
@@ -120,6 +164,7 @@ export async function processBrowserOutput(input) {
120
164
  let parseError = parsed.parseError;
121
165
  let presentationEnvelope = parsed.envelope;
122
166
  let navigationSummary = undefined;
167
+ let failedTransitionReverification = false;
123
168
  if (prepared.pinnedBatchUnwrapMode) {
124
169
  const pinnedBatchResult = unwrapPinnedSessionBatchEnvelope({ envelope: parsed.envelope, includeNavigationSummary: prepared.includePinnedNavigationSummary, mode: prepared.pinnedBatchUnwrapMode });
125
170
  parseError = pinnedBatchResult.parseError ?? parseError;
@@ -132,11 +177,22 @@ export async function processBrowserOutput(input) {
132
177
  presentationEnvelope = repairedBatchScreenshots.envelope;
133
178
  const screenshotArtifactRequest = repairedScreenshot.request;
134
179
  const batchScreenshotArtifactRequests = repairedBatchScreenshots.requests;
180
+ const batchCommandSteps = prepared.executionPlan.commandInfo.command === "batch"
181
+ ? getUpstreamEffectiveBatchSteps(prepared.commandTokens, prepared.runtimeToolStdin)
182
+ : [];
183
+ const nestedBatchClose = prepared.executionPlan.commandInfo.command === "batch"
184
+ ? getSuccessfulBatchCloseLifecycle(presentationEnvelope?.data, batchCommandSteps)
185
+ : undefined;
186
+ const nestedBatchClosed = nestedBatchClose?.endsClosed === true;
187
+ const nestedBatchRemainsActive = nestedBatchClose?.endsClosed === false;
188
+ const nestedBatchClosesAll = prepared.executionPlan.commandInfo.command === "batch"
189
+ && batchHasSuccessfulCloseAll(presentationEnvelope?.data, batchCommandSteps);
190
+ const directCloseAllRequested = isCloseAllCommand(extractUpstreamCommandTokens(prepared.commandTokens));
135
191
  const rawCloseStatePath = isCloseCommand(prepared.executionPlan.commandInfo.command)
136
192
  && isRecord(presentationEnvelope?.data)
137
193
  && typeof presentationEnvelope.data.statePath === "string"
138
194
  ? presentationEnvelope.data.statePath
139
- : undefined;
195
+ : nestedBatchClose?.statePath;
140
196
  if (presentationEnvelope && prepared.exactSensitiveValues.length > 0)
141
197
  presentationEnvelope = redactExactSensitiveValue(presentationEnvelope, prepared.exactSensitiveValues);
142
198
  const parseFailureOutput = parseError && processResult.stdoutSpillPath
@@ -152,7 +208,41 @@ export async function processBrowserOutput(input) {
152
208
  let succeeded = processSucceeded && parseSucceeded && envelopeSuccess;
153
209
  const inspectionText = plainTextInspection ? processResult.stdout.trim() : undefined;
154
210
  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 });
211
+ const closeAllApplied = nestedBatchClosesAll || (directCloseAllRequested && succeeded);
212
+ if (closeAllApplied) {
213
+ networkRoutesBySession = withoutNamespaceEntries(networkRoutesBySession, prepared.executionPlan.namespace);
214
+ deleteNamespaceEntries(state.attachedSessionKeys, prepared.executionPlan.namespace);
215
+ deleteNamespaceEntries(traceOwners, prepared.executionPlan.namespace);
216
+ sessionPageState.clearNamespace(prepared.executionPlan.namespace);
217
+ const retainedSessionKey = nestedBatchRemainsActive ? sessionStateKey : undefined;
218
+ for (const [key, owner] of state.ownedManagedSessions) {
219
+ if (!isAgentBrowserSessionIdentityKeyInNamespace(key, prepared.executionPlan.namespace) || key === retainedSessionKey)
220
+ continue;
221
+ state.closedManagedSessionNames.add(key);
222
+ state.managedSessionRestoreState.clear(owner.sessionName, owner.namespace);
223
+ }
224
+ }
225
+ else if (nestedBatchClose && sessionStateKey) {
226
+ networkRoutesBySession = new Map(networkRoutesBySession);
227
+ networkRoutesBySession.delete(sessionStateKey);
228
+ sessionPageState.clearSession(sessionStateKey);
229
+ }
230
+ if (prepared.executionPlan.commandInfo.command === "batch" && Array.isArray(presentationEnvelope?.data)) {
231
+ for (const [index, row] of presentationEnvelope.data.entries()) {
232
+ if (!isRecord(row))
233
+ continue;
234
+ const rowCommand = Array.isArray(row.command) && row.command.every((token) => typeof token === "string")
235
+ ? row.command
236
+ : batchCommandSteps[index];
237
+ if (!rowCommand)
238
+ continue;
239
+ const [command, subcommand] = extractUpstreamCommandTokens(rowCommand);
240
+ updateTraceOwnerState({ command, sessionName: sessionStateKey, subcommand, succeeded: row.success === true, traceOwners });
241
+ }
242
+ }
243
+ else {
244
+ updateTraceOwnerState({ command: prepared.executionPlan.commandInfo.command, sessionName: sessionStateKey, subcommand: prepared.executionPlan.commandInfo.subcommand, succeeded, traceOwners });
245
+ }
156
246
  let clickDispatchDiagnostic;
157
247
  if (succeeded && prepared.clickDispatchProbe) {
158
248
  clickDispatchDiagnostic = await collectClickDispatchDiagnostic({ cwd, namespace: prepared.executionPlan.namespace, probe: prepared.clickDispatchProbe, sessionName: prepared.executionPlan.sessionName, signal });
@@ -161,29 +251,38 @@ export async function processBrowserOutput(input) {
161
251
  presentationEnvelope = { ...(presentationEnvelope ?? {}), error: clickDispatchDiagnostic.summary, success: false };
162
252
  }
163
253
  }
164
- const resultingPageState = getManagedSessionResultingPageState({
254
+ const tabTransition = prepared.executionPlan.commandInfo.command === "tab" && prepared.executionPlan.commandInfo.subcommand !== undefined && !["list", "new"].includes(prepared.executionPlan.commandInfo.subcommand);
255
+ const resultingPageState = getResultingPageTargetState({
165
256
  args: prepared.executionPlan.effectiveArgs,
166
257
  currentPageUrl: prepared.priorSessionTabTarget?.url,
167
258
  pageUrlUnknown: prepared.priorSessionTabTargetUnknown === true,
168
259
  stdin: prepared.runtimeToolStdin,
169
260
  trustedFirstBatchTabSelection: prepared.pinnedBatchUnwrapMode !== undefined,
170
261
  });
171
- const presentationDataRecord = isRecord(presentationEnvelope?.data) ? presentationEnvelope.data : undefined;
172
- const dataClicked = typeof presentationDataRecord?.clicked === "string" ? presentationDataRecord.clicked : undefined;
173
- const cssClickWithoutHref = prepared.executionPlan.commandInfo.command === "click" && dataClicked !== undefined && !dataClicked.startsWith("@") && !dataClicked.startsWith("ref=") && typeof presentationDataRecord?.href !== "string";
174
- const parsedAllowedDomainsPolicy = parseAllowedDomainsPolicyFromArgs(prepared.runtimeToolArgs);
175
- const sessionAllowedDomainsPolicy = sessionStateKey
176
- ? parsedAllowedDomainsPolicy ?? allowedDomainsBySession.get(sessionStateKey)
177
- : parsedAllowedDomainsPolicy;
178
- const shouldCaptureAllowedDomainNavigationSummary = sessionAllowedDomainsPolicy !== undefined && !cssClickWithoutHref && (prepared.executionPlan.commandInfo.command === "batch" || isNavigationObservableCommandName(prepared.executionPlan.commandInfo.command));
179
262
  if (succeeded &&
180
263
  !navigationSummary &&
181
- (shouldCaptureNavigationSummary(prepared.executionPlan.commandInfo.command, presentationEnvelope?.data) ||
264
+ (shouldCaptureNavigationSummary(prepared.executionPlan.commandInfo.command, presentationEnvelope?.data, prepared.executionPlan.commandInfo.subcommand) ||
182
265
  shouldCaptureSemanticActionNavigationSummary(prepared.compiledSemanticAction, presentationEnvelope?.data) ||
183
- shouldCaptureAllowedDomainNavigationSummary ||
184
- managedSessionCommandRequiresLivePageVerification(prepared.executionPlan.effectiveArgs, prepared.runtimeToolStdin) ||
185
- (prepared.executionPlan.commandInfo.command === "tab" && prepared.executionPlan.commandInfo.subcommand === "close"))) {
186
- navigationSummary = await collectNavigationSummary({ cwd, namespace: prepared.executionPlan.namespace, sessionName: prepared.executionPlan.sessionName, signal });
266
+ commandRequiresLivePageVerification(prepared.executionPlan.effectiveArgs, prepared.runtimeToolStdin) ||
267
+ tabTransition)) {
268
+ navigationSummary = await collectNavigationSummary({ cwd, namespace: prepared.executionPlan.namespace, priorTarget: prepared.priorSessionTabTarget, reusePriorTitle: !tabTransition, sessionName: prepared.executionPlan.sessionName, signal });
269
+ }
270
+ // A failed eval/back/forward/reload/state-load/tab would otherwise drop the page to unverified and force the
271
+ // agent through a manual get url round trip. Probe the live URL ourselves instead: an observed page
272
+ // stays verified, and a failed
273
+ // probe preserves the existing unknown-target behavior.
274
+ if (succeeded === false &&
275
+ !navigationSummary &&
276
+ processResult.agentBrowserStarted &&
277
+ !processResult.aborted &&
278
+ !processResult.timedOut &&
279
+ prepared.executionPlan.commandInfo.command !== "batch" &&
280
+ isUnverifiedPageTransitionCommand(prepared.executionPlan.commandInfo.command, prepared.executionPlan.commandInfo.subcommand)) {
281
+ navigationSummary = await collectNavigationSummary({ cwd, namespace: prepared.executionPlan.namespace, priorTarget: prepared.priorSessionTabTarget, sessionName: prepared.executionPlan.sessionName, signal });
282
+ // A failed transition command can still have mutated or replaced the document before throwing, so
283
+ // keeping the verified URL must not keep the prior snapshot refs (markTabTargetUnknown dropped
284
+ // them before this probe existed); invalidate so the next ref use requires a fresh snapshot.
285
+ failedTransitionReverification = navigationSummary !== undefined;
187
286
  }
188
287
  if (navigationSummary && presentationEnvelope && prepared.executionPlan.commandInfo.command !== "eval" && !Array.isArray(presentationEnvelope.data))
189
288
  presentationEnvelope = { ...presentationEnvelope, data: mergeNavigationSummaryIntoData(presentationEnvelope.data, navigationSummary) };
@@ -198,24 +297,28 @@ export async function processBrowserOutput(input) {
198
297
  }
199
298
  const verifiesCurrentUrl = prepared.executionPlan.commandInfo.command === "get" && prepared.executionPlan.commandInfo.subcommand === "url";
200
299
  const trustsReportedPageTarget = !resultingPageState.pageUrlUnknown || verifiesCurrentUrl;
201
- const observedSessionTabTarget = normalizeSessionTabTarget(navigationSummary)
202
- ?? (trustsReportedPageTarget ? extractSessionTabTargetFromBatchResults(presentationEnvelope?.data) : undefined)
203
- ?? (succeeded && trustsReportedPageTarget ? extractSessionTabTargetFromCommandData(prepared.commandTokens, presentationEnvelope?.data) : undefined);
204
- const observedPageValidationError = succeeded ? getObservedBrowserPageValidationError(prepared.executionPlan.effectiveArgs, observedSessionTabTarget?.url, cwd) : undefined;
205
- if (observedPageValidationError) {
206
- succeeded = false;
207
- presentationEnvelope = { error: observedPageValidationError, success: false };
300
+ const pendingWebMcpMutation = isPendingWebMcpMutation(prepared.executionPlan.commandInfo.command, prepared.executionPlan.commandInfo.subcommand, presentationEnvelope?.data) || (prepared.executionPlan.commandInfo.command === "batch" && batchHasPendingWebMcpMutation(presentationEnvelope?.data));
301
+ const failedWebMcpSettlement = prepared.priorSessionTabTargetUnknown === true && ((!succeeded && isWebMcpSettlementCommand(prepared.executionPlan.commandInfo.command, prepared.executionPlan.commandInfo.subcommand))
302
+ || (prepared.executionPlan.commandInfo.command === "batch" && batchHasFailedWebMcpSettlement(presentationEnvelope?.data)));
303
+ const unsettledWebMcpMutation = pendingWebMcpMutation || failedWebMcpSettlement;
304
+ const observedSessionTabTarget = unsettledWebMcpMutation
305
+ ? undefined
306
+ : normalizeSessionTabTarget(navigationSummary)
307
+ ?? (trustsReportedPageTarget ? extractSessionTabTargetFromBatchResults(presentationEnvelope?.data) : undefined)
308
+ ?? (succeeded && trustsReportedPageTarget ? extractSessionTabTargetFromCommandData(prepared.commandTokens, presentationEnvelope?.data) : undefined);
309
+ const safeObservedSessionTabTarget = observedSessionTabTarget;
310
+ let currentSessionTabTarget = safeObservedSessionTabTarget;
311
+ if (!currentSessionTabTarget && nestedBatchClose === undefined) {
312
+ currentSessionTabTarget = resultingPageState.pageTargetMayHaveChanged
313
+ ? succeeded ? normalizeSessionTabTarget({ url: resultingPageState.currentPageUrl }) : undefined
314
+ : deriveSessionTabTarget({ command: prepared.executionPlan.commandInfo.command, data: presentationEnvelope?.data, navigationSummary, previousTarget: prepared.priorSessionTabTarget, subcommand: prepared.executionPlan.commandInfo.subcommand });
208
315
  }
209
- 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 }));
213
316
  let aboutBlankSessionMismatch;
214
317
  let electronPostCommandHealth;
215
318
  let electronRefFreshnessDiagnostic;
216
319
  let electronSessionMismatch;
217
320
  let electronStatusAfterCommand;
218
- const shouldTreatAboutBlankAsMismatch = succeeded && prepared.priorSessionTabTarget !== undefined && !isAboutBlankSessionTabTarget(prepared.priorSessionTabTarget) && isAboutBlankSessionTabTarget(observedSessionTabTarget ?? currentSessionTabTarget) && !commandExplicitlyTargetsAboutBlank(prepared.commandTokens);
321
+ const shouldTreatAboutBlankAsMismatch = succeeded && !tabTransition && nestedBatchClose === undefined && prepared.priorSessionTabTarget !== undefined && !isAboutBlankSessionTabTarget(prepared.priorSessionTabTarget) && isAboutBlankSessionTabTarget(observedSessionTabTarget ?? currentSessionTabTarget) && !commandExplicitlyTargetsAboutBlank(prepared.commandTokens);
219
322
  let sessionTabCorrection = prepared.sessionTabCorrection;
220
323
  if (shouldTreatAboutBlankAsMismatch && prepared.priorSessionTabTarget) {
221
324
  const aboutBlankObservedTarget = observedSessionTabTarget ?? currentSessionTabTarget;
@@ -228,7 +331,7 @@ export async function processBrowserOutput(input) {
228
331
  else
229
332
  currentSessionTabTarget = aboutBlankObservedTarget ?? normalizeSessionTabTarget({ url: "about:blank" });
230
333
  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);
334
+ const electronRecord = findElectronLaunchRecordForSession(prepared.executionPlan.sessionName, electronLaunchRecords, prepared.executionPlan.namespace);
232
335
  if (electronRecord && prepared.executionPlan.sessionName) {
233
336
  electronStatusAfterCommand = await inspectElectronLaunchStatus(electronRecord);
234
337
  electronSessionMismatch = buildElectronSessionMismatch({ managedSession: { sessionName: prepared.executionPlan.sessionName, title: aboutBlankObservedTarget?.title, url: aboutBlankObservedTarget?.url ?? "about:blank" }, record: electronRecord, statusTargets: electronStatusAfterCommand.targets });
@@ -242,19 +345,7 @@ export async function processBrowserOutput(input) {
242
345
  sessionTabCorrection = appliedPostCommandCorrection;
243
346
  }
244
347
  }
245
- if (succeeded && sessionStateKey && parsedAllowedDomainsPolicy) {
246
- allowedDomainsBySession = new Map(allowedDomainsBySession);
247
- allowedDomainsBySession.set(sessionStateKey, parsedAllowedDomainsPolicy);
248
- }
249
- const allowedDomainsViolation = succeeded ? getAllowedDomainsViolation({
250
- policy: sessionAllowedDomainsPolicy,
251
- url: currentSessionTabTarget?.url ?? observedSessionTabTarget?.url ?? navigationSummary?.url,
252
- }) : undefined;
253
- if (allowedDomainsViolation) {
254
- succeeded = false;
255
- presentationEnvelope = { ...(presentationEnvelope ?? {}), error: allowedDomainsViolation.summary, success: false };
256
- }
257
- const electronRecordForCommand = findElectronLaunchRecordForSession(prepared.executionPlan.sessionName, electronLaunchRecords);
348
+ const electronRecordForCommand = findElectronLaunchRecordForSession(prepared.executionPlan.sessionName, electronLaunchRecords, prepared.executionPlan.namespace);
258
349
  if (succeeded && electronRecordForCommand && shouldInspectElectronPostCommandHealth(prepared.executionPlan.commandInfo.command)) {
259
350
  electronStatusAfterCommand ??= await inspectElectronLaunchStatus(electronRecordForCommand);
260
351
  electronPostCommandHealth = buildElectronPostCommandHealthDiagnostic({ command: prepared.executionPlan.commandInfo.command, record: electronRecordForCommand, status: electronStatusAfterCommand, target: observedSessionTabTarget ?? currentSessionTabTarget });
@@ -288,7 +379,7 @@ export async function processBrowserOutput(input) {
288
379
  if (succeeded) {
289
380
  selectorTextVisibilityDiagnostics = await collectSelectorTextVisibilityDiagnostics({ commandInfo: prepared.executionPlan.commandInfo, commandTokens: prepared.commandTokens, cwd, data: presentationEnvelope?.data, namespace: prepared.executionPlan.namespace, sessionName: prepared.executionPlan.sessionName, signal });
290
381
  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 });
382
+ 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
383
  }
293
384
  const activeNetworkRoutes = sessionStateKey ? networkRoutesBySession.get(sessionStateKey) : undefined;
294
385
  const networkRouteDiagnostics = succeeded && prepared.executionPlan.commandInfo.command === "network" && prepared.executionPlan.commandInfo.subcommand === "requests" && prepared.executionPlan.sessionName
@@ -298,36 +389,65 @@ export async function processBrowserOutput(input) {
298
389
  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
390
  const recordingDependencyWarning = await collectRecordingDependencyWarning({ command: prepared.executionPlan.commandInfo.command, commandTokens: prepared.commandTokens, succeeded });
300
391
  const scrollNoopDiagnostic = succeeded && prepared.shouldProbeScrollNoop ? buildScrollNoopDiagnostic(prepared.scrollPositionBefore, await collectScrollPositionSnapshot({ cwd, namespace: prepared.executionPlan.namespace, sessionName: prepared.executionPlan.sessionName, signal })) : undefined;
392
+ const batchRefSnapshotState = prepared.executionPlan.commandInfo.command === "batch" ? extractLatestRefSnapshotStateFromBatchResults(presentationEnvelope?.data) : undefined;
301
393
  let currentRefSnapshot;
302
394
  let currentRefSnapshotInvalidation;
303
- const batchRefSnapshotState = prepared.executionPlan.commandInfo.command === "batch" ? extractLatestRefSnapshotStateFromBatchResults(presentationEnvelope?.data) : undefined;
304
395
  if (sessionStateKey) {
305
- if (isCloseCommand(prepared.executionPlan.commandInfo.command) && succeeded) {
306
- allowedDomainsBySession = new Map(allowedDomainsBySession);
307
- allowedDomainsBySession.delete(sessionStateKey);
396
+ const sessionClosed = (isCloseCommand(prepared.executionPlan.commandInfo.command) && succeeded) || nestedBatchClosed;
397
+ if (sessionClosed) {
398
+ state.attachedSessionKeys.delete(sessionStateKey);
308
399
  networkRoutesBySession = new Map(networkRoutesBySession);
309
400
  networkRoutesBySession.delete(sessionStateKey);
310
401
  sessionPageState.clearSession(sessionStateKey);
311
402
  state.closedManagedSessionNames.add(sessionStateKey);
312
403
  }
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
404
  else {
328
- const stateView = sessionPageState.get(sessionStateKey);
329
- currentRefSnapshot = stateView.refSnapshot;
330
- currentRefSnapshotInvalidation = stateView.refSnapshotInvalidation;
405
+ // A batch that times out or returns unparseable output yields no result rows, but the daemon
406
+ // may already have executed a recording swap or page-provided WebMCP tool; fall back to the
407
+ // planned steps then. This can over-invalidate by one snapshot, but never under-invalidates.
408
+ const directTransitionInvalidation = getCommandRefSnapshotInvalidation(prepared.commandTokens);
409
+ const plannedBatchTransitionInvalidation = !Array.isArray(presentationEnvelope?.data)
410
+ ? batchCommandSteps.map(getCommandRefSnapshotInvalidation).find((invalidation) => invalidation !== undefined)
411
+ : undefined;
412
+ const pageTransitionInvalidation = unsettledWebMcpMutation
413
+ ? buildPageTransitionRefSnapshotInvalidation("A detached WebMCP invocation is still pending or failed to settle and can mutate, rerender, or navigate the page, so prior snapshot refs remain invalid after URL verification. Run webmcp result or cancel, then take a fresh snapshot before using page-scoped refs.")
414
+ : processResult.agentBrowserStarted && (directTransitionInvalidation || plannedBatchTransitionInvalidation)
415
+ ? directTransitionInvalidation ?? plannedBatchTransitionInvalidation
416
+ : failedTransitionReverification
417
+ ? 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.")
418
+ : batchRefSnapshotState?.invalidation?.reason === "page-transition"
419
+ ? batchRefSnapshotState.invalidation
420
+ : undefined;
421
+ if (currentSessionTabTarget) {
422
+ const tabUpdate = sessionPageState.applyTabTarget({ sessionName: sessionStateKey, target: currentSessionTabTarget, update: sessionPageStateUpdate });
423
+ if (!tabUpdate.applied && succeeded)
424
+ sessionPageState.markPinning(sessionStateKey, "drift");
425
+ }
426
+ else if (processResult.agentBrowserStarted && (resultingPageState.pageUrlUnknown || resultingPageState.pageTargetMayHaveChanged)) {
427
+ sessionPageState.markTabTargetUnknown({ sessionName: sessionStateKey, update: sessionPageStateUpdate });
428
+ }
429
+ const refSnapshot = unsettledWebMcpMutation
430
+ ? undefined
431
+ : prepared.executionPlan.commandInfo.command === "batch"
432
+ ? batchRefSnapshotState?.snapshot
433
+ : succeeded
434
+ ? prepared.executionPlan.commandInfo.command === "snapshot" ? extractRefSnapshotFromData(presentationEnvelope?.data) : prepared.resolvedSemanticActionRefSnapshot ?? overlayBlockerDiagnostic?.snapshot
435
+ : undefined;
436
+ if (refSnapshot) {
437
+ const refUpdate = sessionPageState.applyRefSnapshot({ fallbackTarget: currentSessionTabTarget, sessionName: sessionStateKey, snapshot: refSnapshot, update: sessionPageStateUpdate });
438
+ currentRefSnapshot = refUpdate.refSnapshot;
439
+ currentRefSnapshotInvalidation = refUpdate.refSnapshotInvalidation;
440
+ }
441
+ else if (pageTransitionInvalidation) {
442
+ const refUpdate = sessionPageState.applyRefSnapshotInvalidation({ invalidation: pageTransitionInvalidation, sessionName: sessionStateKey, update: sessionPageStateUpdate });
443
+ currentRefSnapshot = refUpdate.refSnapshot;
444
+ currentRefSnapshotInvalidation = refUpdate.refSnapshotInvalidation;
445
+ }
446
+ else {
447
+ const stateView = sessionPageState.get(sessionStateKey);
448
+ currentRefSnapshot = stateView.refSnapshot;
449
+ currentRefSnapshotInvalidation = stateView.refSnapshotInvalidation;
450
+ }
331
451
  }
332
452
  }
333
453
  const priorManagedSessionActive = managedSessionActive;
@@ -335,12 +455,22 @@ export async function processBrowserOutput(input) {
335
455
  const priorManagedSessionHeadedAutosaveInterval = managedSessionHeadedAutosaveInterval;
336
456
  const priorManagedSessionName = managedSessionName;
337
457
  const priorManagedSessionNamespace = managedSessionNamespace;
338
- const commandClosesSession = isCloseCommand(prepared.executionPlan.commandInfo.command);
458
+ const priorManagedSessionKey = getSessionContextKey(priorManagedSessionName, priorManagedSessionNamespace) ?? priorManagedSessionName;
459
+ const closeAllTargetsPriorManagedSession = closeAllApplied
460
+ && priorManagedSessionActive
461
+ && isAgentBrowserSessionIdentityKeyInNamespace(priorManagedSessionKey, prepared.executionPlan.namespace);
462
+ const closeAllRetainsPriorManagedSession = closeAllTargetsPriorManagedSession
463
+ && nestedBatchRemainsActive
464
+ && sessionStateKey === priorManagedSessionKey;
465
+ const closeAllClosesPriorManagedSession = closeAllTargetsPriorManagedSession && !closeAllRetainsPriorManagedSession;
466
+ const commandClosesSession = isCloseCommand(prepared.executionPlan.commandInfo.command) || nestedBatchClosed || closeAllClosesPriorManagedSession;
467
+ const closeCommandSucceeded = (isCloseCommand(prepared.executionPlan.commandInfo.command) && succeeded) || nestedBatchClosed || closeAllClosesPriorManagedSession;
339
468
  const closeTargetsPriorManagedNamespace = prepared.executionPlan.namespace === priorManagedSessionNamespace;
340
- const managedCloseSessionName = commandClosesSession && succeeded && prepared.executionPlan.sessionName === priorManagedSessionName && closeTargetsPriorManagedNamespace
341
- ? prepared.executionPlan.sessionName
342
- : prepared.executionPlan.managedSessionName;
343
- const policyBlockedFreshManagedSession = allowedDomainsViolation !== undefined && prepared.sessionMode === "fresh" && prepared.executionPlan.managedSessionName === prepared.executionPlan.sessionName;
469
+ const managedCloseSessionName = closeAllClosesPriorManagedSession
470
+ ? priorManagedSessionName
471
+ : closeCommandSucceeded && prepared.executionPlan.sessionName === priorManagedSessionName && closeTargetsPriorManagedNamespace
472
+ ? prepared.executionPlan.sessionName
473
+ : prepared.executionPlan.managedSessionName;
344
474
  const postLaunchBatchFailure = !succeeded && processSucceeded && parseSucceeded && prepared.sessionMode === "fresh" && prepared.executionPlan.commandInfo.command === "batch" && batchStartedManagedBrowser(presentationEnvelope?.data);
345
475
  const postLaunchTimeoutWithPage = !succeeded && processResult.timedOut && prepared.sessionMode === "fresh" && prepared.executionPlan.commandInfo.command === "batch" && timeoutPartialProgress?.liveUrlRecovered === true;
346
476
  const failedFreshSessionMayHaveStarted = !succeeded
@@ -361,8 +491,8 @@ export async function processBrowserOutput(input) {
361
491
  }
362
492
  // Only a confirmed inactive daemon proves that a started fresh command did not establish browser ownership.
363
493
  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 });
494
+ const managedTransitionSucceeded = succeeded || nestedBatchClosed || nestedBatchRemainsActive || postLaunchBatchFailure || postLaunchTimeoutWithPage || postLaunchFreshFailure;
495
+ 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
496
  if (!managedTransitionSucceeded && prepared.sessionMode === "fresh" && prepared.executionPlan.managedSessionName) {
367
497
  state.managedSessionRestoreState.clear(prepared.executionPlan.managedSessionName, prepared.executionPlan.namespace);
368
498
  }
@@ -383,7 +513,7 @@ export async function processBrowserOutput(input) {
383
513
  managedSessionHeadedAutosaveDisabled = prepared.ownedManagedSessionContext?.headedManagedAutosaveDisabled === true;
384
514
  managedSessionHeadedAutosaveInterval = prepared.ownedManagedSessionContext?.headedManagedAutosaveInterval;
385
515
  }
386
- if (commandClosesSession && succeeded && managedCloseSessionName === priorManagedSessionName && !managedSessionActive) {
516
+ if (closeCommandSucceeded && managedCloseSessionName === priorManagedSessionName && !managedSessionActive) {
387
517
  const daemonRestoreKey = state.managedSessionRestoreState.getDaemonRestoreKey(managedCloseSessionName, priorManagedSessionNamespace);
388
518
  const ownedRestoreKey = !state.managedSessionRestoreState.isDisabled(managedCloseSessionName, priorManagedSessionNamespace)
389
519
  && isManagedSessionRestoreKey(daemonRestoreKey) ? daemonRestoreKey : null;
@@ -398,7 +528,7 @@ export async function processBrowserOutput(input) {
398
528
  managedSessionName = createFreshSessionName(state.managedSessionBaseName, state.ephemeralSessionSeed, freshSessionOrdinal);
399
529
  managedSessionNamespace = undefined;
400
530
  }
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 });
531
+ 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
532
  if (prepared.executionPlan.managedSessionName && managedTransitionSucceeded && managedSessionActive) {
403
533
  managedSessionCwd = cwd;
404
534
  managedSessionNamespace = prepared.executionPlan.namespace;
@@ -410,9 +540,7 @@ export async function processBrowserOutput(input) {
410
540
  sessionPageState.clearRestorePinning(sessionStateKey);
411
541
  }
412
542
  if (replacedManagedSessionName) {
413
- allowedDomainsBySession = new Map(allowedDomainsBySession);
414
543
  const replacedSessionStateKey = getSessionContextKey(replacedManagedSessionName, priorManagedSessionNamespace);
415
- allowedDomainsBySession.delete(replacedSessionStateKey ?? replacedManagedSessionName);
416
544
  networkRoutesBySession = new Map(networkRoutesBySession);
417
545
  networkRoutesBySession.delete(replacedSessionStateKey ?? replacedManagedSessionName);
418
546
  sessionPageState.clearSession(replacedSessionStateKey ?? replacedManagedSessionName);
@@ -439,7 +567,7 @@ export async function processBrowserOutput(input) {
439
567
  if (succeeded && prepared.executionPlan.sessionName) {
440
568
  const electronSessionName = prepared.executionPlan.sessionName;
441
569
  const electronSessionStateKey = sessionStateKey ?? electronSessionName;
442
- electronLaunchRecord = { ...prepared.electronLaunch.record, sessionName: electronSessionName };
570
+ electronLaunchRecord = { ...prepared.electronLaunch.record, namespace: prepared.executionPlan.namespace, sessionName: electronSessionName };
443
571
  const electronHandoffMode = prepared.compiledElectron?.action === "launch" ? prepared.compiledElectron.handoff : "connect";
444
572
  try {
445
573
  electronHandoff = await collectElectronHandoff({ cwd, handoff: electronHandoffMode, namespace: prepared.executionPlan.namespace, sessionName: electronSessionName, signal });
@@ -467,8 +595,6 @@ export async function processBrowserOutput(input) {
467
595
  }
468
596
  if (!closeError) {
469
597
  state.closedManagedSessionNames.add(electronSessionStateKey);
470
- allowedDomainsBySession = new Map(allowedDomainsBySession);
471
- allowedDomainsBySession.delete(electronSessionStateKey);
472
598
  networkRoutesBySession = new Map(networkRoutesBySession);
473
599
  networkRoutesBySession.delete(electronSessionStateKey);
474
600
  sessionPageState.clearSession(electronSessionStateKey);
@@ -507,9 +633,7 @@ export async function processBrowserOutput(input) {
507
633
  if (presentationEnvelope?.error !== undefined)
508
634
  presentationEnvelope = { ...presentationEnvelope, error: redactClipboardPermissionErrorValue(prepared.executionPlan.commandInfo, presentationEnvelope.error, clipboardWritePayloadCandidates) };
509
635
  }
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 });
511
- if (observedPageValidationError)
512
- presentation.failureCategory = "validation-error";
636
+ 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 });
513
637
  if (electronHandoff?.error && electronHandoff.failureCategory)
514
638
  presentation.failureCategory = electronHandoff.failureCategory;
515
639
  networkRoutesBySession = applyBatchNetworkRouteState({ data: presentationEnvelope?.data, routesBySession: networkRoutesBySession, sessionName: sessionStateKey, succeeded });
@@ -518,13 +642,20 @@ export async function processBrowserOutput(input) {
518
642
  presentationEnvelope = { ...(presentationEnvelope ?? {}), error: presentation.summary, success: false };
519
643
  }
520
644
  if (scrollNoopDiagnostic) {
645
+ succeeded = false;
646
+ presentation.resultCategory = "failure";
647
+ presentation.failureCategory = "upstream-error";
648
+ presentationEnvelope = { ...(presentationEnvelope ?? {}), error: "Scroll completed with no observed movement.", success: false };
521
649
  presentation.summary = "Scroll completed with no observed movement.";
522
650
  if (isRecord(presentation.data))
523
651
  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
652
+ if (presentation.content[0]?.type === "text") {
653
+ const details = isRecord(presentation.data) ? JSON.stringify(omitUpstreamLifecycle(presentation.data), null, 2) : presentation.content[0].text;
654
+ presentation.content[0] = { ...presentation.content[0], text: `Scroll completed with no observed movement.\n\n${details}` };
655
+ }
656
+ else {
527
657
  presentation.content.unshift({ type: "text", text: "Scroll completed with no observed movement." });
658
+ }
528
659
  }
529
660
  if (parseFailureOutput.artifactManifest) {
530
661
  presentation.artifactManifest = parseFailureOutput.artifactManifest;
@@ -544,7 +675,7 @@ export async function processBrowserOutput(input) {
544
675
  let qaAttachedTarget = prepared.compiledQaPreset?.checks.attached
545
676
  ? await collectQaAttachedTarget({ currentTarget: currentSessionTabTarget ?? prepared.priorSessionTabTarget, cwd, namespace: prepared.executionPlan.namespace, sessionName: prepared.executionPlan.sessionName, signal })
546
677
  : undefined;
547
- const sourceLookupElectronContext = prepared.compiledSourceLookup ? getSourceLookupElectronContext({ currentTarget: currentSessionTabTarget, electronLaunchRecords, priorTarget: prepared.priorSessionTabTarget, sessionName: prepared.executionPlan.sessionName }) : undefined;
678
+ const sourceLookupElectronContext = prepared.compiledSourceLookup ? getSourceLookupElectronContext({ currentTarget: currentSessionTabTarget, electronLaunchRecords, namespace: prepared.executionPlan.namespace, priorTarget: prepared.priorSessionTabTarget, sessionName: prepared.executionPlan.sessionName }) : undefined;
548
679
  const sourceLookup = prepared.compiledSourceLookup ? await analyzeSourceLookupResults(presentationEnvelope?.data, prepared.compiledSourceLookup, cwd, { electronContext: sourceLookupElectronContext, workspaceRoot: cwd }) : undefined;
549
680
  const networkSourceLookup = prepared.compiledNetworkSourceLookup ? redactNetworkSourceLookupAnalysis(await analyzeNetworkSourceLookupResults(presentationEnvelope?.data, prepared.compiledNetworkSourceLookup, cwd)) : undefined;
550
681
  if (networkSourceLookup && presentation.content[0]?.type === "text")
@@ -555,14 +686,22 @@ export async function processBrowserOutput(input) {
555
686
  presentation.content[0] = { ...presentation.content[0], text: `${sourceLookup.summary}\n\n${presentation.content[0].text}` };
556
687
  else if (sourceLookup)
557
688
  presentation.content.unshift({ type: "text", text: sourceLookup.summary });
558
- if (qaPreset && !qaPreset.passed && presentation.failureCategory !== "artifact-missing") {
689
+ if (qaPreset && !qaPreset.passed && prepared.compiledQaPreset && presentation.failureCategory !== "artifact-missing") {
559
690
  succeeded = false;
560
691
  presentation.failureCategory = "qa-failure";
561
692
  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 });
693
+ const compactText = buildQaCompactFailureText({
694
+ batchStepCount: presentation.batchSteps?.length ?? prepared.compiledQaPreset.steps.length,
695
+ checks: prepared.compiledQaPreset.checks,
696
+ page: extractQaPageContext({
697
+ attachedTarget: qaAttachedTarget,
698
+ batchData: presentationEnvelope?.data,
699
+ compiled: prepared.compiledQaPreset,
700
+ }),
701
+ qaPreset,
702
+ });
703
+ const nonTextContent = presentation.content.filter((item) => item.type !== "text");
704
+ presentation.content = [{ type: "text", text: compactText }, ...nonTextContent];
566
705
  }
567
706
  else if (qaPreset?.passed && prepared.compiledQaPreset && succeeded) {
568
707
  const compactText = buildQaCompactPassText({
@@ -616,9 +755,12 @@ export async function processBrowserOutput(input) {
616
755
  const resultHeadedManagedAutosaveInterval = resultRetainsPreparedManagedSession && !(commandClosesSession && succeeded)
617
756
  ? prepared.ownedManagedSessionContext?.headedManagedAutosaveInterval
618
757
  : undefined;
619
- 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 });
620
- const statePatch = { allowedDomainsBySession, artifactManifest, freshSessionOrdinal, managedSessionActive, managedSessionCompatibilityWorkaround, managedSessionHeadedAutosaveDisabled, managedSessionHeadedAutosaveInterval, managedSessionCwd, managedSessionName, managedSessionNamespace, networkRoutesBySession };
621
- return { result, statePatch };
758
+ 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 });
759
+ const resultWithCloseAll = closeAllApplied
760
+ ? { ...result, details: { ...(isRecord(result.details) ? result.details : {}), closeAllApplied: true } }
761
+ : result;
762
+ const statePatch = { artifactManifest, freshSessionOrdinal, managedSessionActive, managedSessionCompatibilityWorkaround, managedSessionHeadedAutosaveDisabled, managedSessionHeadedAutosaveInterval, managedSessionCwd, managedSessionName, managedSessionNamespace, networkRoutesBySession };
763
+ return { result: resultWithCloseAll, statePatch };
622
764
  }
623
765
  finally {
624
766
  if (processResult.stdoutSpillPath)