pi-agent-browser-native 0.6.10 → 0.6.12

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 (48) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +50 -7
  3. package/dist/extensions/agent-browser/index.js +416 -430
  4. package/dist/extensions/agent-browser/lib/argv-grammar.js +1 -1
  5. package/dist/extensions/agent-browser/lib/command-policy.js +41 -2
  6. package/dist/extensions/agent-browser/lib/input-modes/job.js +7 -7
  7. package/dist/extensions/agent-browser/lib/input-modes/params.js +1 -1
  8. package/dist/extensions/agent-browser/lib/input-modes/script.js +3 -2
  9. package/dist/extensions/agent-browser/lib/managed-session-restore.js +40 -10
  10. package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +3 -0
  11. package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +8 -2
  12. package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +1 -0
  13. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/wait-timeouts.js +3 -2
  14. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +25 -14
  15. package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +56 -10
  16. package/dist/extensions/agent-browser/lib/orchestration/browser-run/recording-recovery.js +161 -0
  17. package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +3 -3
  18. package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +2 -4
  19. package/dist/extensions/agent-browser/lib/orchestration/native-session-defaults.js +68 -0
  20. package/dist/extensions/agent-browser/lib/orchestration/output-file.js +41 -6
  21. package/dist/extensions/agent-browser/lib/page-target-validation.js +9 -5
  22. package/dist/extensions/agent-browser/lib/playbook.js +10 -9
  23. package/dist/extensions/agent-browser/lib/process-environment.js +26 -8
  24. package/dist/extensions/agent-browser/lib/process.js +8 -5
  25. package/dist/extensions/agent-browser/lib/read-confirmation.js +59 -0
  26. package/dist/extensions/agent-browser/lib/recording-reservations.js +11 -1
  27. package/dist/extensions/agent-browser/lib/results/action-recommendations.js +8 -0
  28. package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +6 -5
  29. package/dist/extensions/agent-browser/lib/results/categories.js +4 -2
  30. package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +60 -29
  31. package/dist/extensions/agent-browser/lib/results/presentation/batch.js +19 -8
  32. package/dist/extensions/agent-browser/lib/results/presentation/common.js +0 -20
  33. package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +40 -38
  34. package/dist/extensions/agent-browser/lib/results/presentation/errors.js +1 -0
  35. package/dist/extensions/agent-browser/lib/results/presentation/navigation.js +3 -3
  36. package/dist/extensions/agent-browser/lib/results/presentation.js +38 -9
  37. package/dist/extensions/agent-browser/lib/results/recording.js +50 -0
  38. package/dist/extensions/agent-browser/lib/runtime.js +72 -20
  39. package/dist/extensions/agent-browser/lib/session-page-state.js +23 -7
  40. package/dist/extensions/agent-browser/lib/temp.js +4 -0
  41. package/docs/ARCHITECTURE.md +27 -11
  42. package/docs/COMMAND_REFERENCE.md +37 -17
  43. package/docs/RELEASE.md +3 -1
  44. package/docs/SUPPORT_MATRIX.md +10 -4
  45. package/docs/TOOL_CONTRACT.md +87 -25
  46. package/docs/platform-smoke.md +9 -1
  47. package/package.json +1 -1
  48. package/scripts/prepare.mjs +2 -4
@@ -1,6 +1,6 @@
1
1
  import { rm } from "node:fs/promises";
2
2
  import { parseArgvDescriptor } from "../../argv-descriptor.js";
3
- import { needsManagedSession } from "../../command-policy.js";
3
+ import { isBrowserIndependentRead, needsManagedSession } from "../../command-policy.js";
4
4
  import { deleteIdentityKeysInNamespace, getAgentBrowserSessionIdentityKey, isAgentBrowserSessionIdentityKeyInNamespace } from "../../argv-grammar.js";
5
5
  import { batchHasSuccessfulCloseAll, getSuccessfulBatchCloseLifecycle } from "../../batch-lifecycle.js";
6
6
  import { isCloseAllCommand, isCloseCommand, isOpenNavigationCommand, isRecordPageTransitionCommand, isUnverifiedPageTransitionCommand, isWindowOrDiffPageTransitionCommand } from "../../command-taxonomy.js";
@@ -13,11 +13,13 @@ import { applyNetworkRouteRecords, buildNetworkRouteDiagnostics } from "../../re
13
13
  import { buildToolPresentation } from "../../results/presentation.js";
14
14
  import { compactLargePresentationOutput } from "../../results/presentation/large-output.js";
15
15
  import { extractEnvelopeErrorText, getAgentBrowserErrorText, parseAgentBrowserEnvelope } from "../../results/envelope.js";
16
+ import { detectConfirmationRequired } from "../../results/confirmation.js";
16
17
  import { omitUpstreamLifecycle } from "../../results/presentation/common.js";
17
18
  import { getClipboardWritePayloadCandidates, redactClipboardPermissionEcho, redactClipboardPermissionErrorValue } from "../../results/presentation/errors.js";
18
19
  import { shouldCaptureSemanticActionNavigationSummary } from "../../results/presentation/semantic-action.js";
19
20
  import { buildPageTransitionRefSnapshotInvalidation, commandExplicitlyTargetsAboutBlank, getCommandRefSnapshotInvalidation, deriveSessionTabTarget, extractLatestRefSnapshotStateFromBatchResults, extractRefSnapshotFromData, extractSessionTabTargetFromBatchResults, extractSessionTabTargetFromCommandData, isAboutBlankSessionTabTarget, normalizeSessionTabTarget, } from "../../session-page-state.js";
20
21
  import { isRecord } from "../../parsing.js";
22
+ import { buildReadConfirmationNextActions, nextReadConfirmation } from "../../read-confirmation.js";
21
23
  import { pruneOwnedManagedSessionRestoreSnapshots } from "../../managed-session-restore.js";
22
24
  import { isManagedSessionRestoreKey } from "../../managed-session-storage.js";
23
25
  import { createFreshSessionName, extractUpstreamCommandTokens, resolveManagedSessionState } from "../../runtime.js";
