pi-agent-browser-native 0.6.9 → 0.6.11
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.
- package/CHANGELOG.md +48 -0
- package/README.md +62 -19
- package/dist/extensions/agent-browser/index.js +424 -451
- package/dist/extensions/agent-browser/lib/argv-descriptor.js +6 -7
- package/dist/extensions/agent-browser/lib/argv-grammar.js +7 -1
- package/dist/extensions/agent-browser/lib/batch-lifecycle.js +4 -8
- package/dist/extensions/agent-browser/lib/command-policy.js +41 -2
- package/dist/extensions/agent-browser/lib/command-taxonomy.js +15 -2
- package/dist/extensions/agent-browser/lib/input-modes/params.js +1 -1
- package/dist/extensions/agent-browser/lib/input-modes/script.js +3 -2
- package/dist/extensions/agent-browser/lib/managed-session-restore.js +42 -12
- package/dist/extensions/agent-browser/lib/managed-session-snapshots.js +3 -5
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.js +6 -14
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +14 -25
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +12 -6
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +1 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/wait-timeouts.js +3 -2
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +38 -31
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +60 -20
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/recording-recovery.js +161 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +5 -5
- package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +2 -4
- package/dist/extensions/agent-browser/lib/orchestration/native-session-defaults.js +68 -0
- package/dist/extensions/agent-browser/lib/orchestration/output-file.js +41 -6
- package/dist/extensions/agent-browser/lib/page-target-validation.js +9 -5
- package/dist/extensions/agent-browser/lib/playbook.js +13 -12
- package/dist/extensions/agent-browser/lib/process-environment.js +26 -8
- package/dist/extensions/agent-browser/lib/process.js +8 -5
- package/dist/extensions/agent-browser/lib/read-confirmation.js +59 -0
- package/dist/extensions/agent-browser/lib/recording-reservations.js +11 -1
- package/dist/extensions/agent-browser/lib/results/action-recommendations.js +8 -0
- package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +6 -5
- package/dist/extensions/agent-browser/lib/results/categories.js +4 -2
- package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +76 -57
- package/dist/extensions/agent-browser/lib/results/presentation/batch.js +19 -8
- package/dist/extensions/agent-browser/lib/results/presentation/common.js +5 -25
- package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +40 -38
- package/dist/extensions/agent-browser/lib/results/presentation/errors.js +1 -0
- package/dist/extensions/agent-browser/lib/results/presentation/navigation.js +3 -3
- package/dist/extensions/agent-browser/lib/results/presentation.js +38 -9
- package/dist/extensions/agent-browser/lib/results/recording.js +50 -0
- package/dist/extensions/agent-browser/lib/runtime.js +72 -20
- package/dist/extensions/agent-browser/lib/session-page-state.js +24 -8
- package/dist/extensions/agent-browser/lib/temp.js +4 -0
- package/dist/scripts/agent-browser-target.mjs +1 -1
- package/docs/ARCHITECTURE.md +29 -12
- package/docs/COMMAND_REFERENCE.md +67 -31
- package/docs/RELEASE.md +6 -4
- package/docs/SUPPORT_MATRIX.md +24 -16
- package/docs/TOOL_CONTRACT.md +82 -28
- package/package.json +1 -1
- package/scripts/agent-browser-capability-baseline.mjs +10 -3
- package/scripts/agent-browser-target.mjs +1 -1
- package/scripts/prepare.mjs +2 -4
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { copyFile, mkdir } from "node:fs/promises";
|
|
2
2
|
import { dirname, resolve } from "node:path";
|
|
3
|
-
import { getBooleanFlagValue, isUpstreamEnvFlagEnabled, projectUpstreamGlobalFlags } from "../../argv-grammar.js";
|
|
3
|
+
import { extractExplicitSessionName, getBooleanFlagValue, isUpstreamEnvFlagEnabled, projectUpstreamGlobalFlags, resolveAgentBrowserNamespace } from "../../argv-grammar.js";
|
|
4
4
|
import { isCloseCommand } from "../../command-taxonomy.js";
|
|
5
|
+
import { isBrowserIndependentRead } from "../../command-policy.js";
|
|
5
6
|
import { cleanupElectronLaunchResources } from "../../electron/cleanup.js";
|
|
6
7
|
import { launchElectronApp } from "../../electron/launch.js";
|
|
7
8
|
import { pathExists } from "../../fs-utils.js";
|
|
@@ -73,21 +74,19 @@ async function ensureArtifactParentDirectory(commandTokens, cwd) {
|
|
|
73
74
|
return;
|
|
74
75
|
await mkdir(dirname(resolve(cwd, requestedPath)), { recursive: true });
|
|
75
76
|
}
|
|
76
|
-
async function normalizeScreenshotPathInTokens(commandTokens, cwd) {
|
|
77
|
-
|
|
78
|
-
const projection = projectUpstreamGlobalFlags(
|
|
79
|
-
const
|
|
80
|
-
const
|
|
81
|
-
if (
|
|
77
|
+
async function normalizeScreenshotPathInTokens(commandTokens, cwd, batchStep = false) {
|
|
78
|
+
// Native batch rows skip outer CLI global-flag cleanup.
|
|
79
|
+
const projection = batchStep ? undefined : projectUpstreamGlobalFlags(commandTokens);
|
|
80
|
+
const pathIndex = getScreenshotPathTokenIndex(projection?.tokens ?? commandTokens);
|
|
81
|
+
const screenshotPathTokenIndex = pathIndex === undefined ? undefined : projection ? projection.indices[pathIndex] : pathIndex;
|
|
82
|
+
if (screenshotPathTokenIndex === undefined)
|
|
82
83
|
return { tokens: commandTokens };
|
|
83
|
-
}
|
|
84
|
-
const screenshotPathTokenIndex = commandTokens.length - scopedCommandTokens.length + scopedPathTokenIndex;
|
|
85
84
|
const requestedPath = commandTokens[screenshotPathTokenIndex];
|
|
86
85
|
const absolutePath = resolve(cwd, requestedPath);
|
|
87
86
|
await mkdir(dirname(absolutePath), { recursive: true });
|
|
88
87
|
const tokens = [...commandTokens];
|
|
89
88
|
tokens[screenshotPathTokenIndex] = absolutePath;
|
|
90
|
-
const terminatorIndex = tokens.indexOf("--");
|
|
89
|
+
const terminatorIndex = batchStep ? -1 : tokens.indexOf("--");
|
|
91
90
|
if (terminatorIndex >= 0) {
|
|
92
91
|
tokens.splice(terminatorIndex, 1);
|
|
93
92
|
}
|
|
@@ -110,13 +109,12 @@ async function prepareBatchScreenshotPaths(args, stdin, cwd) {
|
|
|
110
109
|
// prepare parent directories for the rows that will run and skip stdin
|
|
111
110
|
// preparation (no directories for never-executed rows).
|
|
112
111
|
for (const step of argumentSteps) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if (stepTokens[0] === "screenshot") {
|
|
112
|
+
await ensureArtifactParentDirectory(step, cwd);
|
|
113
|
+
if (step[0] === "screenshot") {
|
|
116
114
|
// Reuse the screenshot path resolution for its parent-directory side
|
|
117
115
|
// effect only: raw strings are never rewritten, so the normalized
|
|
118
116
|
// tokens and path request are deliberately discarded.
|
|
119
|
-
await normalizeScreenshotPathInTokens(step, cwd);
|
|
117
|
+
await normalizeScreenshotPathInTokens(step, cwd, true);
|
|
120
118
|
}
|
|
121
119
|
}
|
|
122
120
|
return undefined;
|
|
@@ -134,12 +132,11 @@ async function prepareBatchScreenshotPaths(args, stdin, cwd) {
|
|
|
134
132
|
if (!Array.isArray(step) || !step.every((item) => typeof item === "string")) {
|
|
135
133
|
return step;
|
|
136
134
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
if (upstreamStep[0] !== "screenshot") {
|
|
135
|
+
await ensureArtifactParentDirectory(step, cwd);
|
|
136
|
+
if (step[0] !== "screenshot") {
|
|
140
137
|
return step;
|
|
141
138
|
}
|
|
142
|
-
const normalized = await normalizeScreenshotPathInTokens(step, cwd);
|
|
139
|
+
const normalized = await normalizeScreenshotPathInTokens(step, cwd, true);
|
|
143
140
|
batchScreenshotPathRequests[index] = normalized.request;
|
|
144
141
|
if (normalized.request) {
|
|
145
142
|
changed = true;
|
|
@@ -425,6 +422,8 @@ export async function prepareBrowserRun(options) {
|
|
|
425
422
|
} };
|
|
426
423
|
}
|
|
427
424
|
const userRequestedJson = runtimeToolArgs.includes("--json");
|
|
425
|
+
const routedReadConfirmation = state.sessionPageState.findReadConfirmation(preparedArgs.args, resolveAgentBrowserNamespace(preparedArgs.args, agentBrowserProcessEnv.AGENT_BROWSER_NAMESPACE));
|
|
426
|
+
const readConfirmation = routedReadConfirmation?.capabilities?.readRequiresConfirmation === true ? routedReadConfirmation : undefined;
|
|
428
427
|
let executionPlan = buildExecutionPlan(preparedArgs.args, {
|
|
429
428
|
freshSessionName,
|
|
430
429
|
managedSessionActive: state.managedSessionActive,
|
|
@@ -432,13 +431,14 @@ export async function prepareBrowserRun(options) {
|
|
|
432
431
|
managedSessionName: state.managedSessionName,
|
|
433
432
|
managedSessionNamespace: state.managedSessionNamespace,
|
|
434
433
|
sessionMode,
|
|
434
|
+
stdin: runtimeToolStdin,
|
|
435
|
+
browserIndependentReadConfirmation: readConfirmation !== undefined,
|
|
435
436
|
});
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
executionPlan = { ...executionPlan, recoveryHint: undefined, validationError: idleTimeoutMismatch };
|
|
437
|
+
const browserIndependent = readConfirmation !== undefined || isBrowserIndependentRead(extractUpstreamCommandTokens(preparedArgs.args), runtimeToolStdin)
|
|
438
|
+
|| (executionPlan.commandInfo.command === "session" && executionPlan.commandInfo.subcommand === "info");
|
|
439
439
|
const ownedSessionKey = getSessionContextKey(executionPlan.sessionName, executionPlan.namespace);
|
|
440
440
|
const plannedSessionPageState = sessionPageState.get(ownedSessionKey);
|
|
441
|
-
const pageTargetError = getPageTargetValidationError({
|
|
441
|
+
const pageTargetError = readConfirmation ? undefined : getPageTargetValidationError({
|
|
442
442
|
args: executionPlan.effectiveArgs,
|
|
443
443
|
currentPageUrl: plannedSessionPageState.tabTarget?.url,
|
|
444
444
|
pageUrlUnknown: plannedSessionPageState.tabTargetUnknown === true,
|
|
@@ -450,13 +450,18 @@ export async function prepareBrowserRun(options) {
|
|
|
450
450
|
const targetsCurrentManagedSession = state.managedSessionActive
|
|
451
451
|
&& ownedSessionKey === getSessionContextKey(state.managedSessionName, state.managedSessionNamespace);
|
|
452
452
|
const targetsOffCurrentOwnedSession = recordedOwnedSession !== undefined && !targetsCurrentManagedSession;
|
|
453
|
+
const idleTimeoutMismatch = !browserIndependent && (executionPlan.managedSessionName || recordedOwnedSession || targetsCurrentManagedSession || (state.managedSessionActive && extractExplicitSessionName(preparedArgs.args) === undefined))
|
|
454
|
+
? getIdleTimeoutMismatch(preparedArgs.args, options.implicitSessionIdleTimeoutMs)
|
|
455
|
+
: undefined;
|
|
456
|
+
if (idleTimeoutMismatch)
|
|
457
|
+
executionPlan = { ...executionPlan, recoveryHint: undefined, validationError: idleTimeoutMismatch };
|
|
453
458
|
const offCurrentLaunchScopedFlags = targetsOffCurrentOwnedSession
|
|
454
459
|
? executionPlan.startupScopedFlags.filter((flag) => flag !== "--namespace")
|
|
455
460
|
: [];
|
|
456
461
|
const offCurrentCompatibilityUpgrade = targetsOffCurrentOwnedSession
|
|
457
462
|
&& executionPlan.compatibilityWorkaround !== undefined
|
|
458
463
|
&& recordedOwnedSession.compatibilityWorkaround === undefined;
|
|
459
|
-
if (targetsOffCurrentOwnedSession && canUseHeadlessCompatibilityUserAgent(preparedArgs.args, agentBrowserProcessEnv)) {
|
|
464
|
+
if (!browserIndependent && targetsOffCurrentOwnedSession && canUseHeadlessCompatibilityUserAgent(preparedArgs.args, agentBrowserProcessEnv)) {
|
|
460
465
|
const compatibilityWorkaround = executionPlan.compatibilityWorkaround ?? recordedOwnedSession.compatibilityWorkaround;
|
|
461
466
|
if (compatibilityWorkaround) {
|
|
462
467
|
const userAgentIndex = executionPlan.effectiveArgs.indexOf("--user-agent");
|
|
@@ -475,7 +480,7 @@ export async function prepareBrowserRun(options) {
|
|
|
475
480
|
?? (targetsCurrentManagedSession ? state.managedSessionHeadedAutosaveInterval : undefined);
|
|
476
481
|
const explicitAutosaveInterval = resolveExplicitAutosaveInterval(agentBrowserProcessEnv.AGENT_BROWSER_AUTOSAVE_INTERVAL_MS);
|
|
477
482
|
const autosavePolicyChangeError = getRunningHeadedAutosavePolicyChangeError(retainedHeadedAutosaveInterval, isCloseCommand(executionPlan.commandInfo.command));
|
|
478
|
-
if (!executionPlan.validationError && autosavePolicyChangeError) {
|
|
483
|
+
if (!browserIndependent && !executionPlan.validationError && autosavePolicyChangeError) {
|
|
479
484
|
executionPlan = { ...executionPlan, recoveryHint: undefined, validationError: autosavePolicyChangeError };
|
|
480
485
|
}
|
|
481
486
|
const headedLaunch = getBooleanFlagValue(executionPlan.effectiveArgs, "--headed") ?? isUpstreamEnvFlagEnabled(agentBrowserProcessEnv.AGENT_BROWSER_HEADED);
|
|
@@ -485,13 +490,14 @@ export async function prepareBrowserRun(options) {
|
|
|
485
490
|
const compatibilityUserAgent = executionPlan.compatibilityWorkaround ? getDefaultHeadlessCompatUserAgent() : undefined;
|
|
486
491
|
const compatibilityUserAgentApplied = compatibilityUserAgent !== undefined
|
|
487
492
|
&& executionPlan.effectiveArgs.some((token, index) => token === "--user-agent" && executionPlan.effectiveArgs[index + 1] === compatibilityUserAgent);
|
|
488
|
-
const ownedManagedSession = buildOwnedManagedSessionRestoreContext({
|
|
493
|
+
const ownedManagedSession = browserIndependent && !recordedOwnedSession && !targetsCurrentManagedSession ? undefined : buildOwnedManagedSessionRestoreContext({
|
|
489
494
|
args: executionPlan.effectiveArgs,
|
|
495
|
+
reuseOnly: browserIndependent,
|
|
490
496
|
cwd: recordedOwnedSession?.cwd ?? cwd,
|
|
491
497
|
currentManagedSessionName: state.managedSessionName,
|
|
492
498
|
currentManagedSessionNamespace: state.managedSessionNamespace,
|
|
493
|
-
headedManagedAutosaveDisabled,
|
|
494
|
-
headedManagedAutosaveInterval,
|
|
499
|
+
headedManagedAutosaveDisabled: browserIndependent ? retainedHeadedAutosaveDisabled : headedManagedAutosaveDisabled,
|
|
500
|
+
headedManagedAutosaveInterval: browserIndependent ? retainedHeadedAutosaveInterval : headedManagedAutosaveInterval,
|
|
495
501
|
managedSessionName: executionPlan.managedSessionName,
|
|
496
502
|
namespace: executionPlan.namespace,
|
|
497
503
|
parentEnv: agentBrowserProcessEnv,
|
|
@@ -504,7 +510,7 @@ export async function prepareBrowserRun(options) {
|
|
|
504
510
|
});
|
|
505
511
|
let managedSessionDaemonInactive = false;
|
|
506
512
|
let managedSessionCleanupOnlyReason;
|
|
507
|
-
if (!executionPlan.validationError && ownedManagedSession) {
|
|
513
|
+
if (!browserIndependent && !executionPlan.validationError && ownedManagedSession) {
|
|
508
514
|
const closeCommand = isCloseCommand(executionPlan.commandInfo.command);
|
|
509
515
|
const policy = await acquireOwnedManagedSessionDaemonPolicy({
|
|
510
516
|
context: ownedManagedSession,
|
|
@@ -554,7 +560,7 @@ export async function prepareBrowserRun(options) {
|
|
|
554
560
|
const sessionTabPinningReason = priorSessionPageState.pinningReason;
|
|
555
561
|
let priorRefSnapshotState = priorSessionPageState.refSnapshot;
|
|
556
562
|
let priorRefSnapshotInvalidation = priorSessionPageState.refSnapshotInvalidation;
|
|
557
|
-
const coldManagedSession = (managedSessionDaemonInactive || priorSessionPageState.tabReopenPending === true)
|
|
563
|
+
const coldManagedSession = !browserIndependent && (managedSessionDaemonInactive || priorSessionPageState.tabReopenPending === true)
|
|
558
564
|
&& recordedOwnedSession !== undefined
|
|
559
565
|
&& sessionTabPinningReason === "restore"
|
|
560
566
|
&& ownedManagedSession?.restoreDecision === "enabled"
|
|
@@ -577,7 +583,7 @@ export async function prepareBrowserRun(options) {
|
|
|
577
583
|
command: executionPlan.commandInfo.command,
|
|
578
584
|
commandTokens: plannedCommandTokens,
|
|
579
585
|
// URL QA clears diagnostics before its explicit open; those clears do not need the old tab.
|
|
580
|
-
pinningRequired: sessionTabPinningReason !== undefined && compiledQaPreset?.checks.url === undefined,
|
|
586
|
+
pinningRequired: !readConfirmation && sessionTabPinningReason !== undefined && compiledQaPreset?.checks.url === undefined,
|
|
581
587
|
reopenPending: coldManagedSession,
|
|
582
588
|
sessionName: executionPlan.sessionName,
|
|
583
589
|
stdin: runtimeToolStdin,
|
|
@@ -617,7 +623,7 @@ export async function prepareBrowserRun(options) {
|
|
|
617
623
|
const isCallerOwnedExplicitSession = () => executionPlan.sessionName !== undefined
|
|
618
624
|
&& executionPlan.usedImplicitSession === false
|
|
619
625
|
&& ownedManagedSession === undefined;
|
|
620
|
-
const requiresLivePageVerification = () => isCallerOwnedExplicitSession() || options.preserveAttachedBrowserSession === true;
|
|
626
|
+
const requiresLivePageVerification = () => !readConfirmation && (isCallerOwnedExplicitSession() || options.preserveAttachedBrowserSession === true);
|
|
621
627
|
const verifyLivePage = async (request) => {
|
|
622
628
|
if (!request.requirement || !executionPlan.sessionName)
|
|
623
629
|
return;
|
|
@@ -1054,6 +1060,7 @@ export async function prepareBrowserRun(options) {
|
|
|
1054
1060
|
executionPlan,
|
|
1055
1061
|
ownedManagedSessionContext: ownedManagedSession,
|
|
1056
1062
|
preparedArgs,
|
|
1063
|
+
readConfirmation,
|
|
1057
1064
|
priorRefSnapshotState,
|
|
1058
1065
|
priorSessionTabTarget,
|
|
1059
1066
|
priorSessionTabTargetUnknown,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { rm } from "node:fs/promises";
|
|
2
2
|
import { parseArgvDescriptor } from "../../argv-descriptor.js";
|
|
3
|
-
import { needsManagedSession } from "../../command-policy.js";
|
|
4
|
-
import { getAgentBrowserSessionIdentityKey, isAgentBrowserSessionIdentityKeyInNamespace } from "../../argv-grammar.js";
|
|
3
|
+
import { isBrowserIndependentRead, needsManagedSession } from "../../command-policy.js";
|
|
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";
|
|
7
7
|
import { OPEN_RESULT_TAB_CORRECTION_FLAGS } from "../../launch-scoped-flags.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) {
|
|
@@ -108,12 +111,6 @@ function batchStartedManagedBrowser(data) {
|
|
|
108
111
|
function withoutNamespaceEntries(entries, namespace) {
|
|
109
112
|
return new Map([...entries].filter(([key]) => !isAgentBrowserSessionIdentityKeyInNamespace(key, namespace)));
|
|
110
113
|
}
|
|
111
|
-
function deleteNamespaceEntries(entries, namespace) {
|
|
112
|
-
for (const key of entries.keys()) {
|
|
113
|
-
if (isAgentBrowserSessionIdentityKeyInNamespace(key, namespace))
|
|
114
|
-
entries.delete(key);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
114
|
function setNetworkRouteState(options) {
|
|
118
115
|
if (!options.sessionName)
|
|
119
116
|
return options.routesBySession;
|
|
@@ -167,7 +164,13 @@ export async function processBrowserOutput(input) {
|
|
|
167
164
|
const plainTextUpgrade = !prepared.executionPlan.plainTextInspection && prepared.executionPlan.commandInfo.command === "upgrade" && !needsManagedSession(parseArgvDescriptor(prepared.runtimeToolArgs));
|
|
168
165
|
const parsed = await parseAgentBrowserEnvelope({ stdout: processResult.stdout, stdoutPath: processResult.stdoutSpillPath, plainText: plainTextUpgrade });
|
|
169
166
|
let parseError = parsed.parseError;
|
|
170
|
-
|
|
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;
|
|
171
174
|
let navigationSummary = undefined;
|
|
172
175
|
let failedTransitionReverification = false;
|
|
173
176
|
const repairedScreenshot = await repairScreenshotArtifact({ cwd, envelope: presentationEnvelope, request: prepared.preparedArgs.screenshotPathRequest });
|
|
@@ -184,6 +187,25 @@ export async function processBrowserOutput(input) {
|
|
|
184
187
|
: Array.isArray(presentationEnvelope?.data)
|
|
185
188
|
? presentationEnvelope.data.flatMap((row, index) => isRecord(row) ? [Array.isArray(row.command) && row.command.every((token) => typeof token === "string") ? row.command : batchCommandSteps[index] ?? []] : [])
|
|
186
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
|
+
}
|
|
187
209
|
const destinationTransition = dispatchedCommands.some((step) => {
|
|
188
210
|
const [command, subcommand] = extractUpstreamCommandTokens(step);
|
|
189
211
|
return isWindowOrDiffPageTransitionCommand(command, subcommand);
|
|
@@ -206,6 +228,7 @@ export async function processBrowserOutput(input) {
|
|
|
206
228
|
const parseFailureOutput = parseError && processResult.stdoutSpillPath
|
|
207
229
|
? { fullOutputUnavailable: "Malformed upstream output was discarded because it may contain sensitive browser data." }
|
|
208
230
|
: {};
|
|
231
|
+
const browserIndependentRead = prepared.readConfirmation !== undefined || isBrowserIndependentRead(prepared.commandTokens, prepared.runtimeToolStdin);
|
|
209
232
|
const processSucceeded = !processResult.timedOut && !processResult.aborted && !processResult.spawnError && processResult.exitCode === 0;
|
|
210
233
|
const plainTextInspection = prepared.executionPlan.plainTextInspection && processSucceeded;
|
|
211
234
|
const parseSucceeded = plainTextInspection || parseError === undefined;
|
|
@@ -213,7 +236,7 @@ export async function processBrowserOutput(input) {
|
|
|
213
236
|
presentationEnvelope = { success: true, data: { alreadyEnabled: true, enabled: true, message: getEnvelopeErrorString(presentationEnvelope) ?? "Stream already enabled" } };
|
|
214
237
|
}
|
|
215
238
|
const envelopeSuccess = plainTextInspection ? true : presentationEnvelope?.success !== false;
|
|
216
|
-
let succeeded = processSucceeded && parseSucceeded && envelopeSuccess;
|
|
239
|
+
let succeeded = (processSucceeded && parseSucceeded && envelopeSuccess) || recordingStopRecovery?.recovery.healed === true;
|
|
217
240
|
const inspectionText = plainTextInspection ? processResult.stdout.trim() : undefined;
|
|
218
241
|
const sessionStateKey = getSessionContextKey(prepared.executionPlan.sessionName, prepared.executionPlan.namespace);
|
|
219
242
|
const closeAllApplied = nestedBatchClosesAll || (directCloseAllRequested && succeeded);
|
|
@@ -223,8 +246,8 @@ export async function processBrowserOutput(input) {
|
|
|
223
246
|
}
|
|
224
247
|
if (closeAllApplied) {
|
|
225
248
|
networkRoutesBySession = withoutNamespaceEntries(networkRoutesBySession, prepared.executionPlan.namespace);
|
|
226
|
-
|
|
227
|
-
|
|
249
|
+
deleteIdentityKeysInNamespace(state.attachedSessionKeys, prepared.executionPlan.namespace);
|
|
250
|
+
deleteIdentityKeysInNamespace(traceOwners, prepared.executionPlan.namespace);
|
|
228
251
|
sessionPageState.clearNamespace(prepared.executionPlan.namespace);
|
|
229
252
|
const retainedSessionKey = nestedBatchRemainsActive ? sessionStateKey : undefined;
|
|
230
253
|
for (const [key, owner] of state.ownedManagedSessions) {
|
|
@@ -370,7 +393,7 @@ export async function processBrowserOutput(input) {
|
|
|
370
393
|
let fillVerificationDiagnostic;
|
|
371
394
|
let selectorTextVisibilityDiagnostics = [];
|
|
372
395
|
let electronBroadGetTextScopeDiagnostics = [];
|
|
373
|
-
const timeoutPartialProgress = processResult.timedOut ? await collectTimeoutPartialProgress({
|
|
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;
|
|
374
397
|
if (!currentSessionTabTarget && timeoutPartialProgress?.currentPage?.source === "live") {
|
|
375
398
|
currentSessionTabTarget = normalizeSessionTabTarget(timeoutPartialProgress.currentPage);
|
|
376
399
|
}
|
|
@@ -402,7 +425,7 @@ export async function processBrowserOutput(input) {
|
|
|
402
425
|
const batchRefSnapshotState = prepared.executionPlan.commandInfo.command === "batch" ? extractLatestRefSnapshotStateFromBatchResults(presentationEnvelope?.data) : undefined;
|
|
403
426
|
let currentRefSnapshot;
|
|
404
427
|
let currentRefSnapshotInvalidation;
|
|
405
|
-
if (sessionStateKey) {
|
|
428
|
+
if (sessionStateKey && !browserIndependentRead) {
|
|
406
429
|
const sessionClosed = (isCloseCommand(prepared.executionPlan.commandInfo.command) && succeeded) || nestedBatchClosed;
|
|
407
430
|
if (sessionClosed) {
|
|
408
431
|
state.attachedSessionKeys.delete(sessionStateKey);
|
|
@@ -433,7 +456,7 @@ export async function processBrowserOutput(input) {
|
|
|
433
456
|
if (!tabUpdate.applied && succeeded)
|
|
434
457
|
sessionPageState.markPinning(sessionStateKey, "drift");
|
|
435
458
|
}
|
|
436
|
-
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")) {
|
|
437
460
|
sessionPageState.markTabTargetUnknown({ sessionName: sessionStateKey, update: sessionPageStateUpdate });
|
|
438
461
|
}
|
|
439
462
|
const refSnapshot = unsettledWebMcpMutation
|
|
@@ -518,7 +541,7 @@ export async function processBrowserOutput(input) {
|
|
|
518
541
|
managedSessionHeadedAutosaveDisabled = false;
|
|
519
542
|
managedSessionHeadedAutosaveInterval = undefined;
|
|
520
543
|
}
|
|
521
|
-
else if (managedTransitionSucceeded && executionTargetsManagedSession) {
|
|
544
|
+
else if (managedTransitionSucceeded && executionTargetsManagedSession && prepared.ownedManagedSessionContext && !prepared.ownedManagedSessionContext.reuseOnly) {
|
|
522
545
|
managedSessionCompatibilityWorkaround = prepared.compatibilityWorkaround;
|
|
523
546
|
managedSessionHeadedAutosaveDisabled = prepared.ownedManagedSessionContext?.headedManagedAutosaveDisabled === true;
|
|
524
547
|
managedSessionHeadedAutosaveInterval = prepared.ownedManagedSessionContext?.headedManagedAutosaveInterval;
|
|
@@ -636,7 +659,7 @@ export async function processBrowserOutput(input) {
|
|
|
636
659
|
electronLaunchRecord = electronFailedConnectCleanup.record;
|
|
637
660
|
}
|
|
638
661
|
}
|
|
639
|
-
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 }) });
|
|
640
663
|
if (errorText && presentationEnvelope?.success === false && extractEnvelopeErrorText(presentationEnvelope.error) === undefined)
|
|
641
664
|
presentationEnvelope = { ...presentationEnvelope, error: errorText };
|
|
642
665
|
if (errorText) {
|
|
@@ -647,7 +670,21 @@ export async function processBrowserOutput(input) {
|
|
|
647
670
|
}
|
|
648
671
|
if (plainTextUpgrade && errorText)
|
|
649
672
|
presentationEnvelope = { ...presentationEnvelope, success: false, error: errorText };
|
|
650
|
-
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
|
+
}
|
|
651
688
|
if (plainTextUpgrade && !succeeded && typeof presentationEnvelope?.data === "string") {
|
|
652
689
|
presentation.data = presentationEnvelope.data;
|
|
653
690
|
const errorContent = presentation.content[0];
|
|
@@ -757,13 +794,15 @@ export async function processBrowserOutput(input) {
|
|
|
757
794
|
const evalPageUrl = evalNavigationSummary?.url ?? currentSessionTabTarget?.url ?? prepared.priorSessionTabTarget?.url ?? evalSessionTabUrl;
|
|
758
795
|
const evalStdinHint = getEvalStdinHint({ command: prepared.executionPlan.commandInfo.command, data: presentationEnvelope?.data, stdin: prepared.runtimeToolStdin });
|
|
759
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);
|
|
760
799
|
const resultArtifactManifest = presentation.artifactManifest ?? artifactManifest;
|
|
761
800
|
const artifactCleanup = await getArtifactCleanupGuidance({ command: prepared.executionPlan.commandInfo.command, cwd, manifest: resultArtifactManifest, succeeded });
|
|
762
801
|
const recordingTransitionReached = prepared.executionPlan.commandInfo.command === "batch"
|
|
763
802
|
? presentation.batchSteps?.some((step) => isRecordPageTransitionCommand(extractUpstreamCommandTokens(step.command ?? [])))
|
|
764
803
|
: isRecordPageTransitionCommand(prepared.commandTokens);
|
|
765
804
|
const recordingPageWarning = processResult.agentBrowserStarted && !prepared.executionPlan.plainTextInspection && recordingTransitionReached
|
|
766
|
-
? "Page state: this
|
|
805
|
+
? "Page state: this wrapper conservatively invalidates earlier refs after recording starts and URL-bearing restarts. Take a fresh snapshot before continuing; this does not prove the page changed."
|
|
767
806
|
: undefined;
|
|
768
807
|
const sessionWarning = electronPostCommandHealth ? formatElectronPostCommandHealthText(electronPostCommandHealth) : electronSessionMismatch ? formatElectronSessionMismatchText(electronSessionMismatch) : aboutBlankSessionMismatch ? buildAboutBlankWarning(aboutBlankSessionMismatch) : undefined;
|
|
769
808
|
const warningText = [sessionWarning, recordingPageWarning].filter(Boolean).join("\n\n") || undefined;
|
|
@@ -778,9 +817,10 @@ export async function processBrowserOutput(input) {
|
|
|
778
817
|
const resultRetainsPreparedManagedSession = !managedSessionOutcome || (managedSessionOutcome.activeAfter
|
|
779
818
|
&& managedSessionOutcome.attemptedSessionName === managedSessionOutcome.currentSessionName);
|
|
780
819
|
const resultHeadedManagedAutosaveDisabled = prepared.ownedManagedSessionContext?.headedManagedAutosaveDisabled === true
|
|
820
|
+
&& !prepared.ownedManagedSessionContext.reuseOnly
|
|
781
821
|
&& resultRetainsPreparedManagedSession
|
|
782
822
|
&& !(commandClosesSession && succeeded);
|
|
783
|
-
const resultHeadedManagedAutosaveInterval = resultRetainsPreparedManagedSession && !(commandClosesSession && succeeded)
|
|
823
|
+
const resultHeadedManagedAutosaveInterval = resultRetainsPreparedManagedSession && !prepared.ownedManagedSessionContext?.reuseOnly && !(commandClosesSession && succeeded)
|
|
784
824
|
? prepared.ownedManagedSessionContext?.headedManagedAutosaveInterval
|
|
785
825
|
: undefined;
|
|
786
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,13 +1,13 @@
|
|
|
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";
|
|
8
8
|
import { buildNextToolAction, withOptionalNamespaceArgs, withOptionalSessionArgs } from "../../results/next-actions.js";
|
|
9
9
|
import { getSessionPageStateKey, isAboutBlankUrl, normalizeComparableUrl, targetsMatch, } from "../../session-page-state.js";
|
|
10
|
-
import { isCloseCommand, isElectronPostCommandHealthCommand, isNavigationObservableCommandName, isOpenNavigationCommand, isRefGuardedCommand, isRefInvalidatingBatchCommand,
|
|
10
|
+
import { getRecordCommandOperands, isCloseCommand, isElectronPostCommandHealthCommand, isNavigationObservableCommandName, isOpenNavigationCommand, isRefGuardedCommand, isRefInvalidatingBatchCommand, isSessionTabPinningExcludedCommand, isSessionTabPostCommandCorrectionExcludedCommand, isWindowOrDiffPageTransitionCommand, } from "../../command-taxonomy.js";
|
|
11
11
|
import { chooseOpenResultTabCorrection } from "../../runtime.js";
|
|
12
12
|
import { isRecord, parseRefId } from "../../parsing.js";
|
|
13
13
|
import { getUpstreamEffectiveBatchSteps } from "../batch-stdin.js";
|
|
@@ -424,10 +424,10 @@ export function commandChoosesSessionTabTarget(args) {
|
|
|
424
424
|
|| isWindowOrDiffPageTransitionCommand(command, subcommand)
|
|
425
425
|
|| (command === "a11y" && findFirstPositionalArgument(tokens) !== undefined)
|
|
426
426
|
|| (["vitals", "web-vitals"].includes(command) && tokens.slice(1).some((token) => !token.startsWith("--")))
|
|
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 === "
|
|
444
|
+
if (command === "record" && subcommand === "stop")
|
|
445
445
|
continue;
|
|
446
446
|
if (["console", "errors"].includes(command))
|
|
447
447
|
continue;
|