pi-agent-browser-native 0.5.0 → 0.6.6

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 (64) hide show
  1. package/CHANGELOG.md +144 -0
  2. package/README.md +75 -42
  3. package/dist/extensions/agent-browser/index.js +13 -83
  4. package/dist/extensions/agent-browser/lib/argv-grammar.js +8 -2
  5. package/dist/extensions/agent-browser/lib/batch-lifecycle.js +1 -1
  6. package/dist/extensions/agent-browser/lib/command-policy.js +4 -7
  7. package/dist/extensions/agent-browser/lib/command-taxonomy.js +19 -11
  8. package/dist/extensions/agent-browser/lib/config-policy.js +25 -1
  9. package/dist/extensions/agent-browser/lib/config.js +1 -1
  10. package/dist/extensions/agent-browser/lib/input-modes/job.js +0 -9
  11. package/dist/extensions/agent-browser/lib/input-modes/params.js +1 -1
  12. package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +18 -4
  13. package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +3 -138
  14. package/dist/extensions/agent-browser/lib/managed-session-restore.js +1 -81
  15. package/dist/extensions/agent-browser/lib/managed-session-storage.js +4 -1
  16. package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +31 -26
  17. package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +60 -8
  18. package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +1 -4
  19. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/snapshot-filter.js +119 -2
  20. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/wait-timeouts.js +7 -6
  21. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +31 -40
  22. package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +76 -70
  23. package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +6 -8
  24. package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +5 -10
  25. package/dist/extensions/agent-browser/lib/orchestration/output-file.js +41 -21
  26. package/dist/extensions/agent-browser/lib/page-target-validation.js +270 -0
  27. package/dist/extensions/agent-browser/lib/playbook.js +13 -12
  28. package/dist/extensions/agent-browser/lib/process-identity.js +11 -10
  29. package/dist/extensions/agent-browser/lib/process.js +18 -82
  30. package/dist/extensions/agent-browser/lib/recording-reservations.js +11 -78
  31. package/dist/extensions/agent-browser/lib/results/action-recommendations.js +1 -1
  32. package/dist/extensions/agent-browser/lib/results/presentation/batch.js +27 -14
  33. package/dist/extensions/agent-browser/lib/results/presentation/common.js +20 -2
  34. package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +11 -16
  35. package/dist/extensions/agent-browser/lib/results/presentation/navigation.js +12 -9
  36. package/dist/extensions/agent-browser/lib/results/presentation/registry.js +2 -2
  37. package/dist/extensions/agent-browser/lib/results/presentation.js +31 -4
  38. package/dist/extensions/agent-browser/lib/results/recovery-actions.js +1 -1
  39. package/dist/extensions/agent-browser/lib/results/recovery-next-actions.js +9 -0
  40. package/dist/extensions/agent-browser/lib/results/selector-recovery.js +3 -3
  41. package/dist/extensions/agent-browser/lib/results/snapshot-spill.js +2 -1
  42. package/dist/extensions/agent-browser/lib/results/snapshot.js +4 -4
  43. package/dist/extensions/agent-browser/lib/runtime.js +73 -72
  44. package/dist/extensions/agent-browser/lib/session-page-state.js +12 -3
  45. package/dist/extensions/agent-browser/lib/temp.js +1 -2
  46. package/dist/extensions/agent-browser/lib/upstream-version.js +5 -5
  47. package/dist/extensions/agent-browser/lib/web-search.js +108 -24
  48. package/dist/scripts/agent-browser-target.mjs +19 -1
  49. package/docs/ARCHITECTURE.md +24 -20
  50. package/docs/COMMAND_REFERENCE.md +181 -49
  51. package/docs/ELECTRON.md +2 -2
  52. package/docs/RELEASE.md +10 -8
  53. package/docs/REQUIREMENTS.md +8 -7
  54. package/docs/SUPPORT_MATRIX.md +31 -26
  55. package/docs/TOOL_CONTRACT.md +89 -56
  56. package/package.json +1 -1
  57. package/scripts/agent-browser-capability-baseline.mjs +65 -5
  58. package/scripts/agent-browser-target.mjs +19 -1
  59. package/scripts/config.mjs +1 -0
  60. package/scripts/doctor.mjs +15 -9
  61. package/dist/extensions/agent-browser/lib/managed-session-capabilities.js +0 -20
  62. package/dist/extensions/agent-browser/lib/managed-session-state-policy.js +0 -601
  63. package/dist/extensions/agent-browser/lib/navigation-policy.js +0 -78
  64. package/dist/extensions/agent-browser/lib/results/presentation/managed-list-filter.js +0 -37
@@ -1,11 +1,10 @@
1
1
  import { rm } from "node:fs/promises";
2
2
  import { getAgentBrowserSessionIdentityKey, isAgentBrowserSessionIdentityKeyInNamespace } from "../../argv-grammar.js";
