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.
- package/CHANGELOG.md +43 -0
- package/README.md +50 -7
- package/dist/extensions/agent-browser/index.js +416 -430
- package/dist/extensions/agent-browser/lib/argv-grammar.js +1 -1
- package/dist/extensions/agent-browser/lib/command-policy.js +41 -2
- package/dist/extensions/agent-browser/lib/input-modes/job.js +7 -7
- 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 +40 -10
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +3 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +8 -2
- 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 +25 -14
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +56 -10
- 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 +3 -3
- 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 +10 -9
- 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 +60 -29
- package/dist/extensions/agent-browser/lib/results/presentation/batch.js +19 -8
- package/dist/extensions/agent-browser/lib/results/presentation/common.js +0 -20
- 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 +23 -7
- package/dist/extensions/agent-browser/lib/temp.js +4 -0
- package/docs/ARCHITECTURE.md +27 -11
- package/docs/COMMAND_REFERENCE.md +37 -17
- package/docs/RELEASE.md +3 -1
- package/docs/SUPPORT_MATRIX.md +10 -4
- package/docs/TOOL_CONTRACT.md +87 -25
- package/docs/platform-smoke.md +9 -1
- package/package.json +1 -1
- 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
|
-
|
|
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 === "
|
|
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,
|
|
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
|
-
:
|
|
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
|
|
60
|
+
if (!details)
|
|
46
61
|
return { source: "content.text", value: getTextContent(result) };
|
|
47
62
|
const manifest = isSessionArtifactManifest(details.artifactManifest) ? details.artifactManifest : undefined;
|
|
48
|
-
|
|
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 (
|
|
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:
|
|
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
|
-
|
|
170
|
+
const descriptor = parseArgvDescriptor(options.args);
|
|
171
|
+
if (!options.pageUrlUnknown || !needsManagedSession(descriptor))
|
|
171
172
|
return undefined;
|
|
172
|
-
const { command, subcommand } =
|
|
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;
|