pi-agent-browser-native 0.2.77 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/CHANGELOG.md +154 -0
  2. package/README.md +69 -35
  3. package/dist/extensions/agent-browser/index.js +911 -121
  4. package/dist/extensions/agent-browser/lib/argv-descriptor.js +35 -8
  5. package/dist/extensions/agent-browser/lib/argv-grammar.js +48 -7
  6. package/dist/extensions/agent-browser/lib/batch-lifecycle.js +71 -0
  7. package/dist/extensions/agent-browser/lib/command-policy.js +1 -6
  8. package/dist/extensions/agent-browser/lib/command-taxonomy.js +35 -8
  9. package/dist/extensions/agent-browser/lib/config-policy.js +0 -5
  10. package/dist/extensions/agent-browser/lib/config.js +0 -6
  11. package/dist/extensions/agent-browser/lib/electron/cdp.js +6 -7
  12. package/dist/extensions/agent-browser/lib/electron/cleanup.js +0 -7
  13. package/dist/extensions/agent-browser/lib/electron/discovery.js +0 -7
  14. package/dist/extensions/agent-browser/lib/electron/launch.js +0 -7
  15. package/dist/extensions/agent-browser/lib/input-modes/electron.js +0 -5
  16. package/dist/extensions/agent-browser/lib/input-modes/job.js +61 -9
  17. package/dist/extensions/agent-browser/lib/input-modes/lookups.js +2 -7
  18. package/dist/extensions/agent-browser/lib/input-modes/params.js +23 -29
  19. package/dist/extensions/agent-browser/lib/input-modes/script.js +462 -0
  20. package/dist/extensions/agent-browser/lib/input-modes/semantic-action.js +51 -17
  21. package/dist/extensions/agent-browser/lib/input-modes/shared.js +0 -5
  22. package/dist/extensions/agent-browser/lib/input-modes/types.js +0 -5
  23. package/dist/extensions/agent-browser/lib/json-schema.js +0 -5
  24. package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +12 -5
  25. package/dist/extensions/agent-browser/lib/managed-session-capabilities.js +0 -2
  26. package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +3 -4
  27. package/dist/extensions/agent-browser/lib/managed-session-restore.js +55 -48
  28. package/dist/extensions/agent-browser/lib/managed-session-snapshots.js +2 -9
  29. package/dist/extensions/agent-browser/lib/managed-session-state-policy.js +48 -36
  30. package/dist/extensions/agent-browser/lib/managed-session-storage.js +50 -29
  31. package/dist/extensions/agent-browser/lib/navigation-policy.js +0 -5
  32. package/dist/extensions/agent-browser/lib/orchestration/batch-stdin.js +26 -5
  33. package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.js +110 -30
  34. package/dist/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.js +2 -1
  35. package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +27 -26
  36. package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +58 -33
  37. package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +2 -1
  38. package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +22 -14
  39. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/direct-anchor-download.js +1 -1
  40. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/network-page-filter.js +1 -1
  41. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/scroll-shims.js +1 -1
  42. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/snapshot-filter.js +1 -1
  43. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +148 -34
  44. package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +225 -61
  45. package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +74 -30
  46. package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +47 -26
  47. package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +40 -19
  48. package/dist/extensions/agent-browser/lib/orchestration/output-file.js +42 -1
  49. package/dist/extensions/agent-browser/lib/orchestration/script-mode.js +299 -0
  50. package/dist/extensions/agent-browser/lib/parsing.js +0 -7
  51. package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +36 -11
  52. package/dist/extensions/agent-browser/lib/playbook.js +20 -24
  53. package/dist/extensions/agent-browser/lib/process-environment.js +14 -0
  54. package/dist/extensions/agent-browser/lib/process-identity.js +4 -9
  55. package/dist/extensions/agent-browser/lib/process.js +131 -48
  56. package/dist/extensions/agent-browser/lib/prompt-policy.js +171 -27
  57. package/dist/extensions/agent-browser/lib/recording-reservations.js +183 -0
  58. package/dist/extensions/agent-browser/lib/results/action-recommendations.js +63 -13
  59. package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +68 -11
  60. package/dist/extensions/agent-browser/lib/results/categories.js +7 -9
  61. package/dist/extensions/agent-browser/lib/results/confirmation.js +0 -7
  62. package/dist/extensions/agent-browser/lib/results/contracts.js +0 -7
  63. package/dist/extensions/agent-browser/lib/results/editable-ref-evidence.js +0 -7
  64. package/dist/extensions/agent-browser/lib/results/envelope.js +0 -7
  65. package/dist/extensions/agent-browser/lib/results/network.js +0 -7
  66. package/dist/extensions/agent-browser/lib/results/next-actions.js +19 -35
  67. package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +85 -44
  68. package/dist/extensions/agent-browser/lib/results/presentation/batch.js +66 -11
  69. package/dist/extensions/agent-browser/lib/results/presentation/browser-profile-recovery.js +0 -5
  70. package/dist/extensions/agent-browser/lib/results/presentation/common.js +18 -5
  71. package/dist/extensions/agent-browser/lib/results/presentation/content.js +0 -5
  72. package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +7 -6
  73. package/dist/extensions/agent-browser/lib/results/presentation/errors.js +2 -1
  74. package/dist/extensions/agent-browser/lib/results/presentation/large-output.js +0 -5
  75. package/dist/extensions/agent-browser/lib/results/presentation/managed-list-filter.js +0 -5
  76. package/dist/extensions/agent-browser/lib/results/presentation/navigation.js +26 -16
  77. package/dist/extensions/agent-browser/lib/results/presentation/registry.js +58 -13
  78. package/dist/extensions/agent-browser/lib/results/presentation/semantic-action.js +1 -15
  79. package/dist/extensions/agent-browser/lib/results/presentation/skills.js +0 -5
  80. package/dist/extensions/agent-browser/lib/results/presentation.js +6 -8
  81. package/dist/extensions/agent-browser/lib/results/recovery-actions.js +2 -7
  82. package/dist/extensions/agent-browser/lib/results/recovery-next-actions.js +0 -7
  83. package/dist/extensions/agent-browser/lib/results/selector-recovery.js +51 -15
  84. package/dist/extensions/agent-browser/lib/results/snapshot-high-value-controls.js +13 -14
  85. package/dist/extensions/agent-browser/lib/results/snapshot-refs.js +0 -7
  86. package/dist/extensions/agent-browser/lib/results/snapshot-segments.js +0 -7
  87. package/dist/extensions/agent-browser/lib/results/snapshot-spill.js +0 -7
  88. package/dist/extensions/agent-browser/lib/results/snapshot.js +0 -7
  89. package/dist/extensions/agent-browser/lib/results/text.js +0 -7
  90. package/dist/extensions/agent-browser/lib/runtime.js +119 -52
  91. package/dist/extensions/agent-browser/lib/session-page-state.js +62 -17
  92. package/dist/extensions/agent-browser/lib/string-enum-schema.js +0 -5
  93. package/dist/extensions/agent-browser/lib/temp.js +0 -7
  94. package/dist/extensions/agent-browser/lib/upstream-version.js +14 -0
  95. package/dist/extensions/agent-browser/lib/web-search.js +0 -5
  96. package/dist/extensions/agent-browser/script-worker.js +169 -0
  97. package/dist/scripts/agent-browser-target.mjs +3 -0
  98. package/docs/ARCHITECTURE.md +50 -31
  99. package/docs/COMMAND_REFERENCE.md +97 -41
  100. package/docs/ELECTRON.md +3 -19
  101. package/docs/RELEASE.md +8 -20
  102. package/docs/REQUIREMENTS.md +8 -6
  103. package/docs/SUPPORT_MATRIX.md +32 -25
  104. package/docs/TOOL_CONTRACT.md +107 -64
  105. package/docs/platform-smoke.md +1 -1
  106. package/package.json +7 -6
  107. package/platform-smoke.config.mjs +2 -2
  108. package/scripts/agent-browser-capability-baseline.mjs +24 -6
  109. package/scripts/agent-browser-target.mjs +3 -0
  110. package/scripts/build.mjs +41 -0
  111. package/scripts/doctor.mjs +8 -7
  112. package/scripts/platform-smoke/browser-dogfood-windows.ps1 +9 -3
  113. package/scripts/platform-smoke/targets.mjs +12 -6
  114. package/dist/extensions/agent-browser/lib/electron/text.js +0 -13
  115. package/dist/extensions/agent-browser/lib/input-modes.js +0 -10
  116. package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-artifacts.js +0 -5
  117. package/dist/extensions/agent-browser/lib/results/artifact-state.js +0 -13
  118. package/dist/extensions/agent-browser/lib/results.js +0 -14