3
3
  import { batchHasSuccessfulCloseAll, getSuccessfulBatchCloseLifecycle } from "../../batch-lifecycle.js";
4
- import { isCloseAllCommand, isCloseCommand, isNavigationObservableCommandName, isOpenNavigationCommand, isRecordPageTransitionCommand, isUnverifiedPageTransitionCommand } from "../../command-taxonomy.js";
4
+ import { isCloseAllCommand, isCloseCommand, isOpenNavigationCommand, isUnverifiedPageTransitionCommand } from "../../command-taxonomy.js";
5
5
  import { OPEN_RESULT_TAB_CORRECTION_FLAGS } from "../../launch-scoped-flags.js";
6
6
  import { cleanupElectronLaunchResources, inspectElectronLaunchStatus } from "../../electron/cleanup.js";
7
- import { getAllowedDomainsViolation, parseAllowedDomainsPolicyFromArgs } from "../../navigation-policy.js";
8
- import { getManagedSessionResultingPageState, getObservedBrowserPageValidationError, managedSessionCommandRequiresLivePageVerification } from "../../managed-session-state-policy.js";
7
+ import { getResultingPageTargetState, commandRequiresLivePageVerification } from "../../page-target-validation.js";
9
8
  import { analyzeNetworkSourceLookupResults, analyzeSourceLookupResults, redactNetworkSourceLookupAnalysis } from "../../input-modes/lookups.js";
10
9
  import { analyzeQaPresetResults, analyzeQaPresetTimeout, buildQaCompactFailureText, buildQaCompactPassText, extractQaPageContext } from "../../input-modes/job.js";
11
10
  import { applyNetworkRouteRecords, buildNetworkRouteDiagnostics } from "../../results/network-routes.js";
@@ -14,7 +13,7 @@ import { getAgentBrowserErrorText, parseAgentBrowserEnvelope } from "../../resul
14
13
  import { omitUpstreamLifecycle } from "../../results/presentation/common.js";
15
14
  import { getClipboardWritePayloadCandidates, redactClipboardPermissionEcho, redactClipboardPermissionErrorValue } from "../../results/presentation/errors.js";
16
15
  import { shouldCaptureSemanticActionNavigationSummary } from "../../results/presentation/semantic-action.js";
