pi-agent-browser-native 0.6.6 → 0.6.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +40 -1
  2. package/README.md +21 -6
  3. package/dist/extensions/agent-browser/index.js +138 -75
  4. package/dist/extensions/agent-browser/lib/command-taxonomy.js +6 -5
  5. package/dist/extensions/agent-browser/lib/electron/cleanup.js +10 -1
  6. package/dist/extensions/agent-browser/lib/input-modes/params.js +20 -7
  7. package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +0 -1
  8. package/dist/extensions/agent-browser/lib/managed-session-restore.js +13 -12
  9. package/dist/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.js +6 -25
  10. package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +2 -2
  11. package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +2 -3
  12. package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +10 -3
  13. package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +3 -0
  14. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/wait-timeouts.js +1 -1
  15. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +104 -117
  16. package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +64 -36
  17. package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +100 -135
  18. package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +3 -1
  19. package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +3 -1
  20. package/dist/extensions/agent-browser/lib/page-target-validation.js +10 -10
  21. package/dist/extensions/agent-browser/lib/parsing.js +7 -0
  22. package/dist/extensions/agent-browser/lib/playbook.js +6 -9
  23. package/dist/extensions/agent-browser/lib/process.js +28 -113
  24. package/dist/extensions/agent-browser/lib/recording-reservations.js +3 -1
  25. package/dist/extensions/agent-browser/lib/results/action-recommendations.js +5 -2
  26. package/dist/extensions/agent-browser/lib/results/envelope.js +9 -4
  27. package/dist/extensions/agent-browser/lib/results/next-actions.js +8 -0
  28. package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +45 -43
  29. package/dist/extensions/agent-browser/lib/results/presentation/batch.js +2 -1
  30. package/dist/extensions/agent-browser/lib/results/presentation/errors.js +10 -2
  31. package/dist/extensions/agent-browser/lib/results/presentation.js +9 -2
  32. package/dist/extensions/agent-browser/lib/results/recovery-actions.js +4 -4
  33. package/dist/extensions/agent-browser/lib/runtime.js +18 -2
  34. package/dist/extensions/agent-browser/lib/session-page-state.js +29 -10
  35. package/docs/ARCHITECTURE.md +8 -5
  36. package/docs/COMMAND_REFERENCE.md +25 -16
  37. package/docs/ELECTRON.md +6 -6
  38. package/docs/RELEASE.md +14 -5
  39. package/docs/REQUIREMENTS.md +1 -1
  40. package/docs/SUPPORT_MATRIX.md +18 -5
  41. package/docs/TOOL_CONTRACT.md +38 -25
  42. package/package.json +5 -1
@@ -1,7 +1,9 @@
1
1
  import { rm } from "node:fs/promises";
2
+ import { parseArgvDescriptor } from "../../argv-descriptor.js";
3
+ import { needsManagedSession } from "../../command-policy.js";
2
4
  import { getAgentBrowserSessionIdentityKey, isAgentBrowserSessionIdentityKeyInNamespace } from "../../argv-grammar.js";
3
5
  import { batchHasSuccessfulCloseAll, getSuccessfulBatchCloseLifecycle } from "../../batch-lifecycle.js";
4
- import { isCloseAllCommand, isCloseCommand, isOpenNavigationCommand, isUnverifiedPageTransitionCommand } from "../../command-taxonomy.js";
6
+ import { isCloseAllCommand, isCloseCommand, isOpenNavigationCommand, isRecordPageTransitionCommand, isUnverifiedPageTransitionCommand, isWindowOrDiffPageTransitionCommand } from "../../command-taxonomy.js";
5
7
  import { OPEN_RESULT_TAB_CORRECTION_FLAGS } from "../../launch-scoped-flags.js";
6
8
  import { cleanupElectronLaunchResources, inspectElectronLaunchStatus } from "../../electron/cleanup.js";
7
9
  import { getResultingPageTargetState, commandRequiresLivePageVerification } from "../../page-target-validation.js";