@@ -1,25 +1,31 @@
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
- import { analyzeNetworkSourceLookupResults, analyzeQaPresetResults, analyzeQaPresetTimeout, analyzeSourceLookupResults, buildQaCompactPassText, extractQaPageContext, redactNetworkSourceLookupAnalysis, } from "../../input-modes.js";
9
- import { applyNetworkRouteRecords, buildNetworkRouteDiagnostics, buildToolPresentation, getAgentBrowserErrorText, parseAgentBrowserEnvelope, } from "../../results.js";
9
+ import { analyzeNetworkSourceLookupResults, analyzeSourceLookupResults, redactNetworkSourceLookupAnalysis } from "../../input-modes/lookups.js";
10
+ import { analyzeQaPresetResults, analyzeQaPresetTimeout, buildQaCompactFailureText, buildQaCompactPassText, extractQaPageContext } from "../../input-modes/job.js";
11
+ import { applyNetworkRouteRecords, buildNetworkRouteDiagnostics } from "../../results/network-routes.js";
12
+ import { buildToolPresentation } from "../../results/presentation.js";
13
+ import { getAgentBrowserErrorText, parseAgentBrowserEnvelope } from "../../results/envelope.js";
14
+ import { omitUpstreamLifecycle } from "../../results/presentation/common.js";
10
15
  import { getClipboardWritePayloadCandidates, redactClipboardPermissionEcho, redactClipboardPermissionErrorValue } from "../../results/presentation/errors.js";