17
- import { buildPageTransitionRefSnapshotInvalidation, 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";
18
17
  import { isRecord } from "../../parsing.js";
19
18
  import { pruneOwnedManagedSessionRestoreSnapshots } from "../../managed-session-restore.js";
20
19
  import { isManagedSessionRestoreKey } from "../../managed-session-storage.js";
@@ -64,6 +63,35 @@ function isStreamEnableAlreadyEnabledNoop(options) {
64
63
  const message = (getEnvelopeErrorString(options.envelope) ?? "").trim().replace(/[.!]+$/, "").toLowerCase();
65
64
  return message === "streaming is already enabled for this session" || message === "streaming is already enabled" || message === "stream already enabled";
66
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
+ }
67
95
  function batchStartedManagedBrowser(data) {
68
96
  if (!Array.isArray(data))
69
97
  return false;
@@ -120,7 +148,6 @@ export async function processBrowserOutput(input) {
120
148
  const { ctx, cwd, electronPostCommandStatusSettleMs, implicitSessionCloseTimeoutMs, sessionPageStateUpdate, signal, state } = input;
121
149
  const { prepared, processResult } = input;
122
150
  const { electronChildProcesses, electronLaunchRecords, sessionPageState, traceOwners } = state;
123
- let allowedDomainsBySession = state.allowedDomainsBySession;
124
151
  let artifactManifest = state.artifactManifest;
125
152
  let freshSessionOrdinal = state.freshSessionOrdinal;
126
153
  let managedSessionActive = state.managedSessionActive;
@@ -183,7 +210,6 @@ export async function processBrowserOutput(input) {
183
210
  const sessionStateKey = getSessionContextKey(prepared.executionPlan.sessionName, prepared.executionPlan.namespace);
184
211
  const closeAllApplied = nestedBatchClosesAll || (directCloseAllRequested && succeeded);
185
212
  if (closeAllApplied) {
186
- allowedDomainsBySession = withoutNamespaceEntries(allowedDomainsBySession, prepared.executionPlan.namespace);
187
213
  networkRoutesBySession = withoutNamespaceEntries(networkRoutesBySession, prepared.executionPlan.namespace);
188
214
  deleteNamespaceEntries(state.attachedSessionKeys, prepared.executionPlan.namespace);
189
215
  deleteNamespaceEntries(traceOwners, prepared.executionPlan.namespace);
@@ -197,8 +223,6 @@ export async function processBrowserOutput(input) {
197
223
  }
198
224
  }
199
225
  else if (nestedBatchClose && sessionStateKey) {
200
- allowedDomainsBySession = new Map(allowedDomainsBySession);
201
- allowedDomainsBySession.delete(sessionStateKey);
202
226
  networkRoutesBySession = new Map(networkRoutesBySession);
203
227
  networkRoutesBySession.delete(sessionStateKey);
204
228
  sessionPageState.clearSession(sessionStateKey);
@@ -227,33 +251,25 @@ export async function processBrowserOutput(input) {
227
251
  presentationEnvelope = { ...(presentationEnvelope ?? {}), error: clickDispatchDiagnostic.summary, success: false };
228
252
  }
229
253
  }
230
- 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({
231
256
  args: prepared.executionPlan.effectiveArgs,
232
257
  currentPageUrl: prepared.priorSessionTabTarget?.url,
233
258
  pageUrlUnknown: prepared.priorSessionTabTargetUnknown === true,
234
259
  stdin: prepared.runtimeToolStdin,
235
260
  trustedFirstBatchTabSelection: prepared.pinnedBatchUnwrapMode !== undefined,
236
261
  });
237
- const presentationDataRecord = isRecord(presentationEnvelope?.data) ? presentationEnvelope.data : undefined;
238
- const dataClicked = typeof presentationDataRecord?.clicked === "string" ? presentationDataRecord.clicked : undefined;
239
- const cssClickWithoutHref = prepared.executionPlan.commandInfo.command === "click" && dataClicked !== undefined && !dataClicked.startsWith("@") && !dataClicked.startsWith("ref=") && typeof presentationDataRecord?.href !== "string";
240
- const parsedAllowedDomainsPolicy = parseAllowedDomainsPolicyFromArgs(prepared.runtimeToolArgs);
241
- const sessionAllowedDomainsPolicy = sessionStateKey
242
- ? parsedAllowedDomainsPolicy ?? allowedDomainsBySession.get(sessionStateKey)
243
- : parsedAllowedDomainsPolicy;
244
- const shouldCaptureAllowedDomainNavigationSummary = sessionAllowedDomainsPolicy !== undefined && !cssClickWithoutHref && (prepared.executionPlan.commandInfo.command === "batch" || isNavigationObservableCommandName(prepared.executionPlan.commandInfo.command));
245
262
  if (succeeded &&
246
263
  !navigationSummary &&
247
- (shouldCaptureNavigationSummary(prepared.executionPlan.commandInfo.command, presentationEnvelope?.data) ||
264
+ (shouldCaptureNavigationSummary(prepared.executionPlan.commandInfo.command, presentationEnvelope?.data, prepared.executionPlan.commandInfo.subcommand) ||
248
265
  shouldCaptureSemanticActionNavigationSummary(prepared.compiledSemanticAction, presentationEnvelope?.data) ||
249
- shouldCaptureAllowedDomainNavigationSummary ||
250
- managedSessionCommandRequiresLivePageVerification(prepared.executionPlan.effectiveArgs, prepared.runtimeToolStdin) ||
251
- (prepared.executionPlan.commandInfo.command === "tab" && prepared.executionPlan.commandInfo.subcommand === "close"))) {
252
- navigationSummary = await collectNavigationSummary({ cwd, namespace: prepared.executionPlan.namespace, priorTarget: prepared.priorSessionTabTarget, 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 });
253
269
  }
254
270
  // 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
271
+ // agent through a manual get url round trip. Probe the live URL ourselves instead: an observed page
272
+ // stays verified, and a failed
257
273
  // probe preserves the existing unknown-target behavior.
258
274
  if (succeeded === false &&
259
275
  !navigationSummary &&
@@ -281,15 +297,16 @@ export async function processBrowserOutput(input) {
281
297
  }
282
298
  const verifiesCurrentUrl = prepared.executionPlan.commandInfo.command === "get" && prepared.executionPlan.commandInfo.subcommand === "url";
283
299
  const trustsReportedPageTarget = !resultingPageState.pageUrlUnknown || verifiesCurrentUrl;
284
- const observedSessionTabTarget = normalizeSessionTabTarget(navigationSummary)
285
- ?? (trustsReportedPageTarget ? extractSessionTabTargetFromBatchResults(presentationEnvelope?.data) : undefined)
286
- ?? (succeeded && trustsReportedPageTarget ? extractSessionTabTargetFromCommandData(prepared.commandTokens, presentationEnvelope?.data) : undefined);
287
- const observedPageValidationError = succeeded ? getObservedBrowserPageValidationError(prepared.executionPlan.effectiveArgs, observedSessionTabTarget?.url, cwd) : undefined;
288
- if (observedPageValidationError) {
289
- succeeded = false;
290
- presentationEnvelope = { error: observedPageValidationError, success: false };
291
- }
292
- const safeObservedSessionTabTarget = observedPageValidationError ? normalizeSessionTabTarget({ url: observedSessionTabTarget?.url }) : observedSessionTabTarget;
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;
293
310
  let currentSessionTabTarget = safeObservedSessionTabTarget;
294
311
  if (!currentSessionTabTarget && nestedBatchClose === undefined) {
295
312
  currentSessionTabTarget = resultingPageState.pageTargetMayHaveChanged
@@ -301,7 +318,7 @@ export async function processBrowserOutput(input) {
301
318
  let electronRefFreshnessDiagnostic;
302
319
  let electronSessionMismatch;
303
320
  let electronStatusAfterCommand;
304
- const shouldTreatAboutBlankAsMismatch = succeeded && nestedBatchClose === undefined && 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);
305
322
  let sessionTabCorrection = prepared.sessionTabCorrection;
306
323
  if (shouldTreatAboutBlankAsMismatch && prepared.priorSessionTabTarget) {
307
324
  const aboutBlankObservedTarget = observedSessionTabTarget ?? currentSessionTabTarget;
@@ -328,18 +345,6 @@ export async function processBrowserOutput(input) {
328
345
  sessionTabCorrection = appliedPostCommandCorrection;
329
346
  }
330
347
  }
331
- if (succeeded && sessionStateKey && parsedAllowedDomainsPolicy) {
332
- allowedDomainsBySession = new Map(allowedDomainsBySession);
333
- allowedDomainsBySession.set(sessionStateKey, parsedAllowedDomainsPolicy);
334
- }
335
- const allowedDomainsViolation = succeeded ? getAllowedDomainsViolation({
336
- policy: sessionAllowedDomainsPolicy,
337
- url: currentSessionTabTarget?.url ?? observedSessionTabTarget?.url ?? navigationSummary?.url,
338
- }) : undefined;
339
- if (allowedDomainsViolation) {
340
- succeeded = false;
341
- presentationEnvelope = { ...(presentationEnvelope ?? {}), error: allowedDomainsViolation.summary, success: false };
342
- }
343
348
  const electronRecordForCommand = findElectronLaunchRecordForSession(prepared.executionPlan.sessionName, electronLaunchRecords, prepared.executionPlan.namespace);
344
349
  if (succeeded && electronRecordForCommand && shouldInspectElectronPostCommandHealth(prepared.executionPlan.commandInfo.command)) {
345
350
  electronStatusAfterCommand ??= await inspectElectronLaunchStatus(electronRecordForCommand);
@@ -391,8 +396,6 @@ export async function processBrowserOutput(input) {
391
396
  const sessionClosed = (isCloseCommand(prepared.executionPlan.commandInfo.command) && succeeded) || nestedBatchClosed;
392
397
  if (sessionClosed) {
393
398
  state.attachedSessionKeys.delete(sessionStateKey);
394
- allowedDomainsBySession = new Map(allowedDomainsBySession);
395
- allowedDomainsBySession.delete(sessionStateKey);
396
399
  networkRoutesBySession = new Map(networkRoutesBySession);
397
400
  networkRoutesBySession.delete(sessionStateKey);
398
401
  sessionPageState.clearSession(sessionStateKey);
@@ -400,17 +403,21 @@ export async function processBrowserOutput(input) {
400
403
  }
401
404
  else {
402
405
  // 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;
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;
414
421
  if (currentSessionTabTarget) {
415
422
  const tabUpdate = sessionPageState.applyTabTarget({ sessionName: sessionStateKey, target: currentSessionTabTarget, update: sessionPageStateUpdate });
416
423
  if (!tabUpdate.applied && succeeded)
@@ -419,7 +426,13 @@ export async function processBrowserOutput(input) {
419
426
  else if (processResult.agentBrowserStarted && (resultingPageState.pageUrlUnknown || resultingPageState.pageTargetMayHaveChanged)) {
420
427
  sessionPageState.markTabTargetUnknown({ sessionName: sessionStateKey, update: sessionPageStateUpdate });
421
428
  }
422
- const refSnapshot = prepared.executionPlan.commandInfo.command === "batch" ? batchRefSnapshotState?.snapshot : succeeded ? prepared.executionPlan.commandInfo.command === "snapshot" ? extractRefSnapshotFromData(presentationEnvelope?.data) : prepared.resolvedSemanticActionRefSnapshot ?? overlayBlockerDiagnostic?.snapshot : undefined;
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;
423
436
  if (refSnapshot) {
424
437
  const refUpdate = sessionPageState.applyRefSnapshot({ fallbackTarget: currentSessionTabTarget, sessionName: sessionStateKey, snapshot: refSnapshot, update: sessionPageStateUpdate });
425
438
  currentRefSnapshot = refUpdate.refSnapshot;
@@ -458,7 +471,6 @@ export async function processBrowserOutput(input) {
458
471
  : closeCommandSucceeded && prepared.executionPlan.sessionName === priorManagedSessionName && closeTargetsPriorManagedNamespace
459
472
  ? prepared.executionPlan.sessionName
460
473
  : prepared.executionPlan.managedSessionName;
461
- const policyBlockedFreshManagedSession = allowedDomainsViolation !== undefined && prepared.sessionMode === "fresh" && prepared.executionPlan.managedSessionName === prepared.executionPlan.sessionName;
462
474
  const postLaunchBatchFailure = !succeeded && processSucceeded && parseSucceeded && prepared.sessionMode === "fresh" && prepared.executionPlan.commandInfo.command === "batch" && batchStartedManagedBrowser(presentationEnvelope?.data);
463
475
  const postLaunchTimeoutWithPage = !succeeded && processResult.timedOut && prepared.sessionMode === "fresh" && prepared.executionPlan.commandInfo.command === "batch" && timeoutPartialProgress?.liveUrlRecovered === true;
464
476
  const failedFreshSessionMayHaveStarted = !succeeded
@@ -479,7 +491,7 @@ export async function processBrowserOutput(input) {
479
491
  }
480
492
  // Only a confirmed inactive daemon proves that a started fresh command did not establish browser ownership.
481
493
  const postLaunchFreshFailure = failedFreshDaemon !== undefined && failedFreshDaemon.status !== "inactive";
482
- const managedTransitionSucceeded = succeeded || nestedBatchClosed || nestedBatchRemainsActive || policyBlockedFreshManagedSession || postLaunchBatchFailure || postLaunchTimeoutWithPage || postLaunchFreshFailure;
494
+ const managedTransitionSucceeded = succeeded || nestedBatchClosed || nestedBatchRemainsActive || postLaunchBatchFailure || postLaunchTimeoutWithPage || postLaunchFreshFailure;
483
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 });
484
496
  if (!managedTransitionSucceeded && prepared.sessionMode === "fresh" && prepared.executionPlan.managedSessionName) {
485
497
  state.managedSessionRestoreState.clear(prepared.executionPlan.managedSessionName, prepared.executionPlan.namespace);
@@ -528,9 +540,7 @@ export async function processBrowserOutput(input) {
528
540
  sessionPageState.clearRestorePinning(sessionStateKey);
529
541
  }
530
542
  if (replacedManagedSessionName) {
531
- allowedDomainsBySession = new Map(allowedDomainsBySession);
532
543
  const replacedSessionStateKey = getSessionContextKey(replacedManagedSessionName, priorManagedSessionNamespace);
533
- allowedDomainsBySession.delete(replacedSessionStateKey ?? replacedManagedSessionName);
534
544
  networkRoutesBySession = new Map(networkRoutesBySession);
535
545
  networkRoutesBySession.delete(replacedSessionStateKey ?? replacedManagedSessionName);
536
546
  sessionPageState.clearSession(replacedSessionStateKey ?? replacedManagedSessionName);
@@ -585,8 +595,6 @@ export async function processBrowserOutput(input) {
585
595
  }
586
596
  if (!closeError) {
587
597
  state.closedManagedSessionNames.add(electronSessionStateKey);
588
- allowedDomainsBySession = new Map(allowedDomainsBySession);
589
- allowedDomainsBySession.delete(electronSessionStateKey);
590
598
  networkRoutesBySession = new Map(networkRoutesBySession);
591
599
  networkRoutesBySession.delete(electronSessionStateKey);
592
600
  sessionPageState.clearSession(electronSessionStateKey);
@@ -626,8 +634,6 @@ export async function processBrowserOutput(input) {
626
634
  presentationEnvelope = { ...presentationEnvelope, error: redactClipboardPermissionErrorValue(prepared.executionPlan.commandInfo, presentationEnvelope.error, clipboardWritePayloadCandidates) };
627
635
  }
628
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 });
629
- if (observedPageValidationError)
630
- presentation.failureCategory = "validation-error";
631
637
  if (electronHandoff?.error && electronHandoff.failureCategory)
632
638
  presentation.failureCategory = electronHandoff.failureCategory;
633
639
  networkRoutesBySession = applyBatchNetworkRouteState({ data: presentationEnvelope?.data, routesBySession: networkRoutesBySession, sessionName: sessionStateKey, succeeded });
@@ -749,11 +755,11 @@ export async function processBrowserOutput(input) {
749
755
  const resultHeadedManagedAutosaveInterval = resultRetainsPreparedManagedSession && !(commandClosesSession && succeeded)
750
756
  ? prepared.ownedManagedSessionContext?.headedManagedAutosaveInterval
751
757
  : 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 });
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 });
753
759
  const resultWithCloseAll = closeAllApplied
754
760
  ? { ...result, details: { ...(isRecord(result.details) ? result.details : {}), closeAllApplied: true } }
755
761
  : result;
756
- const statePatch = { allowedDomainsBySession, artifactManifest, freshSessionOrdinal, managedSessionActive, managedSessionCompatibilityWorkaround, managedSessionHeadedAutosaveDisabled, managedSessionHeadedAutosaveInterval, managedSessionCwd, managedSessionName, managedSessionNamespace, networkRoutesBySession };
762
+ const statePatch = { artifactManifest, freshSessionOrdinal, managedSessionActive, managedSessionCompatibilityWorkaround, managedSessionHeadedAutosaveDisabled, managedSessionHeadedAutosaveInterval, managedSessionCwd, managedSessionName, managedSessionNamespace, networkRoutesBySession };
757
763
  return { result: resultWithCloseAll, statePatch };
758
764
  }
759
765
  finally {
@@ -13,8 +13,6 @@ export const NAVIGATION_SUMMARY_EVAL = `({ title: document.title, url: location.
13
13
  export function applyBrowserRunStatePatch(state, patch) {
14
14
  if (!patch)
15
15
  return;
16
- if (patch.allowedDomainsBySession)
17
- state.allowedDomainsBySession = patch.allowedDomainsBySession;
18
16
  if ("artifactManifest" in patch)
19
17
  state.artifactManifest = patch.artifactManifest;
20
18
  if (patch.freshSessionOrdinal !== undefined)
@@ -249,12 +247,13 @@ export function extractNavigationSummaryFromData(data) {
249
247
  const result = isRecord(data) && isRecord(data.result) ? data.result : data;
250
248
  const title = extractStringResultField(result, "title");
251
249
  const url = extractStringResultField(result, "url");
252
- return title || url ? { title, url } : undefined;
250
+ const urlChanged = isRecord(result) && typeof result.urlChanged === "boolean" ? result.urlChanged : undefined;
251
+ return title || url ? { title, url, urlChanged } : undefined;
253
252
  }
254
- export function shouldCaptureNavigationSummary(command, data) {
253
+ export function shouldCaptureNavigationSummary(command, data, subcommand) {
255
254
  if (command === "eval")
256
255
  return true;
257
- return (isNavigationObservableCommandName(command) &&
256
+ return (isNavigationObservableCommandName(command, subcommand) &&
258
257
  (!isRecord(data) || (typeof data.title !== "string" && typeof data.url !== "string")));
259
258
  }
260
259
  export function mergeNavigationSummaryIntoData(data, navigationSummary) {
@@ -442,7 +441,7 @@ export function buildPinnedBatchPlan(options) {
442
441
  if (options.commandTokens.length === 0) {
443
442
  return undefined;
444
443
  }
445
- const includeNavigationSummary = isNavigationObservableCommandName(options.command);
444
+ const includeNavigationSummary = isNavigationObservableCommandName(options.command, options.commandTokens[1]);
446
445
  const tabSelectionStep = ["tab", options.selectedTab];
447
446
  const commandStep = options.commandTokens;
448
447
  const navigationSummarySteps = includeNavigationSummary ? [["eval", NAVIGATION_SUMMARY_EVAL]] : [];
@@ -532,11 +531,10 @@ export function unwrapPinnedSessionBatchEnvelope(options) {
532
531
  };
533
532
  }
534
533
  export async function runSessionCommandData(options) {
535
- const { allowManagedSessionTarget, args, cwd, namespace, pinNamespace, sessionName, signal, stdin, throwOnFailure, timeoutMs } = options;
534
+ const { args, cwd, namespace, pinNamespace, sessionName, signal, stdin, throwOnFailure, timeoutMs } = options;
536
535
  if (!sessionName)
537
536
  return undefined;
538
537
  const processResult = await runAgentBrowserProcess({
539
- allowManagedSessionTarget,
540
538
  args: ["--json", ...(namespace !== undefined || pinNamespace ? ["--namespace", namespace ?? ""] : []), "--session", sessionName, ...args],
541
539
  cwd,
542
540
  signal,
@@ -2,7 +2,7 @@ import { cleanupElectronLaunchResources, inspectElectronLaunchStatus } from "../
2
2
  import { discoverElectronApps } from "../../electron/discovery.js";
3
3
  import { boundElectronProbeString } from "../../electron/cdp.js";
4
4
  import { buildOwnedManagedSessionRestoreContext, withOwnedManagedSessionContext, } from "../../managed-session-restore.js";
5
- import { getManagedSessionStateAccessValidationError } from "../../managed-session-state-policy.js";
5
+ import { getPageTargetValidationError } from "../../page-target-validation.js";
6
6
  import { isRecord } from "../../parsing.js";
7
7
  import { withAttachedBrowserSessionContext } from "../../process.js";
8
8
  import { buildAgentBrowserNextActions } from "../../results/action-recommendations.js";
@@ -431,7 +431,6 @@ async function withOwnedElectronManagedSessionPolicy(options, run) {
431
431
  async function collectOwnedElectronManagedSessionTarget(options) {
432
432
  try {
433
433
  const target = await withOwnedElectronManagedSessionPolicy({ ...options, args: ["get", "url"] }, async () => await collectElectronManagedSessionTarget({
434
- allowManagedSessionTarget: true,
435
434
  cwd: options.cwd,
436
435
  namespace: options.namespace,
437
436
  sessionName: options.sessionName,
@@ -446,7 +445,7 @@ async function collectOwnedElectronManagedSessionTarget(options) {
446
445
  }
447
446
  async function runElectronProbeCommandData(options) {
448
447
  try {
449
- return { data: await runSessionCommandData({ ...options, allowManagedSessionTarget: true, pinNamespace: true, throwOnFailure: true }) };
448
+ return { data: await runSessionCommandData({ ...options, pinNamespace: true, throwOnFailure: true }) };
450
449
  }
451
450
  catch (error) {
452
451
  return { error: error instanceof Error ? error.message : String(error) };
@@ -460,9 +459,6 @@ async function collectElectronProbe(options) {
460
459
  const url = boundElectronProbeString(extractStringResultField(urlResult.data, "result") ?? extractStringResultField(urlResult.data, "url"), 300);
461
460
  if (!url)
462
461
  throw new Error("get url returned no active page URL.");
463
- const fileAccessError = getManagedSessionStateAccessValidationError({ args: ["snapshot", "-i"], currentPageUrl: url, cwd: options.cwd });
464
- if (fileAccessError)
465
- throw new ElectronManagedSessionPolicyError(fileAccessError);
466
462
  const titleResult = await runElectronProbeCommandData({ ...commandContext, args: ["get", "title"] });
467
463
  const focusedResult = await runElectronProbeCommandData({ ...commandContext, args: ["eval", "--stdin"], stdin: ELECTRON_FOCUSED_ELEMENT_EVAL });
468
464
  const tabsResult = await runElectronProbeCommandData({ ...commandContext, args: ["tab", "list"] });
@@ -739,14 +735,13 @@ async function handleElectronHostInputInContext(options) {
739
735
  const probeNamespace = compiledElectron.launchId ? launchRecord?.namespace : managedSessionNamespace;
740
736
  const pageStateKey = getSessionPageStateKey(probeSessionName, probeNamespace) ?? probeSessionName;
741
737
  const currentPageState = sessionPageState.get(pageStateKey);
742
- const fileAccessError = getManagedSessionStateAccessValidationError({
738
+ const pageTargetError = getPageTargetValidationError({
743
739
  args: ["snapshot", "-i"],
744
740
  currentPageUrl: currentPageState.tabTarget?.url,
745
- cwd,
746
741
  pageUrlUnknown: currentPageState.tabTargetUnknown === true,
747
742
  });
748
- if (fileAccessError)
749
- throw new ElectronManagedSessionPolicyError(fileAccessError);
743
+ if (pageTargetError)
744
+ throw new ElectronManagedSessionPolicyError(pageTargetError);
750
745
  const managedSessionOwner = ownedManagedSessions.get(pageStateKey);
751
746
  const headedManagedAutosaveDisabled = managedSessionOwner?.headedManagedAutosaveDisabled === true;
752
747
  const headedManagedAutosaveInterval = managedSessionOwner?.headedManagedAutosaveInterval;
@@ -1,7 +1,7 @@
1
- import { mkdir, realpath, stat, writeFile } from "node:fs/promises";
2
- import { dirname, isAbsolute, resolve } from "node:path";
3
- import { getAgentBrowserStoragePathValidationError } from "../managed-session-state-policy.js";
1
+ import { mkdir, readFile, realpath, stat, writeFile } from "node:fs/promises";
2
+ import { dirname, extname, isAbsolute, resolve } from "node:path";
4
3
  import { isRecord } from "../parsing.js";
4
+ import { isSessionArtifactManifest } from "../results/artifact-manifest.js";
5
5
  export function normalizeRequestedOutputPath(path) {
6
6
  return path.startsWith("@") ? path.slice(1) : path;
7
7
  }
@@ -11,11 +11,41 @@ function getTextContent(result) {
11
11
  .map((item) => item.text)
12
12
  .join("\n\n") ?? "";
13
13
  }
14
- function getOutputPayload(result) {
14
+ function getOutputSource(result) {
15
+ return isRecord(result.details) && result.details.data !== undefined ? "details.data" : "content.text";
16
+ }
17
+ async function readCompactedSpill(path, manifest) {
18
+ if (!path || !manifest?.entries.some((entry) => entry.kind === "spill"
19
+ && (entry.path === path || entry.absolutePath === path)
20
+ && (entry.storageScope === "persistent-session" || entry.storageScope === "process-temp")
21
+ && (entry.retentionState === "live" || entry.retentionState === "ephemeral")))
22
+ throw new Error("Full compacted output is unavailable from the wrapper-managed spill; outputPath was not written.");
23
+ const text = await readFile(path, "utf8");
24
+ return extname(path) === ".json" ? JSON.parse(text) : text;
25
+ }
26
+ async function rehydrateCompactedData(data, details, manifest) {
27
+ if (isRecord(data) && data.compacted === true) {
28
+ return readCompactedSpill(typeof details.fullOutputPath === "string" ? details.fullOutputPath : undefined, manifest);
29
+ }
30
+ if (!Array.isArray(data))
31
+ return data;
32
+ const batchSteps = Array.isArray(details.batchSteps) ? details.batchSteps : [];
33
+ return Promise.all(data.map(async (row, index) => {
34
+ if (!isRecord(row) || !isRecord(row.result) || row.result.compacted !== true)
35
+ return row;
36
+ const step = isRecord(batchSteps[index]) ? batchSteps[index] : undefined;
37
+ const path = typeof step?.fullOutputPath === "string"
38
+ ? step.fullOutputPath
39
+ : typeof row.result.fullOutputPath === "string" ? row.result.fullOutputPath : undefined;
40
+ return { ...row, result: await readCompactedSpill(path, manifest) };
41
+ }));
42
+ }
43
+ async function getOutputPayload(result) {
15
44
  const details = isRecord(result.details) ? result.details : undefined;
16
- if (details && details.data !== undefined)
17
- return { source: "details.data", value: details.data };
18
- return { source: "content.text", value: getTextContent(result) };
45
+ if (details?.data === undefined)
46
+ return { source: "content.text", value: getTextContent(result) };
47
+ const manifest = isSessionArtifactManifest(details.artifactManifest) ? details.artifactManifest : undefined;
48
+ return { source: "details.data", value: await rehydrateCompactedData(details.data, details, manifest) };
19
49
  }
20
50
  function serializeOutputPayload(value) {
21
51
  return typeof value === "string" ? value : `${JSON.stringify(value, null, 2)}\n`;
@@ -28,9 +58,6 @@ function appendOutputFileNotice(result, message) {
28
58
  }
29
59
  return [{ type: "text", text: message }, ...content];
30
60
  }
31
- export function getAgentBrowserOutputPathValidationError(outputPath, cwd) {
32
- return outputPath ? getAgentBrowserStoragePathValidationError(normalizeRequestedOutputPath(outputPath), cwd) : undefined;
33
- }
34
61
  function getArtifactPaths(result, cwd) {
35
62
  const details = isRecord(result.details) ? result.details : undefined;
36
63
  if (!details || !Array.isArray(details.artifacts))
@@ -61,24 +88,16 @@ async function pathsReferToSameFile(left, right) {
61
88
  export async function applyAgentBrowserOutputPath(options) {
62
89
  if (!options.outputPath)
63
90
  return options.result;
64
- const validationError = getAgentBrowserOutputPathValidationError(options.outputPath, options.cwd);
65
- if (validationError) {
66
- return {
67
- content: [{ type: "text", text: validationError }],
68
- details: { failureCategory: "validation-error", resultCategory: "failure", validationError },
69
- isError: true,
70
- };
71
- }
72
91
  if (options.result.isError || (isRecord(options.result.details) && options.result.details.resultCategory === "failure"))
73
92
  return options.result;
74
93
  const requestedPath = normalizeRequestedOutputPath(options.outputPath);
75
94
  const absolutePath = isAbsolute(requestedPath) ? requestedPath : resolve(options.cwd, requestedPath);
76
- const payload = getOutputPayload(options.result);
95
+ const source = getOutputSource(options.result);
77
96
  for (const artifactPath of getArtifactPaths(options.result, options.cwd)) {
78
97
  if (!await pathsReferToSameFile(absolutePath, artifactPath))
79
98
  continue;
80
99
  const message = "outputPath resolves to the same file as a browser artifact destination; choose a separate outputPath or omit it. The browser artifact was preserved.";
81
- const outputFile = { absolutePath, error: message, path: requestedPath, source: payload.source, status: "failed" };
100
+ const outputFile = { absolutePath, error: message, path: requestedPath, source, status: "failed" };
82
101
  const details = isRecord(options.result.details) ? { ...options.result.details } : {};
83
102
  delete details.successCategory;
84
103
  return {
@@ -89,6 +108,7 @@ export async function applyAgentBrowserOutputPath(options) {
89
108
  };
90
109
  }
91
110
  try {
111
+ const payload = await getOutputPayload(options.result);
92
112
  const serialized = serializeOutputPayload(payload.value);
93
113
  await mkdir(dirname(absolutePath), { recursive: true });
94
114
  await writeFile(absolutePath, serialized, "utf8");
@@ -103,7 +123,7 @@ export async function applyAgentBrowserOutputPath(options) {
103
123
  }
104
124
  catch (error) {
105
125
  const message = error instanceof Error ? error.message : String(error);
106
- const outputFile = { absolutePath, error: message, path: requestedPath, source: payload.source, status: "failed" };
126
+ const outputFile = { absolutePath, error: message, path: requestedPath, source, status: "failed" };
107
127
  const details = isRecord(options.result.details)
108
128
  ? (() => {
109
129
  const rest = { ...options.result.details };