@@ -9,7 +11,8 @@ import { analyzeNetworkSourceLookupResults, analyzeSourceLookupResults, redactNe
9
11
  import { analyzeQaPresetResults, analyzeQaPresetTimeout, buildQaCompactFailureText, buildQaCompactPassText, extractQaPageContext } from "../../input-modes/job.js";
10
12
  import { applyNetworkRouteRecords, buildNetworkRouteDiagnostics } from "../../results/network-routes.js";
11
13
  import { buildToolPresentation } from "../../results/presentation.js";
12
- import { getAgentBrowserErrorText, parseAgentBrowserEnvelope } from "../../results/envelope.js";
14
+ import { compactLargePresentationOutput } from "../../results/presentation/large-output.js";
15
+ import { extractEnvelopeErrorText, getAgentBrowserErrorText, parseAgentBrowserEnvelope } from "../../results/envelope.js";
13
16
  import { omitUpstreamLifecycle } from "../../results/presentation/common.js";
14
17
  import { getClipboardWritePayloadCandidates, redactClipboardPermissionEcho, redactClipboardPermissionErrorValue } from "../../results/presentation/errors.js";
15
18
  import { shouldCaptureSemanticActionNavigationSummary } from "../../results/presentation/semantic-action.js";
@@ -20,7 +23,7 @@ import { isManagedSessionRestoreKey } from "../../managed-session-storage.js";
20
23
  import { createFreshSessionName, extractUpstreamCommandTokens, resolveManagedSessionState } from "../../runtime.js";
21
24
  import { getUpstreamEffectiveBatchSteps } from "../batch-stdin.js";
22
25
  import { closeManagedSession, inspectManagedSessionDaemon } from "./managed-session-daemon-policy.js";
23
- import { applyOpenResultTabCorrection, buildAboutBlankRecoveryHint, buildAboutBlankWarning, buildElectronPostCommandHealthDiagnostic, buildElectronRefFreshnessDiagnostic, buildElectronSessionMismatch, buildManagedSessionOutcome, collectOpenResultTabCorrection, collectSessionTabSelection, extractNavigationSummaryFromData, extractStringResultField, findElectronLaunchRecordForSession, formatElectronPostCommandHealthText, formatElectronSessionMismatchText, getSessionContextKey, getStaleRefArgs, mergeNavigationSummaryIntoData, shouldCaptureNavigationSummary, shouldCorrectSessionTabAfterCommand, shouldInspectElectronPostCommandHealth, unwrapPinnedSessionBatchEnvelope, updateTraceOwnerState, } from "./session-state.js";
26
+ import { applyOpenResultTabCorrection, buildAboutBlankRecoveryHint, buildAboutBlankWarning, buildElectronPostCommandHealthDiagnostic, buildElectronRefFreshnessDiagnostic, buildElectronSessionMismatch, buildManagedSessionOutcome, collectOpenResultTabCorrection, collectSessionTabSelection, commandChoosesSessionTabTarget, extractNavigationSummaryFromData, extractStringResultField, findElectronLaunchRecordForSession, formatElectronPostCommandHealthText, formatElectronSessionMismatchText, getSessionContextKey, getStaleRefArgs, mergeNavigationSummaryIntoData, shouldCaptureNavigationSummary, shouldCorrectSessionTabAfterCommand, shouldInspectElectronPostCommandHealth, updateTraceOwnerState, } from "./session-state.js";
24
27
  import { collectClickDispatchDiagnostic } from "./click-dispatch.js";
25
28
  import { buildScrollNoopDiagnostic, collectComboboxFocusDiagnostic, collectElectronBroadGetTextScopeDiagnostics, collectElectronHandoff, collectFillVerificationDiagnostic, collectNavigationSummary, collectOverlayBlockerDiagnostic, collectQaAttachedTarget, collectSnapshotOverlayBlockerDiagnostic, collectRecordingDependencyWarning, collectScrollPositionSnapshot, collectSelectorTextVisibilityDiagnostics, collectTimeoutPartialProgress, sleepMs, formatQaAttachedTargetText, getArtifactCleanupGuidance, getEvalResultWarning, getEvalStdinHint, getSourceLookupElectronContext, } from "./diagnostics.js";
26
29
  import { repairScreenshotData } from "./prepare.js";
@@ -160,17 +163,13 @@ export async function processBrowserOutput(input) {
160
163
  let networkRoutesBySession = state.networkRoutesBySession;
161
164
  try {
162
165
  const persistentArtifactStore = getPersistentSessionArtifactStore(ctx);
163
- const parsed = await parseAgentBrowserEnvelope({ stdout: processResult.stdout, stdoutPath: processResult.stdoutSpillPath });
166
+ // Native upgrade prints text even with --json; all other command shapes keep strict JSON parsing.
167
+ const plainTextUpgrade = !prepared.executionPlan.plainTextInspection && prepared.executionPlan.commandInfo.command === "upgrade" && !needsManagedSession(parseArgvDescriptor(prepared.runtimeToolArgs));
168
+ const parsed = await parseAgentBrowserEnvelope({ stdout: processResult.stdout, stdoutPath: processResult.stdoutSpillPath, plainText: plainTextUpgrade });
164
169
  let parseError = parsed.parseError;
165
170
  let presentationEnvelope = parsed.envelope;
166
171
  let navigationSummary = undefined;
167
172
  let failedTransitionReverification = false;
168
- if (prepared.pinnedBatchUnwrapMode) {
169
- const pinnedBatchResult = unwrapPinnedSessionBatchEnvelope({ envelope: parsed.envelope, includeNavigationSummary: prepared.includePinnedNavigationSummary, mode: prepared.pinnedBatchUnwrapMode });
170
- parseError = pinnedBatchResult.parseError ?? parseError;
171
- presentationEnvelope = pinnedBatchResult.envelope ?? presentationEnvelope;
172
- navigationSummary = pinnedBatchResult.navigationSummary;
173
- }
174
173
  const repairedScreenshot = await repairScreenshotArtifact({ cwd, envelope: presentationEnvelope, request: prepared.preparedArgs.screenshotPathRequest });
175
174
  presentationEnvelope = repairedScreenshot.envelope;
176
175
  const repairedBatchScreenshots = await repairBatchScreenshotArtifacts({ cwd, envelope: presentationEnvelope, requests: prepared.preparedArgs.batchScreenshotPathRequests });
@@ -180,6 +179,15 @@ export async function processBrowserOutput(input) {
180
179
  const batchCommandSteps = prepared.executionPlan.commandInfo.command === "batch"
181
180
  ? getUpstreamEffectiveBatchSteps(prepared.commandTokens, prepared.runtimeToolStdin)
182
181
  : [];
182
+ const dispatchedCommands = prepared.executionPlan.commandInfo.command !== "batch"
183
+ ? [prepared.commandTokens]
184
+ : Array.isArray(presentationEnvelope?.data)
185
+ ? presentationEnvelope.data.flatMap((row, index) => isRecord(row) ? [Array.isArray(row.command) && row.command.every((token) => typeof token === "string") ? row.command : batchCommandSteps[index] ?? []] : [])
186
+ : batchCommandSteps;
187
+ const destinationTransition = dispatchedCommands.some((step) => {
188
+ const [command, subcommand] = extractUpstreamCommandTokens(step);
189
+ return isWindowOrDiffPageTransitionCommand(command, subcommand);
190
+ });
183
191
  const nestedBatchClose = prepared.executionPlan.commandInfo.command === "batch"
184
192
  ? getSuccessfulBatchCloseLifecycle(presentationEnvelope?.data, batchCommandSteps)
185
193
  : undefined;
@@ -198,7 +206,7 @@ export async function processBrowserOutput(input) {
198
206
  const parseFailureOutput = parseError && processResult.stdoutSpillPath
199
207
  ? { fullOutputUnavailable: "Malformed upstream output was discarded because it may contain sensitive browser data." }
200
208
  : {};
201
- const processSucceeded = !processResult.aborted && !processResult.spawnError && processResult.exitCode === 0;
209
+ const processSucceeded = !processResult.timedOut && !processResult.aborted && !processResult.spawnError && processResult.exitCode === 0;
202
210
  const plainTextInspection = prepared.executionPlan.plainTextInspection && processSucceeded;
203
211
  const parseSucceeded = plainTextInspection || parseError === undefined;
204
212
  if (isStreamEnableAlreadyEnabledNoop({ command: prepared.executionPlan.commandInfo.command, envelope: presentationEnvelope, processSucceeded, subcommand: prepared.executionPlan.commandInfo.subcommand })) {
@@ -209,6 +217,10 @@ export async function processBrowserOutput(input) {
209
217
  const inspectionText = plainTextInspection ? processResult.stdout.trim() : undefined;
210
218
  const sessionStateKey = getSessionContextKey(prepared.executionPlan.sessionName, prepared.executionPlan.namespace);
211
219
  const closeAllApplied = nestedBatchClosesAll || (directCloseAllRequested && succeeded);
220
+ if (sessionStateKey && processResult.agentBrowserStarted && sessionPageState.get(sessionStateKey).tabReopenPending === true) {
221
+ if (dispatchedCommands.some(commandChoosesSessionTabTarget))
222
+ sessionPageState.setTabReopenPending({ pending: false, sessionName: sessionStateKey, update: sessionPageStateUpdate });
223
+ }
212
224
  if (closeAllApplied) {
213
225
  networkRoutesBySession = withoutNamespaceEntries(networkRoutesBySession, prepared.executionPlan.namespace);
214
226
  deleteNamespaceEntries(state.attachedSessionKeys, prepared.executionPlan.namespace);
@@ -252,36 +264,32 @@ export async function processBrowserOutput(input) {
252
264
  }
253
265
  }
254
266
  const tabTransition = prepared.executionPlan.commandInfo.command === "tab" && prepared.executionPlan.commandInfo.subcommand !== undefined && !["list", "new"].includes(prepared.executionPlan.commandInfo.subcommand);
255
- const resultingPageState = getResultingPageTargetState({
256
- args: prepared.executionPlan.effectiveArgs,
257
- currentPageUrl: prepared.priorSessionTabTarget?.url,
258
- pageUrlUnknown: prepared.priorSessionTabTargetUnknown === true,
259
- stdin: prepared.runtimeToolStdin,
260
- trustedFirstBatchTabSelection: prepared.pinnedBatchUnwrapMode !== undefined,
261
- });
267
+ // Non-page rows (including a failed prefix) cannot retire a cold target for a navigation that never ran.
268
+ const resultingPageState = sessionPageState.get(sessionStateKey).tabReopenPending === true
269
+ ? { currentPageUrl: prepared.priorSessionTabTarget?.url, pageTargetMayHaveChanged: false, pageUrlUnknown: prepared.priorSessionTabTargetUnknown === true }
270
+ : getResultingPageTargetState({
271
+ args: prepared.executionPlan.effectiveArgs,
272
+ executedBatchSteps: dispatchedCommands,
273
+ currentPageUrl: prepared.priorSessionTabTarget?.url,
274
+ pageUrlUnknown: prepared.priorSessionTabTargetUnknown === true,
275
+ });
262
276
  if (succeeded &&
263
- !navigationSummary &&
264
277
  (shouldCaptureNavigationSummary(prepared.executionPlan.commandInfo.command, presentationEnvelope?.data, prepared.executionPlan.commandInfo.subcommand) ||
265
278
  shouldCaptureSemanticActionNavigationSummary(prepared.compiledSemanticAction, presentationEnvelope?.data) ||
266
279
  commandRequiresLivePageVerification(prepared.executionPlan.effectiveArgs, prepared.runtimeToolStdin) ||
267
- tabTransition)) {
280
+ (destinationTransition && !nestedBatchClosed) || tabTransition)) {
268
281
  navigationSummary = await collectNavigationSummary({ cwd, namespace: prepared.executionPlan.namespace, priorTarget: prepared.priorSessionTabTarget, reusePriorTitle: !tabTransition, sessionName: prepared.executionPlan.sessionName, signal });
269
282
  }
270
- // A failed eval/back/forward/reload/state-load/tab would otherwise drop the page to unverified and force the
271
- // agent through a manual get url round trip. Probe the live URL ourselves instead: an observed page
272
- // stays verified, and a failed
273
- // probe preserves the existing unknown-target behavior.
283
+ // Failed transitions may already have changed the page; keep only a live observed URL.
274
284
  if (succeeded === false &&
275
- !navigationSummary &&
276
285
  processResult.agentBrowserStarted &&
277
286
  !processResult.aborted &&
278
287
  !processResult.timedOut &&
279
- prepared.executionPlan.commandInfo.command !== "batch" &&
280
- isUnverifiedPageTransitionCommand(prepared.executionPlan.commandInfo.command, prepared.executionPlan.commandInfo.subcommand)) {
288
+ !nestedBatchClosed &&
289
+ (destinationTransition || (prepared.executionPlan.commandInfo.command !== "batch" &&
290
+ isUnverifiedPageTransitionCommand(prepared.executionPlan.commandInfo.command, prepared.executionPlan.commandInfo.subcommand)))) {
281
291
  navigationSummary = await collectNavigationSummary({ cwd, namespace: prepared.executionPlan.namespace, priorTarget: prepared.priorSessionTabTarget, sessionName: prepared.executionPlan.sessionName, signal });
282
- // A failed transition command can still have mutated or replaced the document before throwing, so
283
- // keeping the verified URL must not keep the prior snapshot refs (markTabTargetUnknown dropped
284
- // them before this probe existed); invalidate so the next ref use requires a fresh snapshot.
292
+ // Re-verifying a failed transition's URL does not make its prior refs valid.
285
293
  failedTransitionReverification = navigationSummary !== undefined;
286
294
  }
287
295
  if (navigationSummary && presentationEnvelope && prepared.executionPlan.commandInfo.command !== "eval" && !Array.isArray(presentationEnvelope.data))
@@ -309,8 +317,9 @@ export async function processBrowserOutput(input) {
309
317
  const safeObservedSessionTabTarget = observedSessionTabTarget;
310
318
  let currentSessionTabTarget = safeObservedSessionTabTarget;
311
319
  if (!currentSessionTabTarget && nestedBatchClose === undefined) {
320
+ // Window/diff responses do not report the final URL; URL2 is intent, not redirect evidence.
312
321
  currentSessionTabTarget = resultingPageState.pageTargetMayHaveChanged
313
- ? succeeded ? normalizeSessionTabTarget({ url: resultingPageState.currentPageUrl }) : undefined
322
+ ? succeeded && !destinationTransition ? normalizeSessionTabTarget({ url: resultingPageState.currentPageUrl }) : undefined
314
323
  : deriveSessionTabTarget({ command: prepared.executionPlan.commandInfo.command, data: presentationEnvelope?.data, navigationSummary, previousTarget: prepared.priorSessionTabTarget, subcommand: prepared.executionPlan.commandInfo.subcommand });
315
324
  }
316
325
  let aboutBlankSessionMismatch;
@@ -318,7 +327,8 @@ export async function processBrowserOutput(input) {
318
327
  let electronRefFreshnessDiagnostic;
319
328
  let electronSessionMismatch;
320
329
  let electronStatusAfterCommand;
321
- const shouldTreatAboutBlankAsMismatch = succeeded && !tabTransition && nestedBatchClose === undefined && prepared.priorSessionTabTarget !== undefined && !isAboutBlankSessionTabTarget(prepared.priorSessionTabTarget) && isAboutBlankSessionTabTarget(observedSessionTabTarget ?? currentSessionTabTarget) && !commandExplicitlyTargetsAboutBlank(prepared.commandTokens);
330
+ const explicitlyTargetsAboutBlank = dispatchedCommands.some((step) => commandExplicitlyTargetsAboutBlank(extractUpstreamCommandTokens(step)));
331
+ const shouldTreatAboutBlankAsMismatch = succeeded && !tabTransition && !destinationTransition && !explicitlyTargetsAboutBlank && nestedBatchClose === undefined && prepared.priorSessionTabTarget !== undefined && !isAboutBlankSessionTabTarget(prepared.priorSessionTabTarget) && isAboutBlankSessionTabTarget(observedSessionTabTarget ?? currentSessionTabTarget);
322
332
  let sessionTabCorrection = prepared.sessionTabCorrection;
323
333
  if (shouldTreatAboutBlankAsMismatch && prepared.priorSessionTabTarget) {
324
334
  const aboutBlankObservedTarget = observedSessionTabTarget ?? currentSessionTabTarget;
@@ -536,7 +546,7 @@ export async function processBrowserOutput(input) {
536
546
  if (sessionStateKey && succeeded) {
537
547
  if (openResultTabCorrection || sessionTabCorrection || aboutBlankSessionMismatch?.recoveryApplied)
538
548
  sessionPageState.markPinning(sessionStateKey, "drift");
539
- else if (prepared.sessionTabPinningReason === "restore")
549
+ else if (prepared.sessionTabPinningReason === "restore" && observedSessionTabTarget)
540
550
  sessionPageState.clearRestorePinning(sessionStateKey);
541
551
  }
542
552
  if (replacedManagedSessionName) {
@@ -626,14 +636,25 @@ export async function processBrowserOutput(input) {
626
636
  electronLaunchRecord = electronFailedConnectCleanup.record;
627
637
  }
628
638
  }
629
- 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({ pinnedBatchUnwrapMode: prepared.pinnedBatchUnwrapMode, sessionTabCorrection }) });
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 }) });
640
+ if (errorText && presentationEnvelope?.success === false && extractEnvelopeErrorText(presentationEnvelope.error) === undefined)
641
+ presentationEnvelope = { ...presentationEnvelope, error: errorText };
630
642
  if (errorText) {
631
643
  const clipboardWritePayloadCandidates = getClipboardWritePayloadCandidates(prepared.commandTokens);
632
644
  errorText = redactClipboardPermissionEcho(prepared.executionPlan.commandInfo, errorText);
633
645
  if (presentationEnvelope?.error !== undefined)
634
646
  presentationEnvelope = { ...presentationEnvelope, error: redactClipboardPermissionErrorValue(prepared.executionPlan.commandInfo, presentationEnvelope.error, clipboardWritePayloadCandidates) };
635
647
  }
636
- const presentation = plainTextInspection ? { artifacts: undefined, batchFailure: undefined, batchSteps: undefined, content: [{ type: "text", text: inspectionText ?? "" }], data: undefined, fullOutputPath: undefined, fullOutputPaths: undefined, imagePath: undefined, imagePaths: undefined, savedFile: undefined, savedFilePath: undefined, summary: `${prepared.redactedArgs.join(" ")} completed` } : await buildToolPresentation({ args: prepared.redactedProcessArgs, artifactManifest, artifactMaxUpdatedAtMs: Date.now(), artifactMinUpdatedAtMs: input.artifactRunStartedAtMs, artifactRequest: screenshotArtifactRequest, batchArtifactRequests: batchScreenshotArtifactRequests, commandInfo: prepared.executionPlan.commandInfo, compiledSemanticAction: prepared.compiledSemanticAction, cwd, envelope: presentationEnvelope, errorText, namespace: prepared.executionPlan.namespace, networkRouteDiagnostics, networkRoutes: activeNetworkRoutes, persistentArtifactStore, sessionName: prepared.executionPlan.sessionName });
648
+ if (plainTextUpgrade && errorText)
649
+ 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 });
651
+ if (plainTextUpgrade && !succeeded && typeof presentationEnvelope?.data === "string") {
652
+ presentation.data = presentationEnvelope.data;
653
+ const errorContent = presentation.content[0];
654
+ if (errorContent?.type === "text" && presentationEnvelope.data)
655
+ errorContent.text += `\n\n${presentationEnvelope.data}`;
656
+ presentation = await compactLargePresentationOutput({ artifactManifest, commandInfo: prepared.executionPlan.commandInfo, data: presentation.data, persistentArtifactStore, presentation });
657
+ }
637
658
  if (electronHandoff?.error && electronHandoff.failureCategory)
638
659
  presentation.failureCategory = electronHandoff.failureCategory;
639
660
  networkRoutesBySession = applyBatchNetworkRouteState({ data: presentationEnvelope?.data, routesBySession: networkRoutesBySession, sessionName: sessionStateKey, succeeded });
@@ -738,7 +759,14 @@ export async function processBrowserOutput(input) {
738
759
  const evalResultWarning = getEvalResultWarning({ command: prepared.executionPlan.commandInfo.command, data: presentationEnvelope?.data, navigationSummary: evalNavigationSummary, pageUrl: evalPageUrl, stdin: prepared.runtimeToolStdin });
739
760
  const resultArtifactManifest = presentation.artifactManifest ?? artifactManifest;
740
761
  const artifactCleanup = await getArtifactCleanupGuidance({ command: prepared.executionPlan.commandInfo.command, cwd, manifest: resultArtifactManifest, succeeded });
741
- const warningText = electronPostCommandHealth ? formatElectronPostCommandHealthText(electronPostCommandHealth) : electronSessionMismatch ? formatElectronSessionMismatchText(electronSessionMismatch) : aboutBlankSessionMismatch ? buildAboutBlankWarning(aboutBlankSessionMismatch) : undefined;
762
+ const recordingTransitionReached = prepared.executionPlan.commandInfo.command === "batch"
763
+ ? presentation.batchSteps?.some((step) => isRecordPageTransitionCommand(extractUpstreamCommandTokens(step.command ?? [])))
764
+ : isRecordPageTransitionCommand(prepared.commandTokens);
765
+ const recordingPageWarning = processResult.agentBrowserStarted && !prepared.executionPlan.plainTextInspection && recordingTransitionReached
766
+ ? "Page state: this recording command can replace or navigate the active page, even on failure; prior in-page DOM and JavaScript state may not carry over. Take a fresh snapshot before continuing with page-scoped refs."
767
+ : undefined;
768
+ const sessionWarning = electronPostCommandHealth ? formatElectronPostCommandHealthText(electronPostCommandHealth) : electronSessionMismatch ? formatElectronSessionMismatchText(electronSessionMismatch) : aboutBlankSessionMismatch ? buildAboutBlankWarning(aboutBlankSessionMismatch) : undefined;
769
+ const warningText = [sessionWarning, recordingPageWarning].filter(Boolean).join("\n\n") || undefined;
742
770
  const redactedContent = buildRedactedPresentationContent({ exactSensitiveValues: prepared.exactSensitiveValues, plainTextInspection, presentation, presentationEnvelope, succeeded, userRequestedJson: prepared.userRequestedJson, warningText });
743
771
  const finalRecoveryState = await prepareFinalResultRecoveryState({ aboutBlankSessionMismatch, batchRefSnapshotState, commandTokens: prepared.commandTokens, compiledSemanticAction: prepared.compiledSemanticAction, currentRefSnapshot, currentRefSnapshotInvalidation, currentSessionTabTarget, cwd, electronPostCommandHealth, errorText, executionPlan: prepared.executionPlan, parseError, plainTextInspection, presentation, processResult, redactedProcessArgs: prepared.redactedProcessArgs, runtimeToolArgs: prepared.runtimeToolArgs, sessionPageState, sessionPageStateUpdate, sessionTabCorrection, signal, succeeded });
744
772
  currentRefSnapshot = finalRecoveryState.currentRefSnapshot;
@@ -1,15 +1,17 @@
1
1
  import { rm } from "node:fs/promises";
2
- import { getAgentBrowserSessionIdentityKey, VALUE_FLAGS } from "../../argv-grammar.js";
2
+ import { getAgentBrowserSessionIdentityKey } from "../../argv-grammar.js";
3
+ import { parseArgvDescriptor } from "../../argv-descriptor.js";
4
+ import { needsManagedSession } from "../../command-policy.js";
3
5
  import { runAgentBrowserProcess } from "../../process.js";
4
6
  import { buildAgentBrowserNextActions } from "../../results/action-recommendations.js";
5
7
  import { parseAgentBrowserEnvelope } from "../../results/envelope.js";
6
8
  import { buildNextToolAction, withOptionalNamespaceArgs, withOptionalSessionArgs } from "../../results/next-actions.js";
7
- import { getSessionPageStateKey, isAboutBlankUrl, normalizeComparableUrl, normalizeSessionTabTarget, targetsMatch, } from "../../session-page-state.js";
8
- import { isCloseCommand, isElectronPostCommandHealthCommand, isNavigationObservableCommandName, isRefGuardedCommand, isRefInvalidatingBatchCommand, isSessionTabPinningExcludedCommand, isSessionTabPostCommandCorrectionExcludedCommand, } from "../../command-taxonomy.js";
9
+ import { getSessionPageStateKey, isAboutBlankUrl, normalizeComparableUrl, targetsMatch, } from "../../session-page-state.js";
10
+ import { isCloseCommand, isElectronPostCommandHealthCommand, isNavigationObservableCommandName, isOpenNavigationCommand, isRefGuardedCommand, isRefInvalidatingBatchCommand, isRecordPageTransitionCommand, isSessionTabPinningExcludedCommand, isSessionTabPostCommandCorrectionExcludedCommand, isWindowOrDiffPageTransitionCommand, } from "../../command-taxonomy.js";
9
11
  import { chooseOpenResultTabCorrection } from "../../runtime.js";
10
- import { isRecord } from "../../parsing.js";
11
- import { getUpstreamEffectiveBatchSteps, parseUserBatchStdin } from "../batch-stdin.js";
12
- export const NAVIGATION_SUMMARY_EVAL = `({ title: document.title, url: location.href })`;
12
+ import { isRecord, parseRefId } from "../../parsing.js";
13
+ import { getUpstreamEffectiveBatchSteps } from "../batch-stdin.js";
14
+ import { findFirstPositionalArgument } from "./prepare/wait-timeouts.js";
13
15
  export function applyBrowserRunStatePatch(state, patch) {
14
16
  if (!patch)
15
17
  return;
@@ -278,39 +280,57 @@ export function getStaleRefArgs(commandTokens, stdin) {
278
280
  const steps = getUpstreamEffectiveBatchSteps(commandTokens, stdin);
279
281
  return steps.length > 0 ? steps.flatMap((step) => step) : commandTokens;
280
282
  }
281
- // Selector-carrying flags whose values remain page-scoped refs; values of other value-taking flags
282
- // (--text, --baseline, --name, and similar) are literal text or paths that may merely look like refs,
283
- // per upstream parsing. Boolean flags such as --new-tab or --full do not consume the following token,
284
- // so a ref after them is a genuine positional selector and must stay guarded.
285
- const SELECTOR_FLAG_TOKENS = new Set(["--selector", "-s"]);
286
- const SHORT_VALUE_FLAG_TOKENS = new Set(["-b", "-o", "-t"]);
287
- function isNonSelectorValueFlagToken(token) {
288
- if (token === undefined || SELECTOR_FLAG_TOKENS.has(token))
289
- return false;
290
- return VALUE_FLAGS.has(token) || SHORT_VALUE_FLAG_TOKENS.has(token);
291
- }
283
+ // Inspect only upstream's selector slots: fill text, file paths and key data are not refs.
292
284
  function collectRefsFromTokens(tokens) {
293
- const refs = [];
294
- for (const [index, token] of tokens.entries()) {
295
- if (!/^@e\d+\b/.test(token))
296
- continue;
297
- if (isNonSelectorValueFlagToken(index > 0 ? tokens[index - 1] : undefined))
298
- continue;
299
- refs.push(token.slice(1));
285
+ if (!isRefGuardedCommand(tokens[0]) || (tokens[0] === "diff" && tokens[1] !== "screenshot"))
286
+ return [];
287
+ let selectors;
288
+ switch (tokens[0]) {
289
+ case "click":
290
+ selectors = [tokens.slice(1).find((token) => token !== "--new-tab")];
291
+ break;
292
+ case "drag":
293
+ selectors = tokens.slice(1, 3);
294
+ break;
295
+ case "get":
296
+ selectors = [!["url", "title", "cdp-url", "count"].includes(tokens[1]) ? tokens[2] : undefined];
297
+ break;
298
+ case "is":
299
+ selectors = [tokens[2]];
300
+ break;
301
+ case "screenshot":
302
+ selectors = [tokens.slice(1).find((token) => !["--full", "-f"].includes(token))];
303
+ break;
304
+ case "diff":
305
+ case "scroll": {
306
+ let selector;
307
+ for (let index = tokens[0] === "diff" ? 2 : 1; index < tokens.length; index += 1) {
308
+ const token = tokens[index];
309
+ if (token === "--selector" || token === "-s")
310
+ selector = tokens[++index];
311
+ else if (tokens[0] === "diff" && ["--baseline", "-b", "--output", "-o", "--threshold", "-t", "--depth", "-d"].includes(token))
312
+ index += 1;
313
+ }
314
+ selectors = [selector];
315
+ break;
316
+ }
317
+ default: selectors = [tokens[1]];
300
318
  }
301
- return refs;
319
+ return selectors.flatMap((selector) => {
320
+ const ref = selector === undefined ? undefined : parseRefId(selector);
321
+ return ref === undefined ? [] : [ref];
322
+ });
302
323
  }
303
324
  export function getGuardedRefUsage(commandTokens, stdin, options = {}) {
304
- const collectFromStep = (step) => isRefGuardedCommand(step[0]) ? collectRefsFromTokens(step) : [];
305
325
  if (commandTokens[0] !== "batch") {
306
- return collectFromStep(commandTokens);
326
+ return collectRefsFromTokens(commandTokens);
307
327
  }
308
328
  const steps = getUpstreamEffectiveBatchSteps(commandTokens, stdin);
309
329
  const refsBeforeInBatchSnapshot = [];
310
330
  for (const step of steps) {
311
331
  if (!options.includeRefsAfterBatchSnapshot && (step[0] ?? "") === "snapshot")
312
332
  break;
313
- refsBeforeInBatchSnapshot.push(...collectFromStep(step));
333
+ refsBeforeInBatchSnapshot.push(...collectRefsFromTokens(step));
314
334
  }
315
335
  return refsBeforeInBatchSnapshot;
316
336
  }
@@ -395,61 +415,41 @@ export function buildStaleRefPreflight(options) {
395
415
  }
396
416
  return undefined;
397
417
  }
398
- function supportsPinnedStdinCommand(options) {
399
- if (options.command === "batch") {
400
- return options.stdin !== undefined;
401
- }
402
- if (options.stdin === undefined) {
403
- return true;
404
- }
405
- if (options.command === "eval") {
406
- return options.commandTokens.includes("--stdin");
407
- }
408
- return false;
418
+ export function commandChoosesSessionTabTarget(args) {
419
+ const tokens = parseArgvDescriptor(args).upstreamCommandTokens;
420
+ const [command, subcommand] = tokens;
421
+ return isOpenNavigationCommand(command) || isCloseCommand(command) || command === "connect"
422
+ || (command === "state" && subcommand === "load")
423
+ || (command === "tab" && subcommand !== undefined && subcommand !== "list")
424
+ || isWindowOrDiffPageTransitionCommand(command, subcommand)
425
+ || (command === "a11y" && findFirstPositionalArgument(tokens) !== undefined)
426
+ || (["vitals", "web-vitals"].includes(command) && tokens.slice(1).some((token) => !token.startsWith("--")))
427
+ || (isRecordPageTransitionCommand(tokens) && tokens[3] !== undefined);
409
428
  }
410
429
  export function shouldPinSessionTabForCommand(options) {
411
- return (options.pinningRequired === true &&
412
- options.sessionName !== undefined &&
413
- options.command !== undefined &&
414
- !isSessionTabPinningExcludedCommand(options.command) &&
415
- supportsPinnedStdinCommand(options));
416
- }
417
- export function buildPinnedBatchPlan(options) {
418
- if (options.command === "batch") {
419
- const tabSelectionStep = ["tab", options.selectedTab];
420
- // Upstream executes raw argument steps exclusively when any exist, so the
421
- // pinned rewrite must dispatch those same steps rather than resurrecting
422
- // caller stdin the guard never scanned.
423
- const argumentSteps = getUpstreamEffectiveBatchSteps(options.commandTokens, undefined);
424
- if (argumentSteps.length > 0) {
425
- return {
426
- includeNavigationSummary: false,
427
- steps: [tabSelectionStep, ...argumentSteps],
428
- unwrapMode: "user-batch",
429
- };
430
- }
431
- const parsed = parseUserBatchStdin(options.stdin);
432
- if (parsed.error) {
433
- return { error: parsed.error };
434
- }
435
- return {
436
- includeNavigationSummary: false,
437
- steps: [tabSelectionStep, ...(parsed.steps ?? [])],
438
- unwrapMode: "user-batch",
439
- };
440
- }
441
- if (options.commandTokens.length === 0) {
442
- return undefined;
430
+ if (!options.pinningRequired || !options.sessionName || !options.command)
431
+ return false;
432
+ const steps = options.command === "batch" ? getUpstreamEffectiveBatchSteps(options.commandTokens, options.stdin) : [options.commandTokens];
433
+ for (const step of steps) {
434
+ const descriptor = parseArgvDescriptor(step);
435
+ const tokens = descriptor.upstreamCommandTokens;
436
+ const [command, subcommand] = tokens;
437
+ // A later page action uses the explicit destination, not the remembered tab.
438
+ if (commandChoosesSessionTabTarget(tokens))
439
+ return false;
440
+ if (!needsManagedSession(descriptor) || isSessionTabPinningExcludedCommand(command))
441
+ continue;
442
+ if (command === "get" && subcommand === "url" && !options.reopenPending)
443
+ continue;
444
+ if (command === "read" && findFirstPositionalArgument(tokens) !== undefined)
445
+ continue;
446
+ if (["console", "errors"].includes(command))
447
+ continue;
448
+ if (command === "network" && !(subcommand === "requests" && tokens.some((token) => ["--current-page", "--current-origin", "--current-url"].includes(token))))
449
+ continue;
450
+ return true;
443
451
  }
444
- const includeNavigationSummary = isNavigationObservableCommandName(options.command, options.commandTokens[1]);
445
- const tabSelectionStep = ["tab", options.selectedTab];
446
- const commandStep = options.commandTokens;
447
- const navigationSummarySteps = includeNavigationSummary ? [["eval", NAVIGATION_SUMMARY_EVAL]] : [];
448
- return {
449
- includeNavigationSummary,
450
- steps: [tabSelectionStep, commandStep, ...navigationSummarySteps],
451
- unwrapMode: "single-command",
452
- };
452
+ return false;
453
453
  }
454
454
  export function shouldCorrectSessionTabAfterCommand(options) {
455
455
  return (options.pinningRequired === true &&
@@ -477,59 +477,11 @@ function selectAnySessionTargetTab(options) {
477
477
  return undefined;
478
478
  const matchingTabs = options.tabs.filter((tab) => normalizeComparableUrl(tab.url ?? "") === targetUrl);
479
479
  const targetTitle = options.target.title?.trim() ?? "";
480
- const selectedTab = (targetTitle ? matchingTabs.find((tab) => tab.title?.trim() === targetTitle) : undefined) ?? matchingTabs[0];
480
+ const titledTabs = targetTitle ? matchingTabs.filter((tab) => tab.title?.trim() === targetTitle) : [];
481
+ const selectedTab = titledTabs.find((tab) => tab.active) ?? titledTabs[0] ?? matchingTabs.find((tab) => tab.active) ?? matchingTabs[0];
481
482
  const selection = selectedTab ? getTabSelection(selectedTab) : undefined;
482
483
  return selection ? { ...selection, ...(targetTitle ? { targetTitle } : {}), targetUrl } : undefined;
483
484
  }
484
- export function unwrapPinnedSessionBatchEnvelope(options) {
485
- if (!options.envelope) {
486
- return {};
487
- }
488
- if (!Array.isArray(options.envelope.data)) {
489
- return {
490
- parseError: "agent-browser returned an unexpected response while applying the wrapper's tab-pinning batch.",
491
- };
492
- }
493
- const steps = options.envelope.data.filter(isRecord);
494
- const tabSelectionStep = steps[0];
495
- const commandStep = steps[1];
496
- if (tabSelectionStep?.success === false) {
497
- return {
498
- envelope: {
499
- success: false,
500
- error: tabSelectionStep.error ?? "agent-browser could not re-select the intended tab before running the command.",
501
- },
502
- };
503
- }
504
- if (options.mode === "user-batch") {
505
- const userSteps = steps.slice(1);
506
- return {
507
- envelope: {
508
- success: userSteps.every((step) => step.success !== false),
509
- data: userSteps,
510
- error: userSteps.find((step) => step.success === false)?.error,
511
- },
512
- };
513
- }
514
- if (!commandStep) {
515
- return {
516
- envelope: {
517
- success: false,
518
- error: "agent-browser did not return the corrected command result.",
519
- },
520
- };
521
- }
522
- const navigationSummaryStep = options.includeNavigationSummary ? steps[2] : undefined;
523
- const navigationSummary = normalizeSessionTabTarget(extractNavigationSummaryFromData(navigationSummaryStep?.result));
524
- return {
525
- envelope: {
526
- success: commandStep.success !== false,
527
- data: commandStep.result,
528
- error: commandStep.success === false ? commandStep.error : undefined,
529
- },
530
- navigationSummary,
531
- };
532
- }
533
485
  export async function runSessionCommandData(options) {
534
486
  const { args, cwd, namespace, pinNamespace, sessionName, signal, stdin, throwOnFailure, timeoutMs } = options;
535
487
  if (!sessionName)
@@ -542,6 +494,7 @@ export async function runSessionCommandData(options) {
542
494
  timeoutMs,
543
495
  });
544
496
  try {
497
+ options.onProcessResult?.(processResult);
545
498
  if (processResult.aborted || processResult.spawnError || processResult.exitCode !== 0) {
546
499
  if (throwOnFailure) {
547
500
  const reason = processResult.aborted
@@ -604,11 +557,23 @@ export async function collectSessionTabSelection(options) {
604
557
  const tabs = mapTabData(tabData);
605
558
  return tabs ? selectSessionTargetTab({ tabs, target }) : undefined;
606
559
  }
607
- export async function collectAnySessionTabSelection(options) {
608
- const { cwd, namespace, sessionName, signal, target } = options;
609
- const tabData = await runSessionCommandData({ args: ["tab", "list"], cwd, namespace, sessionName, signal });
610
- const tabs = mapTabData(tabData);
611
- return tabs ? selectAnySessionTargetTab({ tabs, target }) : undefined;
560
+ export async function ensureSessionTabTarget(options) {
561
+ const readTabs = async () => mapTabData(await runSessionCommandData({ ...options, args: ["tab", "list"] }));
562
+ const tabs = await readTabs();
563
+ const active = tabs?.find((tab) => tab.active);
564
+ const correction = tabs && selectAnySessionTargetTab({ tabs, target: options.target });
565
+ const error = "agent-browser could not re-select and verify the intended tab before running the command. Run tab list and select the intended tab, then snapshot -i before retrying.";
566
+ if (!correction)
567
+ return { error };
568
+ // Native tab selection clears refs and frame scope even when selecting the current tab.
569
+ if (active && getTabSelection(active)?.selectedTab === correction.selectedTab)
570
+ return {};
571
+ if (!await applyOpenResultTabCorrection({ ...options, correction }))
572
+ return { correction, error };
573
+ const selected = (await readTabs())?.find((tab) => tab.active);
574
+ return selected && getTabSelection(selected)?.selectedTab === correction.selectedTab && normalizeComparableUrl(selected.url ?? "") === normalizeComparableUrl(options.target.url)
575
+ ? { correction }
576
+ : { correction, error };
612
577
  }
613
578
  export async function applyOpenResultTabCorrection(options) {
614
579
  const { correction, cwd, namespace, sessionName, signal } = options;
@@ -174,7 +174,9 @@ function formatElectronStatusVisibleText(statuses, records, mismatches = [], man
174
174
  const sessionName = record?.sessionName;
175
175
  const appName = record?.appName ?? "Electron launch";
176
176
  const sessionText = sessionName ? `, sessionName ${sessionName}` : "";
177
- lines.push(`- ${status.launchId}: ${appName}${sessionText}; ${status.portAlive ? "debug port alive" : "debug port dead"}${status.pidAlive === undefined ? "" : status.pidAlive ? ", pid alive" : ", pid dead"} (port ${status.port})`);
177
+ const historyText = status.cleanupState === "cleaned" ? "; historical cleaned launch record" : "";
178
+ lines.push(`- ${status.launchId}: ${appName}${sessionText}${historyText}; ${status.portAlive ? "debug port alive" : "debug port dead"}${status.pidAlive === undefined ? "" : status.pidAlive ? ", pid alive" : ", pid dead"} (port ${status.port})`);
179
+ lines.push(` Tracked profile path: ${status.userDataDirState}.`);
178
180
  lines.push(` Identifiers: launchId ${status.launchId}; sessionName ${sessionName ?? "not attached"}.`);
179
181
  for (const targetLine of formatElectronTargetLines(status.targets, 4))
180
182
  lines.push(` ${targetLine}`);
@@ -114,7 +114,9 @@ export function resolveAgentBrowserInput(options) {
114
114
  const timeoutMsError = params.timeoutMs !== undefined && (typeof params.timeoutMs !== "number" || !Number.isSafeInteger(params.timeoutMs) || params.timeoutMs <= 0)
115
115
  ? "timeoutMs must be a positive integer when provided."
116
116
  : compiledElectron && params.timeoutMs !== undefined
117
- ? "Use electron.timeoutMs for electron actions; top-level timeoutMs applies only to browser CLI subprocess calls."
117
+ ? compiledElectron.action === "list"
118
+ ? "electron.list has no configurable timeout; remove top-level timeoutMs."
119
+ : "Use electron.timeoutMs for this action; top-level timeoutMs applies only to browser CLI subprocess calls."
118
120
  : compiledScript && params.timeoutMs !== undefined && params.timeoutMs > AGENT_BROWSER_SCRIPT_MAX_TIMEOUT_MS
119
121
  ? `script timeoutMs must be ${AGENT_BROWSER_SCRIPT_MAX_TIMEOUT_MS} or less.`
120
122
  : undefined;
@@ -38,6 +38,10 @@ function getExplicitNavigationTarget(args) {
38
38
  return positionals[0];
39
39
  if (descriptor.commandInfo.command === "tab" && descriptor.commandInfo.subcommand === "new")
40
40
  return positionals[1];
41
+ if (descriptor.commandInfo.command === "window" && descriptor.commandInfo.subcommand === "new")
42
+ return "about:blank";
43
+ if (descriptor.commandInfo.command === "diff" && descriptor.commandInfo.subcommand === "url")
44
+ return descriptor.upstreamCommandTokens[3];
41
45
  return undefined;
42
46
  }
43
47
  function getResultingExplicitNavigationTarget(args, currentPageUrl) {
@@ -149,17 +153,11 @@ export function getResultingPageTargetState(options) {
149
153
  || isUnverifiedPageTransitionCommand(descriptor.commandInfo.command, descriptor.commandInfo.subcommand);
150
154
  return { ...getResultingPageState({ ...state, args: options.args, trustedBatchTabSelection: false }), pageTargetMayHaveChanged };
151
155
  }
152
- const batch = getBatchCommandSteps(options.args, options.stdin);
153
- if (batch.error)
154
- return { pageTargetMayHaveChanged: true, pageUrlUnknown: true };
155
- for (let index = 0; index < batch.steps.length; index += 1) {
156
- const step = batch.steps[index];
157
- const trustedBatchTabSelection = options.trustedFirstBatchTabSelection === true && index === 0;
156
+ for (const step of options.executedBatchSteps) {
158
157
  const stepDescriptor = parseArgvDescriptor(step);
159
158
  pageTargetMayHaveChanged ||= getExplicitNavigationTarget(step) !== undefined
160
- || (isUnverifiedPageTransitionCommand(stepDescriptor.commandInfo.command, stepDescriptor.commandInfo.subcommand)
161
- && !(trustedBatchTabSelection && stepDescriptor.commandInfo.command === "tab"));
162
- state = getResultingPageState({ ...state, args: step, trustedBatchTabSelection });
159
+ || isUnverifiedPageTransitionCommand(stepDescriptor.commandInfo.command, stepDescriptor.commandInfo.subcommand);
160
+ state = getResultingPageState({ ...state, args: step, trustedBatchTabSelection: false });
163
161
  }
164
162
  return { ...state, pageTargetMayHaveChanged };
165
163
  }
@@ -189,6 +187,9 @@ export function getPageTargetValidationError(options) {
189
187
  if (["close", "exit", "quit"].includes(command ?? ""))
190
188
  return undefined;
191
189
  if (command === "batch") {
190
+ if (descriptor.upstreamCommandTokens.slice(1).some((token) => token.startsWith("--bail="))) {
191
+ return "Use exact batch --bail for fail-fast, or omit it to continue after errors. --bail=<value> is a raw command upstream; stdin is ignored when raw batch arguments are present.";
192
+ }
192
193
  const batch = getBatchCommandSteps(options.args, options.stdin);
193
194
  if (batch.error)
194
195
  return batch.error.startsWith("agent_browser batch stdin") || batch.error === NESTED_BATCH_ARGUMENT_MESSAGE
@@ -262,7 +263,6 @@ export function getExplicitSessionPageVerificationRequirement(options) {
262
263
  pageUrlUnknown: true,
263
264
  stdin: options.stdin,
264
265
  allowUnverifiedPageTransitions: true,
265
- trustedFirstBatchTabSelection: options.trustedFirstBatchTabSelection,
266
266
  });
267
267
  return validationError === UNVERIFIED_PAGE_MESSAGE || validationError === BATCH_UNVERIFIED_PAGE_MESSAGE || validationError === NON_BAIL_BATCH_NAVIGATION_MESSAGE
268
268
  ? UNVERIFIED_PAGE_MESSAGE
@@ -1,6 +1,13 @@
1
1
  export function isRecord(value) {
2
2
  return typeof value === "object" && value !== null;
3
3
  }
4
+ /** Upstream native element::parse_ref accepts @eN, ref=eN and bare eN. */
5
+ export function parseRefId(selector) {
6
+ const trimmed = selector.trim();
7
+ const prefixed = trimmed.startsWith("@") || trimmed.startsWith("ref=");
8
+ const candidate = trimmed.startsWith("@") ? trimmed.slice(1) : trimmed.startsWith("ref=") ? trimmed.slice(4) : trimmed;
9
+ return (prefixed ? /^e\d*$/ : /^e\d+$/).test(candidate) ? candidate : undefined;
10
+ }
4
11
  export function parsePositiveInteger(rawValue) {
5
12
  if (typeof rawValue !== "string")
6
13
  return undefined;