@@ -27,6 +29,7 @@ import { applyOpenResultTabCorrection, buildAboutBlankRecoveryHint, buildAboutBl
27
29
  import { collectClickDispatchDiagnostic } from "./click-dispatch.js";
28
30
  import { buildScrollNoopDiagnostic, collectComboboxFocusDiagnostic, collectElectronBroadGetTextScopeDiagnostics, collectElectronHandoff, collectFillVerificationDiagnostic, collectNavigationSummary, collectOverlayBlockerDiagnostic, collectQaAttachedTarget, collectSnapshotOverlayBlockerDiagnostic, collectRecordingDependencyWarning, collectScrollPositionSnapshot, collectSelectorTextVisibilityDiagnostics, collectTimeoutPartialProgress, sleepMs, formatQaAttachedTargetText, getArtifactCleanupGuidance, getEvalResultWarning, getEvalStdinHint, getSourceLookupElectronContext, } from "./diagnostics.js";
29
31
  import { repairScreenshotData } from "./prepare.js";
32
+ import { mergeRecordingRecoveryPresentation, recoverRecordingStop } from "./recording-recovery.js";
30
33
  import { getPersistentSessionArtifactStore } from "./session-state.js";
31
34
  import { buildFinalAgentBrowserToolResult, buildRedactedPresentationContent, buildWrapperRecoveryHint, prepareFinalResultRecoveryState, redactExactSensitiveValue, } from "./final-result.js";
32
35
  async function repairScreenshotArtifact(options) {
@@ -161,7 +164,13 @@ export async function processBrowserOutput(input) {
161
164
  const plainTextUpgrade = !prepared.executionPlan.plainTextInspection && prepared.executionPlan.commandInfo.command === "upgrade" && !needsManagedSession(parseArgvDescriptor(prepared.runtimeToolArgs));
162
165
  const parsed = await parseAgentBrowserEnvelope({ stdout: processResult.stdout, stdoutPath: processResult.stdoutSpillPath, plainText: plainTextUpgrade });
163
166
  let parseError = parsed.parseError;
164
- let presentationEnvelope = parsed.envelope;
167
+ const recordingStopRecovery = await recoverRecordingStop({
168
+ artifactManifest, artifactRunStartedAtMs: input.artifactRunStartedAtMs, commandTokens: prepared.commandTokens, cwd,
169
+ envelope: parsed.envelope, namespace: prepared.executionPlan.namespace, parseError, processResult,
170
+ reservation: prepared.executionPlan.sessionName ? state.activeRecordingReservations?.get(getAgentBrowserSessionIdentityKey(prepared.executionPlan.sessionName, prepared.executionPlan.namespace)) : undefined,
171
+ sessionName: prepared.executionPlan.sessionName, signal, stdin: prepared.runtimeToolStdin,
172
+ });
173
+ let presentationEnvelope = recordingStopRecovery?.envelope ?? parsed.envelope;
165
174
  let navigationSummary = undefined;
166
175
  let failedTransitionReverification = false;
167
176
  const repairedScreenshot = await repairScreenshotArtifact({ cwd, envelope: presentationEnvelope, request: prepared.preparedArgs.screenshotPathRequest });
@@ -178,6 +187,25 @@ export async function processBrowserOutput(input) {
178
187
  : Array.isArray(presentationEnvelope?.data)
179
188
  ? presentationEnvelope.data.flatMap((row, index) => isRecord(row) ? [Array.isArray(row.command) && row.command.every((token) => typeof token === "string") ? row.command : batchCommandSteps[index] ?? []] : [])
180
189
  : batchCommandSteps;
190
+ const confirmationSessionName = prepared.executionPlan.sessionName ?? "default";
191
+ let readConfirmation = state.sessionPageState.getReadConfirmation(getAgentBrowserSessionIdentityKey(confirmationSessionName, prepared.executionPlan.namespace));
192
+ let readConfirmationEvent;
193
+ const confirmationRows = prepared.executionPlan.commandInfo.command === "batch" && Array.isArray(presentationEnvelope?.data)
194
+ ? presentationEnvelope.data.flatMap((row, index) => isRecord(row) ? [{ tokens: batchCommandSteps[index] ?? [], data: row.result, response: row, succeeded: row.success === true }] : [])
195
+ : [{ tokens: prepared.commandTokens, data: presentationEnvelope?.data, response: presentationEnvelope, succeeded: presentationEnvelope?.success === true }];
196
+ for (const row of confirmationRows) {
197
+ const transition = nextReadConfirmation({ commandTokens: row.tokens, current: readConfirmation, data: row.data, namespace: prepared.executionPlan.namespace, sessionName: confirmationSessionName, succeeded: row.succeeded });
198
+ if (transition) {
199
+ readConfirmationEvent = transition;
200
+ readConfirmation = transition;
201
+ }
202
+ if (row.tokens.length === 2 && row.tokens[0] === "confirm" && isRecord(row.data) && row.data.confirmed === true && row.data.action === "read" && isRecord(row.data.result) && row.data.result.success === false && row.response) {
203
+ row.response.success = false;
204
+ row.response.error = row.data.result.error;
205
+ if (presentationEnvelope)
206
+ presentationEnvelope.success = false;
207
+ }
208
+ }
181
209
  const destinationTransition = dispatchedCommands.some((step) => {
182
210
  const [command, subcommand] = extractUpstreamCommandTokens(step);
183
211
  return isWindowOrDiffPageTransitionCommand(command, subcommand);
@@ -200,6 +228,7 @@ export async function processBrowserOutput(input) {
200
228
  const parseFailureOutput = parseError && processResult.stdoutSpillPath
201
229
  ? { fullOutputUnavailable: "Malformed upstream output was discarded because it may contain sensitive browser data." }
202
230
  : {};
231
+ const browserIndependentRead = prepared.readConfirmation !== undefined || isBrowserIndependentRead(prepared.commandTokens, prepared.runtimeToolStdin);
203
232
  const processSucceeded = !processResult.timedOut && !processResult.aborted && !processResult.spawnError && processResult.exitCode === 0;
204
233
  const plainTextInspection = prepared.executionPlan.plainTextInspection && processSucceeded;
205
234
  const parseSucceeded = plainTextInspection || parseError === undefined;
@@ -207,7 +236,7 @@ export async function processBrowserOutput(input) {
207
236
  presentationEnvelope = { success: true, data: { alreadyEnabled: true, enabled: true, message: getEnvelopeErrorString(presentationEnvelope) ?? "Stream already enabled" } };
208
237
  }
209
238
  const envelopeSuccess = plainTextInspection ? true : presentationEnvelope?.success !== false;
210
- let succeeded = processSucceeded && parseSucceeded && envelopeSuccess;
239
+ let succeeded = (processSucceeded && parseSucceeded && envelopeSuccess) || recordingStopRecovery?.recovery.healed === true;
211
240
  const inspectionText = plainTextInspection ? processResult.stdout.trim() : undefined;
212
241
  const sessionStateKey = getSessionContextKey(prepared.executionPlan.sessionName, prepared.executionPlan.namespace);
213
242
  const closeAllApplied = nestedBatchClosesAll || (directCloseAllRequested && succeeded);
@@ -364,7 +393,7 @@ export async function processBrowserOutput(input) {
364
393
  let fillVerificationDiagnostic;
365
394
  let selectorTextVisibilityDiagnostics = [];
366
395
  let electronBroadGetTextScopeDiagnostics = [];
367
- const timeoutPartialProgress = processResult.timedOut ? await collectTimeoutPartialProgress({ commandTokens: prepared.commandTokens, compiledJob: prepared.compiledJob, cwd, namespace: prepared.executionPlan.namespace, sessionName: prepared.executionPlan.sessionName, stdin: prepared.runtimeToolStdin }) : undefined;
396
+ const timeoutPartialProgress = processResult.timedOut && !recordingStopRecovery && !prepared.readConfirmation ? await collectTimeoutPartialProgress({ commandTokens: prepared.commandTokens, compiledJob: prepared.compiledJob, cwd, namespace: prepared.executionPlan.namespace, sessionName: prepared.executionPlan.sessionName, stdin: prepared.runtimeToolStdin }) : undefined;
368
397
  if (!currentSessionTabTarget && timeoutPartialProgress?.currentPage?.source === "live") {
369
398
  currentSessionTabTarget = normalizeSessionTabTarget(timeoutPartialProgress.currentPage);
370
399
  }
@@ -396,7 +425,7 @@ export async function processBrowserOutput(input) {
396
425
  const batchRefSnapshotState = prepared.executionPlan.commandInfo.command === "batch" ? extractLatestRefSnapshotStateFromBatchResults(presentationEnvelope?.data) : undefined;
397
426
  let currentRefSnapshot;
398
427
  let currentRefSnapshotInvalidation;
399
- if (sessionStateKey) {
428
+ if (sessionStateKey && !browserIndependentRead) {
400
429
  const sessionClosed = (isCloseCommand(prepared.executionPlan.commandInfo.command) && succeeded) || nestedBatchClosed;
401
430
  if (sessionClosed) {
402
431
  state.attachedSessionKeys.delete(sessionStateKey);
@@ -427,7 +456,7 @@ export async function processBrowserOutput(input) {
427
456
  if (!tabUpdate.applied && succeeded)
428
457
  sessionPageState.markPinning(sessionStateKey, "drift");
429
458
  }
430
- else if (processResult.agentBrowserStarted && (resultingPageState.pageUrlUnknown || resultingPageState.pageTargetMayHaveChanged)) {
459
+ else if (processResult.agentBrowserStarted && (resultingPageState.pageUrlUnknown || resultingPageState.pageTargetMayHaveChanged) && !(prepared.commandTokens[0] === "session" && prepared.commandTokens[1] === "info")) {
431
460
  sessionPageState.markTabTargetUnknown({ sessionName: sessionStateKey, update: sessionPageStateUpdate });
432
461
  }
433
462
  const refSnapshot = unsettledWebMcpMutation
@@ -512,7 +541,7 @@ export async function processBrowserOutput(input) {
512
541
  managedSessionHeadedAutosaveDisabled = false;
513
542
  managedSessionHeadedAutosaveInterval = undefined;
514
543
  }
515
- else if (managedTransitionSucceeded && executionTargetsManagedSession) {
544
+ else if (managedTransitionSucceeded && executionTargetsManagedSession && prepared.ownedManagedSessionContext && !prepared.ownedManagedSessionContext.reuseOnly) {
516
545
  managedSessionCompatibilityWorkaround = prepared.compatibilityWorkaround;
517
546
  managedSessionHeadedAutosaveDisabled = prepared.ownedManagedSessionContext?.headedManagedAutosaveDisabled === true;
518
547
  managedSessionHeadedAutosaveInterval = prepared.ownedManagedSessionContext?.headedManagedAutosaveInterval;
@@ -630,7 +659,7 @@ export async function processBrowserOutput(input) {
630
659
  electronLaunchRecord = electronFailedConnectCleanup.record;
631
660
  }
632
661
  }
633
- let errorText = getAgentBrowserErrorText({ aborted: processResult.aborted, command: prepared.executionPlan.commandInfo.command, effectiveArgs: prepared.redactedProcessArgs, envelope: presentationEnvelope, exitCode: processResult.exitCode, parseError, plainTextInspection, staleRefArgs: getStaleRefArgs(prepared.commandTokens, prepared.runtimeToolStdin), spawnError: processResult.spawnError, stderr: processResult.stderr, timedOut: processResult.timedOut, timeoutMs: processResult.timeoutMs, wrapperRecoveryHint: buildWrapperRecoveryHint({ sessionTabCorrection }) });
662
+ let errorText = recordingStopRecovery?.recovery.healed ? undefined : getAgentBrowserErrorText({ aborted: processResult.aborted, command: prepared.executionPlan.commandInfo.command, effectiveArgs: prepared.redactedProcessArgs, envelope: presentationEnvelope, exitCode: processResult.exitCode, parseError, plainTextInspection, staleRefArgs: getStaleRefArgs(prepared.commandTokens, prepared.runtimeToolStdin), spawnError: processResult.spawnError, stderr: processResult.stderr, timedOut: processResult.timedOut, timeoutMs: processResult.timeoutMs, wrapperRecoveryHint: buildWrapperRecoveryHint({ sessionTabCorrection }) });
634
663
  if (errorText && presentationEnvelope?.success === false && extractEnvelopeErrorText(presentationEnvelope.error) === undefined)
635
664
  presentationEnvelope = { ...presentationEnvelope, error: errorText };
636
665
  if (errorText) {
@@ -641,7 +670,21 @@ export async function processBrowserOutput(input) {
641
670
  }
642
671
  if (plainTextUpgrade && errorText)
643
672
  presentationEnvelope = { ...presentationEnvelope, success: false, error: errorText };
644
- let presentation = plainTextInspection ? { artifacts: undefined, batchFailure: undefined, batchSteps: undefined, content: [{ type: "text", text: inspectionText ?? "" }], data: undefined, fullOutputPath: undefined, fullOutputPaths: undefined, imagePath: undefined, imagePaths: undefined, savedFile: undefined, savedFilePath: undefined, summary: `${prepared.redactedArgs.join(" ")} completed` } : await buildToolPresentation({ args: prepared.redactedProcessArgs, artifactManifest, artifactMaxUpdatedAtMs: Date.now(), artifactMinUpdatedAtMs: input.artifactRunStartedAtMs, artifactRequest: screenshotArtifactRequest, batchArtifactRequests: batchScreenshotArtifactRequests, commandInfo: prepared.executionPlan.commandInfo, compiledSemanticAction: prepared.compiledSemanticAction, cwd, envelope: presentationEnvelope, errorText, namespace: prepared.executionPlan.namespace, networkRouteDiagnostics, networkRoutes: activeNetworkRoutes, persistentArtifactStore, sessionName: prepared.executionPlan.sessionName });
673
+ let presentation = plainTextInspection ? { artifacts: undefined, batchFailure: undefined, batchSteps: undefined, content: [{ type: "text", text: inspectionText ?? "" }], data: undefined, fullOutputPath: undefined, fullOutputPaths: undefined, imagePath: undefined, imagePaths: undefined, savedFile: undefined, savedFilePath: undefined, summary: `${prepared.redactedArgs.join(" ")} completed` } : recordingStopRecovery && !recordingStopRecovery.batch ? recordingStopRecovery.presentation : await buildToolPresentation({ args: prepared.redactedProcessArgs, artifactManifest, artifactMaxUpdatedAtMs: Date.now(), artifactMinUpdatedAtMs: input.artifactRunStartedAtMs, artifactRequest: screenshotArtifactRequest, batchArtifactRequests: batchScreenshotArtifactRequests, commandInfo: prepared.executionPlan.commandInfo, compiledSemanticAction: prepared.compiledSemanticAction, cwd, envelope: presentationEnvelope, errorText, namespace: prepared.executionPlan.namespace, networkRouteDiagnostics, networkRoutes: activeNetworkRoutes, persistentArtifactStore, piCleanupOwnership: sessionStateKey && (state.ownedManagedSessions.has(sessionStateKey) || prepared.executionPlan.managedSessionName !== undefined) ? "wrapper-managed" : "caller-owned", sessionName: prepared.executionPlan.sessionName });
674
+ if (recordingStopRecovery)
675
+ presentation = mergeRecordingRecoveryPresentation(presentation, recordingStopRecovery);
676
+ const confirmation = readConfirmationEvent ?? prepared.readConfirmation;
677
+ if (confirmation) {
678
+ presentation.readConfirmation = confirmation;
679
+ if (confirmation.state !== "cleared" || !detectConfirmationRequired(presentationEnvelope?.data)) {
680
+ presentation.nextActions = buildReadConfirmationNextActions(confirmation, readConfirmationEvent?.state === "pending");
681
+ }
682
+ if (readConfirmationEvent?.state === "pending") {
683
+ presentation.resultCategory = "failure";
684
+ presentation.failureCategory = "confirmation-required";
685
+ presentation.successCategory = undefined;
686
+ }
687
+ }
645
688
  if (plainTextUpgrade && !succeeded && typeof presentationEnvelope?.data === "string") {
646
689
  presentation.data = presentationEnvelope.data;
647
690
  const errorContent = presentation.content[0];
@@ -751,6 +794,8 @@ export async function processBrowserOutput(input) {
751
794
  const evalPageUrl = evalNavigationSummary?.url ?? currentSessionTabTarget?.url ?? prepared.priorSessionTabTarget?.url ?? evalSessionTabUrl;
752
795
  const evalStdinHint = getEvalStdinHint({ command: prepared.executionPlan.commandInfo.command, data: presentationEnvelope?.data, stdin: prepared.runtimeToolStdin });
753
796
  const evalResultWarning = getEvalResultWarning({ command: prepared.executionPlan.commandInfo.command, data: presentationEnvelope?.data, navigationSummary: evalNavigationSummary, pageUrl: evalPageUrl, stdin: prepared.runtimeToolStdin });
797
+ if (readConfirmationEvent)
798
+ state.sessionPageState.applyReadConfirmation(readConfirmationEvent, sessionPageStateUpdate);
754
799
  const resultArtifactManifest = presentation.artifactManifest ?? artifactManifest;
755
800
  const artifactCleanup = await getArtifactCleanupGuidance({ command: prepared.executionPlan.commandInfo.command, cwd, manifest: resultArtifactManifest, succeeded });
756
801
  const recordingTransitionReached = prepared.executionPlan.commandInfo.command === "batch"
@@ -772,9 +817,10 @@ export async function processBrowserOutput(input) {
772
817
  const resultRetainsPreparedManagedSession = !managedSessionOutcome || (managedSessionOutcome.activeAfter
773
818
  && managedSessionOutcome.attemptedSessionName === managedSessionOutcome.currentSessionName);
774
819
  const resultHeadedManagedAutosaveDisabled = prepared.ownedManagedSessionContext?.headedManagedAutosaveDisabled === true
820
+ && !prepared.ownedManagedSessionContext.reuseOnly
775
821
  && resultRetainsPreparedManagedSession
776
822
  && !(commandClosesSession && succeeded);
777
- const resultHeadedManagedAutosaveInterval = resultRetainsPreparedManagedSession && !(commandClosesSession && succeeded)
823
+ const resultHeadedManagedAutosaveInterval = resultRetainsPreparedManagedSession && !prepared.ownedManagedSessionContext?.reuseOnly && !(commandClosesSession && succeeded)
778
824
  ? prepared.ownedManagedSessionContext?.headedManagedAutosaveInterval
779
825
  : undefined;
780
826
  const result = buildFinalAgentBrowserToolResult({ aboutBlankSessionMismatch, artifactCleanup, categoryDetails: finalRecoveryState.categoryDetails, clickDispatchDiagnostic, commandTokens: prepared.commandTokens, comboboxFocusDiagnostic, compiledNetworkSourceLookup: prepared.compiledNetworkSourceLookup, compiledSemanticAction: prepared.compiledSemanticAction, compatibilityWorkaround: prepared.compatibilityWorkaround, currentRefSnapshot, currentRefSnapshotInvalidation, currentSessionTabTarget, currentSessionTabTargetUnknown, electronBroadGetTextScopeDiagnostics, electronFailedConnectCleanup, electronHandoff, electronLaunch: prepared.electronLaunch, electronLaunchRecord, electronLaunchRecords, electronPostCommandHealth, electronProfileIsolationDetails: input.electronProfileIsolationDetails, electronRefFreshnessDiagnostic, electronSessionMismatch, errorText, evalResultWarning, evalStdinHint, exactSensitiveValues: prepared.exactSensitiveValues, executionPlan: prepared.executionPlan, fillVerificationDiagnostic, headedLaunch: prepared.headedLaunch, inspectionText, preserveAttachedBrowserSession: input.preserveAttachedBrowserSession === true, providerLaunch: prepared.providerLaunch, managedSessionHeadedAutosaveDisabled: resultHeadedManagedAutosaveDisabled || undefined, managedSessionHeadedAutosaveInterval: resultHeadedManagedAutosaveInterval, managedSessionOutcome, managedSessionRestoreDisabled: state.managedSessionRestoreState.isDisabled(prepared.executionPlan.sessionName, prepared.executionPlan.namespace), navigationSummary, networkSourceLookup, noActivePageSnapshotFailure: finalRecoveryState.noActivePageSnapshotFailure, openResultTabCorrection, overlayBlockerDiagnostic, parseError, parseFailureOutput, parseSucceeded, plainTextInspection, presentation, presentationEnvelope, priorSessionTabTarget: prepared.priorSessionTabTarget, processResult, qaAttachedTarget, qaPreset, recordingDependencyWarning, redactedArgs: prepared.redactedArgs, redactedCompiledElectron: prepared.redactedCompiledElectron, redactedCompiledJob: prepared.redactedCompiledJob, redactedCompiledNetworkSourceLookup: prepared.redactedCompiledNetworkSourceLookup, redactedCompiledQaPreset: prepared.redactedCompiledQaPreset, redactedCompiledSemanticAction: prepared.redactedCompiledSemanticAction, redactedCompiledSourceLookup: prepared.redactedCompiledSourceLookup, redactedContent, redactedProcessArgs: prepared.redactedProcessArgs, redactedRecoveryHint: prepared.redactedRecoveryHint, resultArtifactManifest, richInputRecoveryDiagnostic: finalRecoveryState.richInputRecoveryDiagnostic, scrollNoopDiagnostic, selectorTextVisibilityDiagnostics, sessionMode: prepared.sessionMode, sessionTabCorrection, sourceLookup, succeeded, timeoutPartialProgress, unsettledWebMcpMutation, userRequestedJson: prepared.userRequestedJson, visibleRefFallbackDiagnostic: finalRecoveryState.visibleRefFallbackDiagnostic, visibleRefFallbackSessionName: finalRecoveryState.visibleRefFallbackSessionName });
@@ -0,0 +1,161 @@
1
+ import { resolve } from "node:path";
2
+ import { getAgentBrowserSessionIdentityKey } from "../../argv-grammar.js";
3
+ import { getRecordCommandOperands } from "../../command-taxonomy.js";
4
+ import { isRecord } from "../../parsing.js";
5
+ import { isPendingRecordingArtifact, mergeSessionArtifactManifest } from "../../results/artifact-manifest.js";
6
+ import { extractEnvelopeErrorText } from "../../results/envelope.js";
7
+ import { appendUniqueAgentBrowserNextActions } from "../../results/next-actions.js";
8
+ import { buildToolPresentation } from "../../results/presentation.js";
9
+ import { buildArtifactVerificationSummary, buildManifestEntriesForFileArtifacts } from "../../results/presentation/artifacts.js";
10
+ import { getRecordingReceipt } from "../../results/recording.js";
11
+ import { getUpstreamEffectiveBatchSteps } from "../batch-stdin.js";
12
+ import { runSessionCommandData } from "./session-state.js";
13
+ function isStop(tokens) {
14
+ return tokens[0] === "record" && tokens[1] === "stop";
15
+ }
16
+ function receiptMatches(receipt, expected, nowMs) {
17
+ if (resolve(expected.cwd, receipt.path) !== expected.absolutePath)
18
+ return false;
19
+ if (expected.recordingId)
20
+ return receipt.recordingId === expected.recordingId;
21
+ const startedAtMs = Date.parse(receipt.capture.startedAt ?? "");
22
+ return expected.startedAtMs !== undefined && startedAtMs >= expected.startedAtMs && startedAtMs <= nowMs;
23
+ }
24
+ export async function recoverRecordingStop(options) {
25
+ const batch = options.commandTokens[0] === "batch";
26
+ const steps = batch ? getUpstreamEffectiveBatchSteps(options.commandTokens, options.stdin) : [options.commandTokens];
27
+ const rows = batch && Array.isArray(options.envelope?.data) ? options.envelope.data : undefined;
28
+ const stopIndex = steps.reduce((last, step, index) => isStop(step) && (options.processResult.timedOut
29
+ || /no recording in progress/i.test(extractEnvelopeErrorText(rows?.[index]?.error ?? options.envelope?.error) ?? "")) ? index : last, -1);
30
+ if (stopIndex < 0 || options.processResult.aborted || options.signal?.aborted || !options.processResult.agentBrowserStarted)
31
+ return undefined;
32
+ let expected = options.reservation;
33
+ for (let index = 0; index < stopIndex; index += 1) {
34
+ const step = steps[index];
35
+ if (step[0] !== "record" || !["start", "restart"].includes(step[1]))
36
+ continue;
37
+ const row = rows?.[index];
38
+ if (isRecord(row) && row.success === false)
39
+ continue;
40
+ const started = isRecord(row) && isRecord(row.result) ? row.result : undefined;
41
+ const path = typeof started?.path === "string" ? started.path : getRecordCommandOperands(step).path;
42
+ if (path && options.sessionName)
43
+ expected = {
44
+ absolutePath: resolve(options.cwd, path), cwd: options.cwd, path, namespace: options.namespace, sessionName: options.sessionName,
45
+ recordingId: typeof started?.recordingId === "string" ? started.recordingId : undefined, startedAtMs: options.artifactRunStartedAtMs,
46
+ };
47
+ }
48
+ const attemptError = options.processResult.timedOut
49
+ ? `Record stop timed out after ${options.processResult.timeoutMs} ms.`
50
+ : extractEnvelopeErrorText(rows?.[stopIndex]?.error ?? options.envelope?.error) ?? "Record stop did not return a receipt.";
51
+ const recovery = {
52
+ attempt: { success: false, exitCode: options.processResult.exitCode, timedOut: options.processResult.timedOut, error: attemptError, parseError: options.parseError },
53
+ expected, healed: false, namespace: options.namespace, sessionName: options.sessionName, source: "session-info", status: "unavailable",
54
+ reason: "Native session info did not provide a matching recording receipt. File presence alone cannot prove successful recording finalization.",
55
+ };
56
+ let info;
57
+ try {
58
+ info = await runSessionCommandData({ args: ["session", "info"], cwd: options.cwd, namespace: options.namespace, pinNamespace: true, sessionName: options.sessionName, signal: options.signal, timeoutMs: 2_000 });
59
+ }
60
+ catch (error) {
61
+ recovery.reason = `Native session info was unavailable: ${error instanceof Error ? error.message : String(error)}. File presence alone cannot prove successful recording finalization.`;
62
+ }
63
+ const runtime = isRecord(info) && isRecord(info.runtime) ? info.runtime : undefined;
64
+ const recordings = isRecord(runtime?.recording) ? runtime.recording : undefined;
65
+ const sameSession = isRecord(info) && typeof info.session === "string" && options.sessionName !== undefined
66
+ && getAgentBrowserSessionIdentityKey(info.session, typeof info.namespace === "string" ? info.namespace : undefined)
67
+ === getAgentBrowserSessionIdentityKey(options.sessionName, options.namespace);
68
+ const current = getRecordingReceipt(recordings?.current);
69
+ let matchedData;
70
+ if (recordings && sameSession && expected) {
71
+ for (const candidate of [recordings.current, recordings.last]) {
72
+ const receipt = getRecordingReceipt(candidate);
73
+ if (!receipt || !receiptMatches(receipt, expected, Date.now()))
74
+ continue;
75
+ recovery.receipt = receipt;
76
+ matchedData = candidate;
77
+ break;
78
+ }
79
+ if (!recovery.receipt) {
80
+ recovery.status = "mismatch";
81
+ recovery.reason = "Native recording IDs, paths or capture windows did not match this attempt; unrelated receipt measurements were not used.";
82
+ }
83
+ }
84
+ else if (recordings && !sameSession) {
85
+ recovery.status = "mismatch";
86
+ recovery.reason = "Native session info reported a different namespace/session; its recording receipt was not used.";
87
+ }
88
+ const receipt = recovery.receipt;
89
+ const currentUsesSamePath = current && receipt && current.recordingId !== receipt.recordingId && expected
90
+ && resolve(expected.cwd, current.path) === expected.absolutePath;
91
+ const terminalMeasurements = receipt?.success === true && receipt.output.encoderSucceeded === true
92
+ && receipt.output.encodedFrames !== null && receipt.output.encodedFrames > 0
93
+ && receipt.file.exists === true && receipt.file.sizeBytes !== null && receipt.file.sizeBytes > 0
94
+ && Number.isFinite(Date.parse(receipt.capture.startedAt ?? "")) && Number.isFinite(Date.parse(receipt.capture.endedAt ?? ""))
95
+ && !currentUsesSamePath;
96
+ const data = matchedData ?? (expected ? { path: expected.absolutePath, recordingId: expected.recordingId, success: null } : undefined);
97
+ const presentation = await buildToolPresentation({
98
+ artifactManifest: options.artifactManifest, artifactMinUpdatedAtMs: expected?.startedAtMs ?? options.artifactRunStartedAtMs, artifactMaxUpdatedAtMs: Date.now(),
99
+ artifactRequest: expected ? { path: expected.path, absolutePath: expected.absolutePath, status: terminalMeasurements ? undefined : "unverified" } : undefined,
100
+ commandInfo: { command: "record", subcommand: "stop" }, cwd: expected?.cwd ?? options.cwd,
101
+ envelope: { success: receipt?.success === true, data }, namespace: options.namespace, sessionName: options.sessionName,
102
+ recordingPending: receipt ? receipt.success === null : options.processResult.timedOut,
103
+ });
104
+ const file = presentation.artifacts?.[0];
105
+ const stopVerified = terminalMeasurements && presentation.artifactVerification?.verified === true && file?.sizeBytes === receipt?.file.sizeBytes;
106
+ if (receipt) {
107
+ recovery.status = stopVerified ? "recovered" : receipt.success === null ? "pending" : receipt.success === false ? "failed" : "unverified";
108
+ recovery.reason = stopVerified ? "Native terminal success and encoder measurements match the expected recording and the verified file."
109
+ : receipt.success === null ? "The matching native take is still pending; no terminal success was reported."
110
+ : receipt.success === false ? `The matching native receipt reports failure: ${receipt.error ?? "unknown encoder/capture failure"}.`
111
+ : currentUsesSamePath ? "Another active recording uses this path. The previous receipt cannot verify the new take's file."
112
+ : "A matching receipt was found, but successful encoding and a matching fresh file could not both be verified.";
113
+ }
114
+ let envelope = options.envelope;
115
+ if (rows) {
116
+ const updatedRows = rows.map((row, index) => index === stopIndex && isRecord(row) ? { ...row, result: data, success: stopVerified === true, error: stopVerified ? undefined : row.error } : row);
117
+ recovery.healed = stopVerified === true && !options.processResult.timedOut && updatedRows.every(row => isRecord(row) && row.success === true);
118
+ envelope = { success: recovery.healed, data: updatedRows, error: recovery.healed ? undefined : options.envelope?.error };
119
+ }
120
+ else if (!batch) {
121
+ recovery.healed = stopVerified === true;
122
+ envelope = { success: recovery.healed, data, error: recovery.healed ? undefined : attemptError };
123
+ }
124
+ if (!recovery.healed) {
125
+ presentation.resultCategory = "failure";
126
+ presentation.failureCategory = options.processResult.timedOut ? "timeout" : "upstream-error";
127
+ presentation.successCategory = undefined;
128
+ presentation.summary = `${attemptError} ${recovery.reason}`;
129
+ }
130
+ const followups = options.sessionName && !recovery.healed ? [{
131
+ id: "inspect-recording-receipt", tool: "agent_browser", params: { args: ["--namespace", options.namespace ?? "", "--session", options.sessionName, "session", "info"] },
132
+ reason: "Inspect the native current/last recording receipts for this exact session.", safety: "Read-only status; does not launch or retarget the browser. Do not infer encoding success from an existing file.",
133
+ }] : [];
134
+ if (current && receipt && current.recordingId === receipt.recordingId && recovery.status === "pending" && options.sessionName)
135
+ followups.push({
136
+ id: "stop-pending-recording", tool: "agent_browser", params: { args: ["--namespace", options.namespace ?? "", "--session", options.sessionName, "record", "stop"] },
137
+ reason: `Finalize the still-current native recording ${receipt.recordingId}; it has not reported a terminal outcome.`, safety: "Run only while this same recording remains current. No repeated stop was dispatched during recovery.",
138
+ });
139
+ presentation.nextActions = followups;
140
+ presentation.recordingRecovery = recovery;
141
+ const text = `Recording receipt recovery (${recovery.status}): ${recovery.reason}\nOriginal attempt: ${attemptError}`;
142
+ presentation.content.unshift({ type: "text", text });
143
+ return { batch, envelope, partialBatch: batch && !rows, stopIndex, presentation, recovery };
144
+ }
145
+ export function mergeRecordingRecoveryPresentation(base, result) {
146
+ if (!result.batch)
147
+ return result.presentation;
148
+ const artifacts = result.partialBatch ? [...(base.artifacts ?? []), ...(result.presentation.artifacts ?? [])] : base.artifacts;
149
+ const laterRecording = base.batchSteps?.slice(result.stopIndex + 1).some(step => step.artifacts?.some(isPendingRecordingArtifact));
150
+ const unrelatedFailure = base.batchFailure && !isStop(base.batchFailure.failedStep.command ?? []);
151
+ const nextActions = (base.nextActions ?? []).filter(action => action.id === "stop-pending-recording" ? laterRecording : unrelatedFailure);
152
+ appendUniqueAgentBrowserNextActions(nextActions, result.presentation.nextActions);
153
+ return {
154
+ ...base, artifacts, artifactVerification: buildArtifactVerificationSummary(artifacts ?? []),
155
+ batchSteps: base.batchSteps?.map((step) => step.index === result.stopIndex ? { ...step, artifacts: result.presentation.artifacts, artifactVerification: result.presentation.artifactVerification } : step),
156
+ artifactManifest: result.partialBatch ? mergeSessionArtifactManifest({ base: base.artifactManifest, entries: buildManifestEntriesForFileArtifacts(result.presentation.artifacts ?? []) }) : base.artifactManifest,
157
+ content: [...base.content, ...(result.partialBatch ? result.presentation.content : result.presentation.content.slice(0, 1))],
158
+ nextActions,
159
+ recordingRecovery: result.recovery,
160
+ };
161
+ }
@@ -1,7 +1,7 @@
1
1
  import { rm } from "node:fs/promises";
2
2
  import { getAgentBrowserSessionIdentityKey } from "../../argv-grammar.js";
3
3
  import { parseArgvDescriptor } from "../../argv-descriptor.js";
4
- import { needsManagedSession } from "../../command-policy.js";
4
+ import { isBrowserIndependentRead, needsManagedSession } from "../../command-policy.js";
5
5
  import { runAgentBrowserProcess } from "../../process.js";
6
6
  import { buildAgentBrowserNextActions } from "../../results/action-recommendations.js";
7
7
  import { parseAgentBrowserEnvelope } from "../../results/envelope.js";
@@ -427,7 +427,7 @@ export function commandChoosesSessionTabTarget(args) {
427
427
  || getRecordCommandOperands(tokens).url !== undefined;
428
428
  }
429
429
  export function shouldPinSessionTabForCommand(options) {
430
- if (!options.pinningRequired || !options.sessionName || !options.command)
430
+ if (!options.pinningRequired || !options.sessionName || !options.command || isBrowserIndependentRead(options.commandTokens, options.stdin))
431
431
  return false;
432
432
  const steps = options.command === "batch" ? getUpstreamEffectiveBatchSteps(options.commandTokens, options.stdin) : [options.commandTokens];
433
433
  for (const step of steps) {
@@ -441,7 +441,7 @@ export function shouldPinSessionTabForCommand(options) {
441
441
  continue;
442
442
  if (command === "get" && subcommand === "url" && !options.reopenPending)
443
443
  continue;
444
- if (command === "read" && findFirstPositionalArgument(tokens) !== undefined)
444
+ if (command === "record" && subcommand === "stop")
445
445
  continue;
446
446
  if (["console", "errors"].includes(command))
447
447
  continue;
@@ -65,7 +65,7 @@ function normalizeExplicitEvalStdinArgs(args, stdin) {
65
65
  };
66
66
  }
67
67
  export function resolveAgentBrowserInput(options) {
68
- const { getBatchPreflightValidationError, managedSessionActive, params } = options;
68
+ const { getBatchPreflightValidationError, params } = options;
69
69
  const semanticActionResult = params.semanticAction === undefined ? {} : compileAgentBrowserSemanticAction(params.semanticAction);
70
70
  const jobResult = params.job === undefined ? {} : compileAgentBrowserJob(params.job);
71
71
  const qaResult = params.qa === undefined ? {} : compileAgentBrowserQaPreset(params.qa);
@@ -126,9 +126,7 @@ export function resolveAgentBrowserInput(options) {
126
126
  const attachedQaSessionError = compiledQaPreset?.checks.attached
127
127
  ? params.sessionMode === "fresh"
128
128
  ? "qa.attached cannot be used with sessionMode=fresh; attach or launch a session first, then run qa.attached with the current session."
129
- : !managedSessionActive
130
- ? "qa.attached requires an active attached session. Run electron.launch or connect to an Electron debug port first."
131
- : undefined
129
+ : undefined
132
130
  : undefined;
133
131
  const validationError = semanticActionResult.error
134
132
  ?? jobResult.error
@@ -0,0 +1,68 @@
1
+ import { readFile, rm } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { join, resolve } from "node:path";
4
+ import { extractExplicitSessionName, scanUpstreamGlobalFlagOccurrences } from "../argv-grammar.js";
5
+ import { isRecord } from "../parsing.js";
6
+ import { getAgentBrowserProcessEnvironment, withAgentBrowserProcessEnvironment } from "../process-environment.js";
7
+ import { runAgentBrowserProcess } from "../process.js";
8
+ import { parseAgentBrowserEnvelope } from "../results/envelope.js";
9
+ import { isPlainTextInspectionArgs } from "../runtime.js";
10
+ // Native `session` reports the resolved name, but not whether it was configured.
11
+ // Read only identity presence/namespace; native validates the rest of its schema.
12
+ async function readNativeIdentity(path, cwd, signal) {
13
+ let config;
14
+ try {
15
+ config = JSON.parse(await readFile(path, "utf8"));
16
+ }
17
+ catch {
18
+ return {};
19
+ }
20
+ if (!isRecord(config) || (typeof config.session !== "string" && typeof config.namespace !== "string"))
21
+ return {};
22
+ const result = await runAgentBrowserProcess({ args: ["--config", path, "--json", "session"], cwd, signal, timeoutMs: 5_000 });
23
+ try {
24
+ if (result.aborted || result.timedOut || result.spawnError)
25
+ throw new Error("Could not resolve native agent-browser session configuration; the browser command was not run.");
26
+ if (result.exitCode !== 0)
27
+ return {}; // Native ignores invalid discovered files; explicit files fail again in the actual command.
28
+ const parsed = await parseAgentBrowserEnvelope({ stdout: result.stdout, stdoutPath: result.stdoutSpillPath });
29
+ const data = parsed.envelope?.data;
30
+ if (!isRecord(data) || typeof data.session !== "string")
31
+ throw new Error("Native agent-browser session inspection returned no session name; the browser command was not run.");
32
+ return {
33
+ ...(typeof config.session === "string" ? { session: data.session } : {}),
34
+ ...(typeof config.namespace === "string" ? { namespace: config.namespace } : {}),
35
+ };
36
+ }
37
+ finally {
38
+ if (result.stdoutSpillPath)
39
+ await rm(result.stdoutSpillPath, { force: true });
40
+ }
41
+ }
42
+ export async function withNativeSessionDefaults(input, cwd, signal, run) {
43
+ if (input.kind === "electron" && input.compiledElectron.action === "launch")
44
+ return withAgentBrowserProcessEnvironment({ AGENT_BROWSER_SESSION: undefined }, () => run(input));
45
+ if (input.kind === "script" || input.kind === "electron" || isPlainTextInspectionArgs(input.toolArgs))
46
+ return run(input);
47
+ const env = getAgentBrowserProcessEnvironment();
48
+ const configArg = scanUpstreamGlobalFlagOccurrences(input.toolArgs, "--config")[0];
49
+ const configPath = configArg?.value ?? env.AGENT_BROWSER_CONFIG;
50
+ const paths = configPath !== undefined
51
+ ? [resolve(cwd, configPath)]
52
+ : [join(homedir(), ".agent-browser", "config.json"), join(cwd, "agent-browser.json")];
53
+ let identity = {};
54
+ for (const path of paths)
55
+ identity = { ...identity, ...await readNativeIdentity(path, cwd, signal) };
56
+ const session = env.AGENT_BROWSER_SESSION ?? identity.session;
57
+ const namespace = env.AGENT_BROWSER_NAMESPACE ?? identity.namespace;
58
+ let args = input.toolArgs;
59
+ if (session !== undefined && extractExplicitSessionName(args) === undefined)
60
+ args = ["--session", session, ...args];
61
+ const idleTimeout = scanUpstreamGlobalFlagOccurrences(args, "--idle-timeout").at(-1)?.value;
62
+ return withAgentBrowserProcessEnvironment({
63
+ ...(idleTimeout !== undefined ? { AGENT_BROWSER_IDLE_TIMEOUT_MS: idleTimeout } : {}),
64
+ ...(configPath !== undefined ? { AGENT_BROWSER_CONFIG: resolve(cwd, configPath) } : {}),
65
+ ...(session !== undefined ? { AGENT_BROWSER_SESSION: session } : {}),
66
+ ...(namespace !== undefined ? { AGENT_BROWSER_NAMESPACE: namespace } : {}),
67
+ }, () => run({ ...input, toolArgs: args }));
68
+ }
@@ -2,6 +2,7 @@ import { mkdir, readFile, realpath, stat, writeFile } from "node:fs/promises";
2
2
  import { dirname, extname, isAbsolute, resolve } from "node:path";
3
3
  import { isRecord } from "../parsing.js";
4
4
  import { isSessionArtifactManifest } from "../results/artifact-manifest.js";
5
+ import { parseCommandInfo, redactSensitiveValue } from "../runtime.js";
5
6
  export function normalizeRequestedOutputPath(path) {
6
7
  return path.startsWith("@") ? path.slice(1) : path;
7
8
  }
@@ -11,7 +12,21 @@ function getTextContent(result) {
11
12
  .map((item) => item.text)
12
13
  .join("\n\n") ?? "";
13
14
  }
15
+ function getResultCommand(details) {
16
+ const args = Array.isArray(details?.args) && details.args.every(value => typeof value === "string") ? details.args : [];
17
+ return parseCommandInfo(args);
18
+ }
19
+ function isRecordingReceiptResult(result) {
20
+ const details = isRecord(result.details) ? result.details : undefined;
21
+ return details?.command === "record" || getResultCommand(details).command === "record" || details?.recordingRecovery !== undefined
22
+ || (Array.isArray(details?.batchSteps) && details.batchSteps.some((step) => isRecord(step) && Array.isArray(step.command) && step.command[0] === "record"));
23
+ }
24
+ export function canWriteAgentBrowserOutput(result) {
25
+ return isRecordingReceiptResult(result) || (!result.isError && !(isRecord(result.details) && result.details.resultCategory === "failure"));
26
+ }
14
27
  function getOutputSource(result) {
28
+ if (isRecordingReceiptResult(result))
29
+ return "recording-receipt";
15
30
  return isRecord(result.details) && result.details.data !== undefined ? "details.data" : "content.text";
16
31
  }
17
32
  async function readCompactedSpill(path, manifest) {
@@ -42,17 +57,37 @@ async function rehydrateCompactedData(data, details, manifest) {
42
57
  }
43
58
  async function getOutputPayload(result) {
44
59
  const details = isRecord(result.details) ? result.details : undefined;
45
- if (details?.data === undefined)
60
+ if (!details)
46
61
  return { source: "content.text", value: getTextContent(result) };
47
62
  const manifest = isSessionArtifactManifest(details.artifactManifest) ? details.artifactManifest : undefined;
48
- return { source: "details.data", value: await rehydrateCompactedData(details.data, details, manifest) };
63
+ const data = await rehydrateCompactedData(details.data, details, manifest);
64
+ if (isRecordingReceiptResult(result)) {
65
+ const success = !result.isError && details.resultCategory !== "failure";
66
+ const recovery = isRecord(details.recordingRecovery) ? details.recordingRecovery : undefined;
67
+ const commandInfo = getResultCommand(details);
68
+ return { source: "recording-receipt", value: redactSensitiveValue({
69
+ success, error: details.error ?? (success ? null : details.summary ?? getTextContent(result)),
70
+ command: details.command ?? commandInfo.command, subcommand: details.subcommand ?? commandInfo.subcommand, sessionName: details.sessionName, namespace: details.namespace,
71
+ attempt: recovery?.attempt ?? { success, agentBrowserStarted: details.agentBrowserStarted ?? null, exitCode: details.exitCode ?? null, timedOut: details.timedOut === true, error: details.error ?? details.validationError ?? null, parseError: details.parseError ?? null },
72
+ data: data ?? null, artifacts: details.artifacts, artifactVerification: details.artifactVerification, recordingRecovery: recovery,
73
+ }) };
74
+ }
75
+ return data === undefined ? { source: "content.text", value: getTextContent(result) } : { source: "details.data", value: data };
49
76
  }
50
77
  function serializeOutputPayload(value) {
51
78
  return typeof value === "string" ? value : `${JSON.stringify(value, null, 2)}\n`;
52
79
  }
53
- function appendOutputFileNotice(result, message) {
80
+ function appendOutputFileNotice(result, message, failed = false) {
54
81
  const content = [...(result.content ?? [])];
55
82
  if (content[0]?.type === "text") {
83
+ try {
84
+ const json = JSON.parse(content[0].text);
85
+ if (isRecord(json) && typeof json.success === "boolean") {
86
+ content[0] = { type: "text", text: JSON.stringify({ ...json, ...(failed ? { success: false, error: message } : {}), outputFileNotice: message }, null, 2) };
87
+ return content;
88
+ }
89
+ }
90
+ catch { }
56
91
  content[0] = { ...content[0], text: `${content[0].text}\n\n${message}` };
57
92
  return content;
58
93
  }
@@ -88,7 +123,7 @@ async function pathsReferToSameFile(left, right) {
88
123
  export async function applyAgentBrowserOutputPath(options) {
89
124
  if (!options.outputPath)
90
125
  return options.result;
91
- if (options.result.isError || (isRecord(options.result.details) && options.result.details.resultCategory === "failure"))
126
+ if (!canWriteAgentBrowserOutput(options.result))
92
127
  return options.result;
93
128
  const requestedPath = normalizeRequestedOutputPath(options.outputPath);
94
129
  const absolutePath = isAbsolute(requestedPath) ? requestedPath : resolve(options.cwd, requestedPath);
@@ -102,7 +137,7 @@ export async function applyAgentBrowserOutputPath(options) {
102
137
  delete details.successCategory;
103
138
  return {
104
139
  ...options.result,
105
- content: appendOutputFileNotice(options.result, `Output file rejected: ${message}`),
140
+ content: appendOutputFileNotice(options.result, `Output file rejected: ${message}`, true),
106
141
  details: { ...details, failureCategory: "validation-error", outputFile, resultCategory: "failure" },
107
142
  isError: true,
108
143
  };
@@ -133,7 +168,7 @@ export async function applyAgentBrowserOutputPath(options) {
133
168
  : { failureCategory: "upstream-error", outputFile, resultCategory: "failure" };
134
169
  return {
135
170
  ...options.result,
136
- content: options.preserveTextContent ? options.result.content : appendOutputFileNotice(options.result, `Output file failed: ${requestedPath} (${message}).`),
171
+ content: appendOutputFileNotice(options.result, `Output file failed: ${requestedPath} (${message}).`, true),
137
172
  details,
138
173
  isError: true,
139
174
  };
@@ -1,6 +1,6 @@
1
1
  import { parseArgvDescriptor } from "./argv-descriptor.js";
2
2
  import { GLOBAL_BOOLEAN_FLAGS_WITH_OPTIONAL_VALUES, VALUE_FLAGS, } from "./argv-grammar.js";
3
- import { needsManagedSession } from "./command-policy.js";
3
+ import { isBrowserIndependentRead, needsManagedSession } from "./command-policy.js";
4
4
  import { isUnverifiedPageTransitionCommand } from "./command-taxonomy.js";
5
5
  import { parseBatchCommandArgument, parseUserBatchStdin } from "./orchestration/batch-stdin.js";
6
6
  const UNVERIFIED_PAGE_MESSAGE = "The active page became unverified after a tab, attachment, history, script, or state-load transition. Run get url or navigate explicitly before page-content inspection.";
@@ -167,23 +167,27 @@ function isRecoveringPageTransitionCommand(command, subcommand) {
167
167
  && isUnverifiedPageTransitionCommand(command, subcommand);
168
168
  }
169
169
  function getUnverifiedPageError(options) {
170
- if (!options.pageUrlUnknown)
170
+ const descriptor = parseArgvDescriptor(options.args);
171
+ if (!options.pageUrlUnknown || !needsManagedSession(descriptor))
171
172
  return undefined;
172
- const { command, subcommand } = parseArgvDescriptor(options.args).commandInfo;
173
+ const { command, subcommand } = descriptor.commandInfo;
173
174
  const closesPage = ["close", "exit", "quit"].includes(command ?? "") || (command === "tab" && subcommand === "close");
174
175
  const inspectsTarget = (command === "tab" && subcommand === "list") || (command === "get" && subcommand === "url");
175
176
  const selectsTab = command === "tab" && subcommand !== undefined && !["close", "list", "new"].includes(subcommand);
176
177
  const settlesPendingWebMcp = command === "webmcp" && ["result", "cancel"].includes(subcommand ?? "");
178
+ const stopsRecording = command === "record" && subcommand === "stop";
177
179
  const handlesBlockingDialog = command === "dialog" && ["status", "accept", "dismiss"].includes(subcommand ?? "");
178
180
  const transitionsPage = options.allowUnverifiedPageTransitions === true && isRecoveringPageTransitionCommand(command, subcommand);
179
181
  const navigatesExplicitly = getExplicitNavigationTarget(options.args) !== undefined;
180
- return closesPage || handlesBlockingDialog || inspectsTarget || selectsTab || settlesPendingWebMcp || transitionsPage || navigatesExplicitly || (options.trustedBatchTabSelection && command === "tab")
182
+ return closesPage || handlesBlockingDialog || inspectsTarget || selectsTab || settlesPendingWebMcp || stopsRecording || transitionsPage || navigatesExplicitly || (options.trustedBatchTabSelection && command === "tab")
181
183
  ? undefined
182
184
  : UNVERIFIED_PAGE_MESSAGE;
183
185
  }
184
186
  export function getPageTargetValidationError(options) {
185
187
  const descriptor = parseArgvDescriptor(options.args);
186
188
  const command = descriptor.commandInfo.command;
189
+ if (isBrowserIndependentRead(descriptor.upstreamCommandTokens, options.stdin))
190
+ return undefined;
187
191
  if (["close", "exit", "quit"].includes(command ?? ""))
188
192
  return undefined;
189
193
  if (command === "batch") {
@@ -254,7 +258,7 @@ export function getPageTargetValidationError(options) {
254
258
  }
255
259
  export function getExplicitSessionPageVerificationRequirement(options) {
256
260
  const descriptor = parseArgvDescriptor(options.args);
257
- if (!needsManagedSession(descriptor))
261
+ if (!needsManagedSession(descriptor, options.stdin))
258
262
  return undefined;
259
263
  if (isRecoveringPageTransitionCommand(descriptor.commandInfo.command, descriptor.commandInfo.subcommand))
260
264
  return undefined;