11
16
  import { shouldCaptureSemanticActionNavigationSummary } from "../../results/presentation/semantic-action.js";
12
- 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";
13
18
  import { isRecord } from "../../parsing.js";
14
19
  import { pruneOwnedManagedSessionRestoreSnapshots } from "../../managed-session-restore.js";
15
20
  import { isManagedSessionRestoreKey } from "../../managed-session-storage.js";
16
- import { createFreshSessionName, extractCommandTokens, resolveManagedSessionState } from "../../runtime.js";
21
+ import { createFreshSessionName, extractUpstreamCommandTokens, resolveManagedSessionState } from "../../runtime.js";
22
+ import { getUpstreamEffectiveBatchSteps } from "../batch-stdin.js";
17
23
  import { closeManagedSession, inspectManagedSessionDaemon } from "./managed-session-daemon-policy.js";
18
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";
19
25
  import { collectClickDispatchDiagnostic } from "./click-dispatch.js";
20
26
  import { buildScrollNoopDiagnostic, collectComboboxFocusDiagnostic, collectElectronBroadGetTextScopeDiagnostics, collectElectronHandoff, collectFillVerificationDiagnostic, collectNavigationSummary, collectOverlayBlockerDiagnostic, collectQaAttachedTarget, collectSnapshotOverlayBlockerDiagnostic, collectRecordingDependencyWarning, collectScrollPositionSnapshot, collectSelectorTextVisibilityDiagnostics, collectTimeoutPartialProgress, sleepMs, formatQaAttachedTargetText, getArtifactCleanupGuidance, getEvalResultWarning, getEvalStdinHint, getSourceLookupElectronContext, } from "./diagnostics.js";
21
27
  import { repairScreenshotData } from "./prepare.js";
22
- import { getPersistentSessionArtifactStore } from "./session-artifacts.js";
28
+ import { getPersistentSessionArtifactStore } from "./session-state.js";
23
29
  import { buildFinalAgentBrowserToolResult, buildRedactedPresentationContent, buildWrapperRecoveryHint, prepareFinalResultRecoveryState, redactExactSensitiveValue, } from "./final-result.js";
24
30
  async function repairScreenshotArtifact(options) {
25
31
  const { cwd, envelope, request } = options;
@@ -68,6 +74,15 @@ function batchStartedManagedBrowser(data) {
68
74
  return command === "connect" || command === "goto" || command === "navigate" || isOpenNavigationCommand(command);
69
75
  });
70
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
+ }
71
86
  function setNetworkRouteState(options) {
72
87
  if (!options.sessionName)
73
88
  return options.routesBySession;
@@ -92,7 +107,12 @@ function applyBatchNetworkRouteState(options) {
92
107
  for (const item of options.data) {
93
108
  if (!isRecord(item) || !Array.isArray(item.command) || !item.command.every((token) => typeof token === "string"))
94
109
  continue;
95
- 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);
96
116
  }
97
117
  return setNetworkRouteState({ routes, routesBySession: options.routesBySession, sessionName: options.sessionName });
98
118
  }
@@ -105,6 +125,8 @@ export async function processBrowserOutput(input) {
105
125
  let freshSessionOrdinal = state.freshSessionOrdinal;
106
126
  let managedSessionActive = state.managedSessionActive;
107
127
  let managedSessionCompatibilityWorkaround = state.managedSessionCompatibilityWorkaround;
128
+ let managedSessionHeadedAutosaveDisabled = state.managedSessionHeadedAutosaveDisabled === true;
129
+ let managedSessionHeadedAutosaveInterval = state.managedSessionHeadedAutosaveInterval;
108
130
  let managedSessionCwd = state.managedSessionCwd;
109
131
  let managedSessionName = state.managedSessionName;
110
132
  let managedSessionNamespace = state.managedSessionNamespace;
@@ -115,6 +137,7 @@ export async function processBrowserOutput(input) {
115
137
  let parseError = parsed.parseError;
116
138
  let presentationEnvelope = parsed.envelope;
117
139
  let navigationSummary = undefined;
140
+ let failedTransitionReverification = false;
118
141
  if (prepared.pinnedBatchUnwrapMode) {
119
142
  const pinnedBatchResult = unwrapPinnedSessionBatchEnvelope({ envelope: parsed.envelope, includeNavigationSummary: prepared.includePinnedNavigationSummary, mode: prepared.pinnedBatchUnwrapMode });
120
143
  parseError = pinnedBatchResult.parseError ?? parseError;
@@ -127,11 +150,22 @@ export async function processBrowserOutput(input) {
127
150
  presentationEnvelope = repairedBatchScreenshots.envelope;
128
151
  const screenshotArtifactRequest = repairedScreenshot.request;
129
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));
130
164
  const rawCloseStatePath = isCloseCommand(prepared.executionPlan.commandInfo.command)
131
165
  && isRecord(presentationEnvelope?.data)
132
166
  && typeof presentationEnvelope.data.statePath === "string"
133
167
  ? presentationEnvelope.data.statePath
134
- : undefined;
168
+ : nestedBatchClose?.statePath;
135
169
  if (presentationEnvelope && prepared.exactSensitiveValues.length > 0)
136
170
  presentationEnvelope = redactExactSensitiveValue(presentationEnvelope, prepared.exactSensitiveValues);
137
171
  const parseFailureOutput = parseError && processResult.stdoutSpillPath
@@ -147,7 +181,44 @@ export async function processBrowserOutput(input) {
147
181
  let succeeded = processSucceeded && parseSucceeded && envelopeSuccess;
148
182
  const inspectionText = plainTextInspection ? processResult.stdout.trim() : undefined;
149
183
  const sessionStateKey = getSessionContextKey(prepared.executionPlan.sessionName, prepared.executionPlan.namespace);
150
- 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
+ }
151
222
  let clickDispatchDiagnostic;
152
223
  if (succeeded && prepared.clickDispatchProbe) {
153
224
  clickDispatchDiagnostic = await collectClickDispatchDiagnostic({ cwd, namespace: prepared.executionPlan.namespace, probe: prepared.clickDispatchProbe, sessionName: prepared.executionPlan.sessionName, signal });
@@ -178,7 +249,24 @@ export async function processBrowserOutput(input) {
178
249
  shouldCaptureAllowedDomainNavigationSummary ||
179
250
  managedSessionCommandRequiresLivePageVerification(prepared.executionPlan.effectiveArgs, prepared.runtimeToolStdin) ||
180
251
  (prepared.executionPlan.commandInfo.command === "tab" && prepared.executionPlan.commandInfo.subcommand === "close"))) {
181
- 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;
182
270
  }
183
271
  if (navigationSummary && presentationEnvelope && prepared.executionPlan.commandInfo.command !== "eval" && !Array.isArray(presentationEnvelope.data))
184
272
  presentationEnvelope = { ...presentationEnvelope, data: mergeNavigationSummaryIntoData(presentationEnvelope.data, navigationSummary) };
@@ -202,15 +290,18 @@ export async function processBrowserOutput(input) {
202
290
  presentationEnvelope = { error: observedPageValidationError, success: false };
203
291
  }
204
292
  const safeObservedSessionTabTarget = observedPageValidationError ? normalizeSessionTabTarget({ url: observedSessionTabTarget?.url }) : observedSessionTabTarget;
205
- let currentSessionTabTarget = safeObservedSessionTabTarget ?? (resultingPageState.pageTargetMayHaveChanged
206
- ? succeeded ? normalizeSessionTabTarget({ url: resultingPageState.currentPageUrl }) : undefined
207
- : 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
+ }
208
299
  let aboutBlankSessionMismatch;
209
300
  let electronPostCommandHealth;
210
301
  let electronRefFreshnessDiagnostic;
211
302
  let electronSessionMismatch;
212
303
  let electronStatusAfterCommand;
213
- 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);
214
305
  let sessionTabCorrection = prepared.sessionTabCorrection;
215
306
  if (shouldTreatAboutBlankAsMismatch && prepared.priorSessionTabTarget) {
216
307
  const aboutBlankObservedTarget = observedSessionTabTarget ?? currentSessionTabTarget;
@@ -223,7 +314,7 @@ export async function processBrowserOutput(input) {
223
314
  else
224
315
  currentSessionTabTarget = aboutBlankObservedTarget ?? normalizeSessionTabTarget({ url: "about:blank" });
225
316
  aboutBlankSessionMismatch = { activeUrl: "about:blank", recoveryApplied: appliedAboutBlankRecovery !== undefined, recoveryHint: buildAboutBlankRecoveryHint(), targetTitle: prepared.priorSessionTabTarget.title, targetUrl: prepared.priorSessionTabTarget.url };
226
- const electronRecord = findElectronLaunchRecordForSession(prepared.executionPlan.sessionName, electronLaunchRecords);
317
+ const electronRecord = findElectronLaunchRecordForSession(prepared.executionPlan.sessionName, electronLaunchRecords, prepared.executionPlan.namespace);
227
318
  if (electronRecord && prepared.executionPlan.sessionName) {
228
319
  electronStatusAfterCommand = await inspectElectronLaunchStatus(electronRecord);
229
320
  electronSessionMismatch = buildElectronSessionMismatch({ managedSession: { sessionName: prepared.executionPlan.sessionName, title: aboutBlankObservedTarget?.title, url: aboutBlankObservedTarget?.url ?? "about:blank" }, record: electronRecord, statusTargets: electronStatusAfterCommand.targets });
@@ -249,7 +340,7 @@ export async function processBrowserOutput(input) {
249
340
  succeeded = false;
250
341
  presentationEnvelope = { ...(presentationEnvelope ?? {}), error: allowedDomainsViolation.summary, success: false };
251
342
  }
252
- const electronRecordForCommand = findElectronLaunchRecordForSession(prepared.executionPlan.sessionName, electronLaunchRecords);
343
+ const electronRecordForCommand = findElectronLaunchRecordForSession(prepared.executionPlan.sessionName, electronLaunchRecords, prepared.executionPlan.namespace);
253
344
  if (succeeded && electronRecordForCommand && shouldInspectElectronPostCommandHealth(prepared.executionPlan.commandInfo.command)) {
254
345
  electronStatusAfterCommand ??= await inspectElectronLaunchStatus(electronRecordForCommand);
255
346
  electronPostCommandHealth = buildElectronPostCommandHealthDiagnostic({ command: prepared.executionPlan.commandInfo.command, record: electronRecordForCommand, status: electronStatusAfterCommand, target: observedSessionTabTarget ?? currentSessionTabTarget });
@@ -283,7 +374,7 @@ export async function processBrowserOutput(input) {
283
374
  if (succeeded) {
284
375
  selectorTextVisibilityDiagnostics = await collectSelectorTextVisibilityDiagnostics({ commandInfo: prepared.executionPlan.commandInfo, commandTokens: prepared.commandTokens, cwd, data: presentationEnvelope?.data, namespace: prepared.executionPlan.namespace, sessionName: prepared.executionPlan.sessionName, signal });
285
376
  if (electronRecordForCommand)
286
- 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 });
287
378
  }
288
379
  const activeNetworkRoutes = sessionStateKey ? networkRoutesBySession.get(sessionStateKey) : undefined;
289
380
  const networkRouteDiagnostics = succeeded && prepared.executionPlan.commandInfo.command === "network" && prepared.executionPlan.commandInfo.subcommand === "requests" && prepared.executionPlan.sessionName
@@ -293,11 +384,13 @@ export async function processBrowserOutput(input) {
293
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;
294
385
  const recordingDependencyWarning = await collectRecordingDependencyWarning({ command: prepared.executionPlan.commandInfo.command, commandTokens: prepared.commandTokens, succeeded });
295
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;
296
388
  let currentRefSnapshot;
297
389
  let currentRefSnapshotInvalidation;
298
- const batchRefSnapshotState = prepared.executionPlan.commandInfo.command === "batch" ? extractLatestRefSnapshotStateFromBatchResults(presentationEnvelope?.data) : undefined;
299
390
  if (sessionStateKey) {
300
- 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);
301
394
  allowedDomainsBySession = new Map(allowedDomainsBySession);
302
395
  allowedDomainsBySession.delete(sessionStateKey);
303
396
  networkRoutesBySession = new Map(networkRoutesBySession);
@@ -305,35 +398,66 @@ export async function processBrowserOutput(input) {
305
398
  sessionPageState.clearSession(sessionStateKey);
306
399
  state.closedManagedSessionNames.add(sessionStateKey);
307
400
  }
308
- else if (currentSessionTabTarget) {
309
- const tabUpdate = sessionPageState.applyTabTarget({ sessionName: sessionStateKey, target: currentSessionTabTarget, update: sessionPageStateUpdate });
310
- if (!tabUpdate.applied && succeeded)
311
- sessionPageState.markPinning(sessionStateKey, "drift");
312
- }
313
- else if (processResult.agentBrowserStarted && (resultingPageState.pageUrlUnknown || resultingPageState.pageTargetMayHaveChanged)) {
314
- sessionPageState.markTabTargetUnknown({ sessionName: sessionStateKey, update: sessionPageStateUpdate });
315
- }
316
- const refSnapshot = prepared.executionPlan.commandInfo.command === "batch" ? batchRefSnapshotState?.snapshot : succeeded ? prepared.executionPlan.commandInfo.command === "snapshot" ? extractRefSnapshotFromData(presentationEnvelope?.data) : prepared.resolvedSemanticActionRefSnapshot ?? overlayBlockerDiagnostic?.snapshot : undefined;
317
- if (refSnapshot) {
318
- const refUpdate = sessionPageState.applyRefSnapshot({ fallbackTarget: currentSessionTabTarget, sessionName: sessionStateKey, snapshot: refSnapshot, update: sessionPageStateUpdate });
319
- currentRefSnapshot = refUpdate.refSnapshot;
320
- currentRefSnapshotInvalidation = refUpdate.refSnapshotInvalidation;
321
- }
322
401
  else {
323
- const stateView = sessionPageState.get(sessionStateKey);
324
- currentRefSnapshot = stateView.refSnapshot;
325
- 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
+ }
326
438
  }
327
439
  }
328
440
  const priorManagedSessionActive = managedSessionActive;
329
441
  const priorManagedSessionCwd = managedSessionCwd;
442
+ const priorManagedSessionHeadedAutosaveInterval = managedSessionHeadedAutosaveInterval;
330
443
  const priorManagedSessionName = managedSessionName;
331
444
  const priorManagedSessionNamespace = managedSessionNamespace;
332
- 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;
333
455
  const closeTargetsPriorManagedNamespace = prepared.executionPlan.namespace === priorManagedSessionNamespace;
334
- const managedCloseSessionName = commandClosesSession && succeeded && prepared.executionPlan.sessionName === priorManagedSessionName && closeTargetsPriorManagedNamespace
335
- ? prepared.executionPlan.sessionName
336
- : prepared.executionPlan.managedSessionName;
456
+ const managedCloseSessionName = closeAllClosesPriorManagedSession
457
+ ? priorManagedSessionName
458
+ : closeCommandSucceeded && prepared.executionPlan.sessionName === priorManagedSessionName && closeTargetsPriorManagedNamespace
459
+ ? prepared.executionPlan.sessionName
460
+ : prepared.executionPlan.managedSessionName;
337
461
  const policyBlockedFreshManagedSession = allowedDomainsViolation !== undefined && prepared.sessionMode === "fresh" && prepared.executionPlan.managedSessionName === prepared.executionPlan.sessionName;
338
462
  const postLaunchBatchFailure = !succeeded && processSucceeded && parseSucceeded && prepared.sessionMode === "fresh" && prepared.executionPlan.commandInfo.command === "batch" && batchStartedManagedBrowser(presentationEnvelope?.data);
339
463
  const postLaunchTimeoutWithPage = !succeeded && processResult.timedOut && prepared.sessionMode === "fresh" && prepared.executionPlan.commandInfo.command === "batch" && timeoutPartialProgress?.liveUrlRecovered === true;
@@ -344,6 +468,7 @@ export async function processBrowserOutput(input) {
344
468
  const failedFreshDaemon = failedFreshSessionMayHaveStarted && prepared.executionPlan.sessionName
345
469
  ? await inspectManagedSessionDaemon({
346
470
  cwd,
471
+ headedManagedAutosaveInterval: prepared.ownedManagedSessionContext?.headedManagedAutosaveInterval,
347
472
  namespace: prepared.executionPlan.namespace,
348
473
  sessionName: prepared.executionPlan.sessionName,
349
474
  timeoutMs: Math.min(implicitSessionCloseTimeoutMs, 2_000),
@@ -354,8 +479,8 @@ export async function processBrowserOutput(input) {
354
479
  }
355
480
  // Only a confirmed inactive daemon proves that a started fresh command did not establish browser ownership.
356
481
  const postLaunchFreshFailure = failedFreshDaemon !== undefined && failedFreshDaemon.status !== "inactive";
357
- const managedTransitionSucceeded = succeeded || policyBlockedFreshManagedSession || postLaunchBatchFailure || postLaunchTimeoutWithPage || postLaunchFreshFailure;
358
- 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 });
359
484
  if (!managedTransitionSucceeded && prepared.sessionMode === "fresh" && prepared.executionPlan.managedSessionName) {
360
485
  state.managedSessionRestoreState.clear(prepared.executionPlan.managedSessionName, prepared.executionPlan.namespace);
361
486
  }
@@ -368,11 +493,15 @@ export async function processBrowserOutput(input) {
368
493
  === getAgentBrowserSessionIdentityKey(managedSessionName, managedSessionNamespace);
369
494
  if (!managedSessionActive) {
370
495
  managedSessionCompatibilityWorkaround = undefined;
496
+ managedSessionHeadedAutosaveDisabled = false;
497
+ managedSessionHeadedAutosaveInterval = undefined;
371
498
  }
372
499
  else if (managedTransitionSucceeded && executionTargetsManagedSession) {
373
500
  managedSessionCompatibilityWorkaround = prepared.compatibilityWorkaround;
501
+ managedSessionHeadedAutosaveDisabled = prepared.ownedManagedSessionContext?.headedManagedAutosaveDisabled === true;
502
+ managedSessionHeadedAutosaveInterval = prepared.ownedManagedSessionContext?.headedManagedAutosaveInterval;
374
503
  }
375
- if (commandClosesSession && succeeded && managedCloseSessionName === priorManagedSessionName && !managedSessionActive) {
504
+ if (closeCommandSucceeded && managedCloseSessionName === priorManagedSessionName && !managedSessionActive) {
376
505
  const daemonRestoreKey = state.managedSessionRestoreState.getDaemonRestoreKey(managedCloseSessionName, priorManagedSessionNamespace);
377
506
  const ownedRestoreKey = !state.managedSessionRestoreState.isDisabled(managedCloseSessionName, priorManagedSessionNamespace)
378
507
  && isManagedSessionRestoreKey(daemonRestoreKey) ? daemonRestoreKey : null;
@@ -387,7 +516,7 @@ export async function processBrowserOutput(input) {
387
516
  managedSessionName = createFreshSessionName(state.managedSessionBaseName, state.ephemeralSessionSeed, freshSessionOrdinal);
388
517
  managedSessionNamespace = undefined;
389
518
  }
390
- 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 });
391
520
  if (prepared.executionPlan.managedSessionName && managedTransitionSucceeded && managedSessionActive) {
392
521
  managedSessionCwd = cwd;
393
522
  managedSessionNamespace = prepared.executionPlan.namespace;
@@ -406,7 +535,16 @@ export async function processBrowserOutput(input) {
406
535
  networkRoutesBySession.delete(replacedSessionStateKey ?? replacedManagedSessionName);
407
536
  sessionPageState.clearSession(replacedSessionStateKey ?? replacedManagedSessionName);
408
537
  const replacedSessionKey = replacedSessionStateKey ?? replacedManagedSessionName;
409
- const replacedCloseError = await closeManagedSession({ cwd: priorManagedSessionCwd, namespace: priorManagedSessionNamespace, preserveAttachedBrowserSession: state.attachedSessionKeys.has(replacedSessionKey), restoreState: state.managedSessionRestoreState, sessionName: replacedManagedSessionName, timeoutMs: implicitSessionCloseTimeoutMs });
538
+ const replacedCloseError = await closeManagedSession({ cwd: priorManagedSessionCwd, headedManagedAutosaveInterval: priorManagedSessionHeadedAutosaveInterval, namespace: priorManagedSessionNamespace, preserveAttachedBrowserSession: state.attachedSessionKeys.has(replacedSessionKey), restoreState: state.managedSessionRestoreState, sessionName: replacedManagedSessionName, timeoutMs: implicitSessionCloseTimeoutMs });
539
+ if (managedSessionOutcome) {
540
+ managedSessionOutcome = {
541
+ ...managedSessionOutcome,
542
+ replacedSessionClosed: !replacedCloseError,
543
+ summary: replacedCloseError
544
+ ? `${managedSessionOutcome.summary} Previous session ${replacedManagedSessionName} remains wrapper-owned because automatic close failed; retry an explicit close.`
545
+ : managedSessionOutcome.summary,
546
+ };
547
+ }
410
548
  if (!replacedCloseError) {
411
549
  state.attachedSessionKeys.delete(replacedSessionKey);
412
550
  state.closedManagedSessionNames.add(replacedSessionKey);
@@ -419,7 +557,7 @@ export async function processBrowserOutput(input) {
419
557
  if (succeeded && prepared.executionPlan.sessionName) {
420
558
  const electronSessionName = prepared.executionPlan.sessionName;
421
559
  const electronSessionStateKey = sessionStateKey ?? electronSessionName;
422
- electronLaunchRecord = { ...prepared.electronLaunch.record, sessionName: electronSessionName };
560
+ electronLaunchRecord = { ...prepared.electronLaunch.record, namespace: prepared.executionPlan.namespace, sessionName: electronSessionName };
423
561
  const electronHandoffMode = prepared.compiledElectron?.action === "launch" ? prepared.compiledElectron.handoff : "connect";
424
562
  try {
425
563
  electronHandoff = await collectElectronHandoff({ cwd, handoff: electronHandoffMode, namespace: prepared.executionPlan.namespace, sessionName: electronSessionName, signal });
@@ -434,7 +572,7 @@ export async function processBrowserOutput(input) {
434
572
  if (electronHandoff.error) {
435
573
  succeeded = false;
436
574
  presentationEnvelope = { error: electronHandoff.error, success: false };
437
- const closeError = await closeManagedSession({ cwd, namespace: prepared.executionPlan.namespace, policyLock: prepared.managedSessionPolicyLock, preserveAttachedBrowserSession: input.preserveAttachedBrowserSession, restoreState: state.managedSessionRestoreState, sessionName: electronSessionName, timeoutMs: implicitSessionCloseTimeoutMs });
575
+ const closeError = await closeManagedSession({ cwd, headedManagedAutosaveInterval: prepared.ownedManagedSessionContext?.headedManagedAutosaveInterval, namespace: prepared.executionPlan.namespace, policyLock: prepared.managedSessionPolicyLock, preserveAttachedBrowserSession: input.preserveAttachedBrowserSession, restoreState: state.managedSessionRestoreState, sessionName: electronSessionName, timeoutMs: implicitSessionCloseTimeoutMs });
438
576
  electronFailedConnectCleanup = await cleanupElectronLaunchResources({ child: prepared.electronLaunch.child, record: electronLaunchRecord, timeoutMs: implicitSessionCloseTimeoutMs });
439
577
  electronLaunchRecord = electronFailedConnectCleanup.record;
440
578
  if (electronFailedConnectCleanup.partial) {
@@ -487,7 +625,7 @@ export async function processBrowserOutput(input) {
487
625
  if (presentationEnvelope?.error !== undefined)
488
626
  presentationEnvelope = { ...presentationEnvelope, error: redactClipboardPermissionErrorValue(prepared.executionPlan.commandInfo, presentationEnvelope.error, clipboardWritePayloadCandidates) };
489
627
  }
490
- 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 });
491
629
  if (observedPageValidationError)
492
630
  presentation.failureCategory = "validation-error";
493
631
  if (electronHandoff?.error && electronHandoff.failureCategory)
@@ -498,13 +636,20 @@ export async function processBrowserOutput(input) {
498
636
  presentationEnvelope = { ...(presentationEnvelope ?? {}), error: presentation.summary, success: false };
499
637
  }
500
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 };
501
643
  presentation.summary = "Scroll completed with no observed movement.";
502
644
  if (isRecord(presentation.data))
503
645
  presentation.data = { ...presentation.data, noMovement: true, scrolled: false };
504
- if (presentation.content[0]?.type === "text")
505
- presentation.content[0] = { ...presentation.content[0], text: `Scroll completed with no observed movement.\n\n${presentation.content[0].text}` };
506
- 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 {
507
651
  presentation.content.unshift({ type: "text", text: "Scroll completed with no observed movement." });
652
+ }
508
653
  }
509
654
  if (parseFailureOutput.artifactManifest) {
510
655
  presentation.artifactManifest = parseFailureOutput.artifactManifest;
@@ -524,7 +669,7 @@ export async function processBrowserOutput(input) {
524
669
  let qaAttachedTarget = prepared.compiledQaPreset?.checks.attached
525
670
  ? await collectQaAttachedTarget({ currentTarget: currentSessionTabTarget ?? prepared.priorSessionTabTarget, cwd, namespace: prepared.executionPlan.namespace, sessionName: prepared.executionPlan.sessionName, signal })
526
671
  : undefined;
527
- 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;
528
673
  const sourceLookup = prepared.compiledSourceLookup ? await analyzeSourceLookupResults(presentationEnvelope?.data, prepared.compiledSourceLookup, cwd, { electronContext: sourceLookupElectronContext, workspaceRoot: cwd }) : undefined;
529
674
  const networkSourceLookup = prepared.compiledNetworkSourceLookup ? redactNetworkSourceLookupAnalysis(await analyzeNetworkSourceLookupResults(presentationEnvelope?.data, prepared.compiledNetworkSourceLookup, cwd)) : undefined;
530
675
  if (networkSourceLookup && presentation.content[0]?.type === "text")
@@ -535,14 +680,22 @@ export async function processBrowserOutput(input) {
535
680
  presentation.content[0] = { ...presentation.content[0], text: `${sourceLookup.summary}\n\n${presentation.content[0].text}` };
536
681
  else if (sourceLookup)
537
682
  presentation.content.unshift({ type: "text", text: sourceLookup.summary });
538
- if (qaPreset && !qaPreset.passed && presentation.failureCategory !== "artifact-missing") {
683
+ if (qaPreset && !qaPreset.passed && prepared.compiledQaPreset && presentation.failureCategory !== "artifact-missing") {
539
684
  succeeded = false;
540
685
  presentation.failureCategory = "qa-failure";
541
686
  presentation.summary = qaPreset.summary;
542
- if (presentation.content[0]?.type === "text")
543
- presentation.content[0] = { ...presentation.content[0], text: `${qaPreset.summary}\n\n${presentation.content[0].text}` };
544
- else
545
- 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];
546
699
  }
547
700
  else if (qaPreset?.passed && prepared.compiledQaPreset && succeeded) {
548
701
  const compactText = buildQaCompactPassText({
@@ -588,9 +741,20 @@ export async function processBrowserOutput(input) {
588
741
  if (sessionStateKey)
589
742
  currentSessionTabTarget = authoritativePageState?.tabTarget;
590
743
  const currentSessionTabTargetUnknown = authoritativePageState?.tabTargetUnknown === true ? true : undefined;
591
- 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, 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 });
592
- const statePatch = { allowedDomainsBySession, artifactManifest, freshSessionOrdinal, managedSessionActive, managedSessionCompatibilityWorkaround, managedSessionCwd, managedSessionName, managedSessionNamespace, networkRoutesBySession };
593
- return { result, statePatch };
744
+ const resultRetainsPreparedManagedSession = !managedSessionOutcome || (managedSessionOutcome.activeAfter
745
+ && managedSessionOutcome.attemptedSessionName === managedSessionOutcome.currentSessionName);
746
+ const resultHeadedManagedAutosaveDisabled = prepared.ownedManagedSessionContext?.headedManagedAutosaveDisabled === true
747
+ && resultRetainsPreparedManagedSession
748
+ && !(commandClosesSession && succeeded);
749
+ const resultHeadedManagedAutosaveInterval = resultRetainsPreparedManagedSession && !(commandClosesSession && succeeded)
750
+ ? prepared.ownedManagedSessionContext?.headedManagedAutosaveInterval
751
+ : undefined;
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;
756
+ const statePatch = { allowedDomainsBySession, artifactManifest, freshSessionOrdinal, managedSessionActive, managedSessionCompatibilityWorkaround, managedSessionHeadedAutosaveDisabled, managedSessionHeadedAutosaveInterval, managedSessionCwd, managedSessionName, managedSessionNamespace, networkRoutesBySession };
757
+ return { result: resultWithCloseAll, statePatch };
594
758
  }
595
759
  finally {
596
760
  if (processResult.stdoutSpillPath)