pi-agent-browser-native 0.9.1 → 0.9.3
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 +26 -0
- package/README.md +35 -8
- package/dist/extensions/agent-browser/index.js +39 -16
- package/dist/extensions/agent-browser/lib/batch-lifecycle.js +6 -3
- package/dist/extensions/agent-browser/lib/browser-transcript.js +2 -1
- package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +4 -0
- package/dist/extensions/agent-browser/lib/native-output.js +2 -1
- package/dist/extensions/agent-browser/lib/orchestration/batch-stdin.js +2 -8
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.js +12 -2
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +62 -10
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +18 -12
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +33 -24
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +111 -53
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/screenshot-observation.js +6 -2
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +32 -20
- package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +37 -32
- package/dist/extensions/agent-browser/lib/orchestration/native-session-defaults.js +30 -11
- package/dist/extensions/agent-browser/lib/orchestration/output-file.js +3 -1
- package/dist/extensions/agent-browser/lib/page-target-validation.js +10 -3
- package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +2 -1
- package/dist/extensions/agent-browser/lib/playbook.js +7 -6
- package/dist/extensions/agent-browser/lib/process-environment.js +12 -3
- package/dist/extensions/agent-browser/lib/process.js +6 -6
- package/dist/extensions/agent-browser/lib/read-confirmation.js +56 -16
- package/dist/extensions/agent-browser/lib/results/confirmation.js +3 -0
- package/dist/extensions/agent-browser/lib/results/envelope.js +5 -1
- package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +10 -1
- package/dist/extensions/agent-browser/lib/results/presentation/common.js +1 -0
- package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +34 -1
- package/dist/extensions/agent-browser/lib/results/presentation/registry.js +5 -1
- package/dist/extensions/agent-browser/lib/results/presentation.js +15 -2
- package/dist/extensions/agent-browser/lib/results/selector-recovery.js +5 -5
- package/dist/extensions/agent-browser/lib/runtime.js +5 -1
- package/dist/extensions/agent-browser/lib/session-page-state.js +35 -16
- package/docs/ARCHITECTURE.md +10 -9
- package/docs/COMMAND_REFERENCE.md +37 -13
- package/docs/RELEASE.md +6 -5
- package/docs/REQUIREMENTS.md +3 -1
- package/docs/SESSION_STORAGE.md +1 -1
- package/docs/SUPPORT_MATRIX.md +12 -3
- package/docs/TOOL_CONTRACT.md +29 -20
- package/package.json +4 -4
|
@@ -2,7 +2,7 @@ import { rm } from "node:fs/promises";
|
|
|
2
2
|
import { observeNativeWebMcp } from "../../webmcp-observation.js";
|
|
3
3
|
import { parseArgvDescriptor } from "../../argv-descriptor.js";
|
|
4
4
|
import { isBrowserIndependentRead, needsManagedSession } from "../../command-policy.js";
|
|
5
|
-
import { deleteIdentityKeysInNamespace, getAgentBrowserSessionIdentityKey, isAgentBrowserSessionIdentityKeyInNamespace } from "../../argv-grammar.js";
|
|
5
|
+
import { deleteIdentityKeysInNamespace, getAgentBrowserSessionIdentityKey, getBooleanFlagValue, isAgentBrowserSessionIdentityKeyInNamespace } from "../../argv-grammar.js";
|
|
6
6
|
import { batchHasSuccessfulCloseAll, getSuccessfulBatchCloseLifecycle } from "../../batch-lifecycle.js";
|
|
7
7
|
import { isCloseAllCommand, isCloseCommand, isNavigationObservableCommandName, isOpenNavigationCommand, isPageMutationCommand, isRecordPageTransitionCommand, isUnverifiedPageTransitionCommand, isWindowOrDiffPageTransitionCommand } from "../../command-taxonomy.js";
|
|
8
8
|
import { OPEN_RESULT_TAB_CORRECTION_FLAGS } from "../../launch-scoped-flags.js";
|
|
@@ -20,7 +20,8 @@ import { getClipboardWritePayloadCandidates, redactClipboardPermissionEcho, reda
|
|
|
20
20
|
import { shouldCaptureSemanticActionNavigationSummary } from "../../results/presentation/semantic-action.js";
|
|
21
21
|
import { buildPageTransitionRefSnapshotInvalidation, commandExplicitlyTargetsAboutBlank, getCommandRefSnapshotInvalidation, deriveSessionTabTarget, extractLatestRefSnapshotStateFromBatchResults, extractRefSnapshotFromData, extractSessionTabTargetFromBatchResults, extractSessionTabTargetFromCommandData, isAboutBlankSessionTabTarget, normalizeSessionTabTarget, } from "../../session-page-state.js";
|
|
22
22
|
import { isRecord } from "../../parsing.js";
|
|
23
|
-
import {
|
|
23
|
+
import { getAgentBrowserProcessEnvironment } from "../../process-environment.js";
|
|
24
|
+
import { buildReadConfirmationNextActions, isBrowserIndependentConfirmation, isSuccessfulNativeConfirmedClose, nextReadConfirmation, parseReadConfirmation } from "../../read-confirmation.js";
|
|
24
25
|
import { pruneOwnedManagedSessionRestoreSnapshots } from "../../managed-session-restore.js";
|
|
25
26
|
import { isManagedSessionRestoreKey } from "../../managed-session-storage.js";
|
|
26
27
|
import { createFreshSessionName, extractUpstreamCommandTokens, redactInvocationArgs, resolveManagedSessionState } from "../../runtime.js";
|
|
@@ -32,6 +33,7 @@ import { buildScrollNoopDiagnostic, collectComboboxFocusDiagnostic, collectElect
|
|
|
32
33
|
import { repairScreenshotData } from "./prepare.js";
|
|
33
34
|
import { mergeRecordingRecoveryPresentation, recoverRecordingStop } from "./recording-recovery.js";
|
|
34
35
|
import { getPersistentSessionArtifactStore } from "./session-state.js";
|
|
36
|
+
import { nativePolicyDefinitelyUnestablished } from "./session-state.js";
|
|
35
37
|
import { buildFinalAgentBrowserToolResult, buildRedactedPresentationContent, buildWrapperRecoveryHint, prepareFinalResultRecoveryState, redactExactSensitiveValue, } from "./final-result.js";
|
|
36
38
|
async function repairScreenshotArtifact(options) {
|
|
37
39
|
const { cwd, envelope, request } = options;
|
|
@@ -137,7 +139,7 @@ function applyBatchNetworkRouteState(options) {
|
|
|
137
139
|
if (!isRecord(item) || !Array.isArray(item.command) || !item.command.every((token) => typeof token === "string"))
|
|
138
140
|
continue;
|
|
139
141
|
const commandTokens = extractUpstreamCommandTokens(item.command);
|
|
140
|
-
const stepSucceeded = item.success !== false;
|
|
142
|
+
const stepSucceeded = item.success !== false && !detectConfirmationRequired(item.result);
|
|
141
143
|
if (stepSucceeded && isCloseCommand(commandTokens[0]))
|
|
142
144
|
routes = undefined;
|
|
143
145
|
else
|
|
@@ -162,9 +164,10 @@ export async function processBrowserOutput(input) {
|
|
|
162
164
|
let networkRoutesBySession = state.networkRoutesBySession;
|
|
163
165
|
try {
|
|
164
166
|
const persistentArtifactStore = getPersistentSessionArtifactStore(ctx);
|
|
165
|
-
// Native upgrade prints text even with --json;
|
|
167
|
+
// Native upgrade prints text even with --json; explicit false selects opaque native text.
|
|
166
168
|
const plainTextUpgrade = !prepared.executionPlan.plainTextInspection && prepared.executionPlan.commandInfo.command === "upgrade" && !needsManagedSession(parseArgvDescriptor(prepared.runtimeToolArgs));
|
|
167
|
-
const
|
|
169
|
+
const textOutput = getBooleanFlagValue(prepared.processArgs, "--json") === false;
|
|
170
|
+
const parsed = await parseAgentBrowserEnvelope({ stdout: processResult.stdout, stdoutPath: processResult.stdoutSpillPath, plainText: plainTextUpgrade, textOutput });
|
|
168
171
|
observeNativeWebMcp(parsed.envelope?.data);
|
|
169
172
|
let parseError = parsed.parseError;
|
|
170
173
|
const recordingStopRecovery = await recoverRecordingStop({
|
|
@@ -194,22 +197,35 @@ export async function processBrowserOutput(input) {
|
|
|
194
197
|
const confirmationSessionName = prepared.executionPlan.sessionName ?? "default";
|
|
195
198
|
let readConfirmation = state.sessionPageState.getReadConfirmation(getAgentBrowserSessionIdentityKey(confirmationSessionName, prepared.executionPlan.namespace));
|
|
196
199
|
let readConfirmationEvent;
|
|
200
|
+
const confirmedEffects = [];
|
|
197
201
|
const confirmationRows = prepared.executionPlan.commandInfo.command === "batch" && Array.isArray(presentationEnvelope?.data)
|
|
198
|
-
? presentationEnvelope.data.flatMap((row, index) => isRecord(row) ? [{ tokens: batchCommandSteps[index] ?? [], data: row.result, response: row, succeeded: row.success === true }] : [])
|
|
199
|
-
: [{ tokens: prepared.commandTokens, data: presentationEnvelope?.data, response: presentationEnvelope, succeeded: presentationEnvelope?.success === true }];
|
|
202
|
+
? presentationEnvelope.data.flatMap((row, index) => isRecord(row) ? [{ tokens: Array.isArray(row.command) && row.command.every(token => typeof token === "string") ? row.command : batchCommandSteps[index] ?? [], data: row.result, index, response: row, succeeded: row.success === true }] : [])
|
|
203
|
+
: [{ tokens: prepared.commandTokens, data: presentationEnvelope?.data, index: 0, response: presentationEnvelope, succeeded: presentationEnvelope?.success === true }];
|
|
200
204
|
for (const row of confirmationRows) {
|
|
205
|
+
if (readConfirmation?.state === "pending" && readConfirmation.source === "native-guarded-action" && typeof readConfirmation.command === "string"
|
|
206
|
+
&& row.tokens.length === 2 && row.tokens[0] === "confirm" && row.tokens[1] === readConfirmation.id
|
|
207
|
+
&& isRecord(row.data) && row.data.confirmed === true && row.data.action === readConfirmation.action
|
|
208
|
+
&& isRecord(row.data.result) && !detectConfirmationRequired(row.data)) {
|
|
209
|
+
confirmedEffects.push({ command: readConfirmation.command, data: row.data.result.data, index: row.index, succeeded: row.succeeded && row.data.result.success === true });
|
|
210
|
+
}
|
|
201
211
|
const transition = nextReadConfirmation({ commandTokens: row.tokens, current: readConfirmation, data: row.data, namespace: prepared.executionPlan.namespace, sessionName: confirmationSessionName, succeeded: row.succeeded });
|
|
202
212
|
if (transition) {
|
|
203
213
|
readConfirmationEvent = transition;
|
|
204
214
|
readConfirmation = transition;
|
|
205
215
|
}
|
|
206
|
-
if (row.tokens.length === 2 && row.tokens[0] === "confirm" && isRecord(row.data) && row.data.confirmed === true &&
|
|
216
|
+
if (row.tokens.length === 2 && row.tokens[0] === "confirm" && isRecord(row.data) && row.data.confirmed === true && isRecord(row.data.result) && row.data.result.success === false && row.response) {
|
|
207
217
|
row.response.success = false;
|
|
208
218
|
row.response.error = row.data.result.error;
|
|
209
219
|
if (presentationEnvelope)
|
|
210
220
|
presentationEnvelope.success = false;
|
|
211
221
|
}
|
|
212
222
|
}
|
|
223
|
+
const confirmedData = prepared.commandTokens[0] === "confirm" ? confirmedEffects[0]?.data : undefined;
|
|
224
|
+
const confirmedCommand = prepared.commandTokens[0] === "confirm" ? confirmedEffects[0]?.command : undefined;
|
|
225
|
+
const directClose = isCloseCommand(prepared.executionPlan.commandInfo.command)
|
|
226
|
+
|| isSuccessfulNativeConfirmedClose(prepared.commandTokens, presentationEnvelope?.data);
|
|
227
|
+
if (readConfirmationEvent?.state === "pending")
|
|
228
|
+
state.observedBrowserEffects = { ...state.observedBrowserEffects, readConfirmation: readConfirmationEvent };
|
|
213
229
|
const destinationTransition = dispatchedCommands.some((step) => {
|
|
214
230
|
const [command, subcommand] = extractUpstreamCommandTokens(step);
|
|
215
231
|
return isWindowOrDiffPageTransitionCommand(command, subcommand);
|
|
@@ -222,24 +238,26 @@ export async function processBrowserOutput(input) {
|
|
|
222
238
|
const nestedBatchClosesAll = prepared.executionPlan.commandInfo.command === "batch"
|
|
223
239
|
&& batchHasSuccessfulCloseAll(presentationEnvelope?.data, batchCommandSteps);
|
|
224
240
|
const directCloseAllRequested = isCloseAllCommand(extractUpstreamCommandTokens(prepared.commandTokens));
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
? presentationEnvelope.data.statePath
|
|
241
|
+
const directCloseData = confirmedData ?? presentationEnvelope?.data;
|
|
242
|
+
const rawCloseStatePath = directClose && isRecord(directCloseData) && typeof directCloseData.statePath === "string"
|
|
243
|
+
? directCloseData.statePath
|
|
229
244
|
: nestedBatchClose?.statePath;
|
|
230
245
|
if (presentationEnvelope && prepared.exactSensitiveValues.length > 0)
|
|
231
246
|
presentationEnvelope = redactExactSensitiveValue(presentationEnvelope, prepared.exactSensitiveValues);
|
|
232
247
|
const parseFailureOutput = parseError && processResult.stdoutSpillPath
|
|
233
248
|
? { fullOutputUnavailable: "Malformed upstream output was discarded because it may contain sensitive browser data." }
|
|
234
249
|
: {};
|
|
235
|
-
const browserIndependentRead = prepared.readConfirmation
|
|
250
|
+
const browserIndependentRead = isBrowserIndependentConfirmation(prepared.readConfirmation) || isBrowserIndependentRead(prepared.commandTokens, prepared.runtimeToolStdin);
|
|
251
|
+
const nativeCommandMayHaveExecuted = !nativePolicyDefinitelyUnestablished(processResult, parsed.envelope, prepared.commandTokens);
|
|
252
|
+
if (state.confirmationPolicyMayBeEstablished !== true)
|
|
253
|
+
state.confirmationPolicyMayBeEstablished = nativeCommandMayHaveExecuted;
|
|
236
254
|
const processSucceeded = !processResult.timedOut && !processResult.aborted && !processResult.spawnError && processResult.exitCode === 0;
|
|
237
255
|
const plainTextInspection = prepared.executionPlan.plainTextInspection && processSucceeded;
|
|
238
256
|
const parseSucceeded = plainTextInspection || parseError === undefined;
|
|
239
257
|
if (isStreamEnableAlreadyEnabledNoop({ command: prepared.executionPlan.commandInfo.command, envelope: presentationEnvelope, processSucceeded, subcommand: prepared.executionPlan.commandInfo.subcommand })) {
|
|
240
258
|
presentationEnvelope = { success: true, data: { alreadyEnabled: true, enabled: true, message: getEnvelopeErrorString(presentationEnvelope) ?? "Stream already enabled" } };
|
|
241
259
|
}
|
|
242
|
-
const envelopeSuccess = plainTextInspection ? true : presentationEnvelope?.success !== false;
|
|
260
|
+
const envelopeSuccess = plainTextInspection ? true : presentationEnvelope?.success !== false && !detectConfirmationRequired(presentationEnvelope?.data);
|
|
243
261
|
let succeeded = (processSucceeded && parseSucceeded && envelopeSuccess) || recordingStopRecovery?.recovery.healed === true;
|
|
244
262
|
const inspectionText = plainTextInspection ? processResult.stdout.trim() : undefined;
|
|
245
263
|
const unobservedMutation = processResult.agentBrowserStarted && !plainTextInspection
|
|
@@ -272,6 +290,11 @@ export async function processBrowserOutput(input) {
|
|
|
272
290
|
networkRoutesBySession.delete(sessionStateKey);
|
|
273
291
|
sessionPageState.clearSession(sessionStateKey);
|
|
274
292
|
}
|
|
293
|
+
if (sessionStateKey && nestedBatchRemainsActive) {
|
|
294
|
+
const confirmActions = getAgentBrowserProcessEnvironment().AGENT_BROWSER_CONFIRM_ACTIONS;
|
|
295
|
+
if (confirmActions !== undefined)
|
|
296
|
+
sessionPageState.setConfirmActions(sessionStateKey, confirmActions);
|
|
297
|
+
}
|
|
275
298
|
if (prepared.executionPlan.commandInfo.command === "batch" && Array.isArray(presentationEnvelope?.data)) {
|
|
276
299
|
for (const [index, row] of presentationEnvelope.data.entries()) {
|
|
277
300
|
if (!isRecord(row))
|
|
@@ -282,11 +305,11 @@ export async function processBrowserOutput(input) {
|
|
|
282
305
|
if (!rowCommand)
|
|
283
306
|
continue;
|
|
284
307
|
const [command, subcommand] = extractUpstreamCommandTokens(rowCommand);
|
|
285
|
-
updateTraceOwnerState({ command, sessionName: sessionStateKey, subcommand, succeeded: row.success === true, traceOwners });
|
|
308
|
+
updateTraceOwnerState({ command, sessionName: sessionStateKey, subcommand, succeeded: row.success === true && !detectConfirmationRequired(row.result), traceOwners });
|
|
286
309
|
}
|
|
287
310
|
}
|
|
288
311
|
else {
|
|
289
|
-
updateTraceOwnerState({ command: prepared.executionPlan.commandInfo.command, sessionName: sessionStateKey, subcommand: prepared.executionPlan.commandInfo.subcommand, succeeded, traceOwners });
|
|
312
|
+
updateTraceOwnerState({ command: directClose ? "close" : prepared.executionPlan.commandInfo.command, sessionName: sessionStateKey, subcommand: prepared.executionPlan.commandInfo.subcommand, succeeded, traceOwners });
|
|
290
313
|
}
|
|
291
314
|
let clickDispatchDiagnostic;
|
|
292
315
|
if (succeeded && prepared.clickDispatchProbe) {
|
|
@@ -296,21 +319,22 @@ export async function processBrowserOutput(input) {
|
|
|
296
319
|
presentationEnvelope = { ...(presentationEnvelope ?? {}), error: clickDispatchDiagnostic.summary, success: false };
|
|
297
320
|
}
|
|
298
321
|
}
|
|
299
|
-
const tabTransition = dispatchedCommands.some((step) => {
|
|
322
|
+
const tabTransition = confirmedEffects.some(effect => effect.command === "tab") || dispatchedCommands.some((step) => {
|
|
300
323
|
const [command, subcommand] = extractUpstreamCommandTokens(step);
|
|
301
324
|
return command === "tab" && subcommand !== undefined && subcommand !== "list";
|
|
302
325
|
});
|
|
303
326
|
// Non-page rows (including a failed prefix) cannot retire a cold target for a navigation that never ran.
|
|
304
|
-
const resultingPageState = sessionPageState.get(sessionStateKey).tabReopenPending === true
|
|
327
|
+
const resultingPageState = !nativeCommandMayHaveExecuted || detectConfirmationRequired(presentationEnvelope?.data) || sessionPageState.get(sessionStateKey).tabReopenPending === true
|
|
305
328
|
? { currentPageUrl: prepared.priorSessionTabTarget?.url, pageTargetMayHaveChanged: false, pageUrlUnknown: prepared.priorSessionTabTargetUnknown === true }
|
|
306
329
|
: getResultingPageTargetState({
|
|
307
330
|
args: prepared.executionPlan.effectiveArgs,
|
|
308
331
|
executedBatchSteps: dispatchedCommands,
|
|
332
|
+
batchResults: presentationEnvelope?.data,
|
|
309
333
|
currentPageUrl: prepared.priorSessionTabTarget?.url,
|
|
310
334
|
pageUrlUnknown: prepared.priorSessionTabTargetUnknown === true,
|
|
311
335
|
});
|
|
312
|
-
if (succeeded && !nestedBatchClosed &&
|
|
313
|
-
(shouldCaptureNavigationSummary(prepared.executionPlan.commandInfo.command, presentationEnvelope?.data, prepared.executionPlan.commandInfo.subcommand) ||
|
|
336
|
+
if ((succeeded || processSucceeded && confirmedEffects.some(effect => effect.succeeded)) && readConfirmation?.state !== "pending" && !nestedBatchClosed && !directClose &&
|
|
337
|
+
(confirmedEffects.some(effect => shouldCaptureNavigationSummary(effect.command, effect.data)) || shouldCaptureNavigationSummary(prepared.executionPlan.commandInfo.command, presentationEnvelope?.data, prepared.executionPlan.commandInfo.subcommand) ||
|
|
314
338
|
(prepared.executionPlan.commandInfo.command === "batch" && dispatchedCommands.some(([command, subcommand]) => isNavigationObservableCommandName(command, subcommand))) ||
|
|
315
339
|
shouldCaptureSemanticActionNavigationSummary(prepared.compiledSemanticAction, presentationEnvelope?.data) ||
|
|
316
340
|
commandRequiresLivePageVerification(prepared.executionPlan.effectiveArgs, prepared.runtimeToolStdin) ||
|
|
@@ -318,21 +342,21 @@ export async function processBrowserOutput(input) {
|
|
|
318
342
|
navigationSummary = await collectNavigationSummary({ cwd, namespace: prepared.executionPlan.namespace, priorTarget: prepared.priorSessionTabTarget, reusePriorTitle: !tabTransition, sessionName: prepared.executionPlan.sessionName, signal });
|
|
319
343
|
}
|
|
320
344
|
// Failed transitions may already have changed the page; keep only a live observed URL.
|
|
321
|
-
if (succeeded === false &&
|
|
322
|
-
processResult.agentBrowserStarted &&
|
|
345
|
+
if (succeeded === false && navigationSummary === undefined && readConfirmation?.state !== "pending" &&
|
|
346
|
+
processResult.agentBrowserStarted && nativeCommandMayHaveExecuted &&
|
|
323
347
|
!processResult.aborted &&
|
|
324
348
|
!processResult.timedOut &&
|
|
325
|
-
!nestedBatchClosed &&
|
|
326
|
-
dispatchedCommands.some((step) => {
|
|
349
|
+
!nestedBatchClosed && !directClose &&
|
|
350
|
+
(confirmedEffects.some(effect => isUnverifiedPageTransitionCommand(effect.command)) || dispatchedCommands.some((step) => {
|
|
327
351
|
const [command, subcommand] = extractUpstreamCommandTokens(step);
|
|
328
352
|
return (prepared.executionPlan.commandInfo.command === "batch" && isOpenNavigationCommand(command))
|
|
329
353
|
|| isUnverifiedPageTransitionCommand(command, subcommand);
|
|
330
|
-
})) {
|
|
354
|
+
}))) {
|
|
331
355
|
navigationSummary = await collectNavigationSummary({ cwd, namespace: prepared.executionPlan.namespace, priorTarget: prepared.priorSessionTabTarget, sessionName: prepared.executionPlan.sessionName, signal });
|
|
332
356
|
// Re-verifying a failed transition's URL does not make its prior refs valid.
|
|
333
357
|
failedTransitionReverification = navigationSummary !== undefined;
|
|
334
358
|
}
|
|
335
|
-
if (navigationSummary && presentationEnvelope && prepared.executionPlan.commandInfo.command !== "eval" && !Array.isArray(presentationEnvelope.data))
|
|
359
|
+
if (!textOutput && navigationSummary && presentationEnvelope && prepared.executionPlan.commandInfo.command !== "eval" && !Array.isArray(presentationEnvelope.data))
|
|
336
360
|
presentationEnvelope = { ...presentationEnvelope, data: mergeNavigationSummaryIntoData(presentationEnvelope.data, navigationSummary) };
|
|
337
361
|
let overlayBlockerDiagnostic;
|
|
338
362
|
let openResultTabCorrection;
|
|
@@ -343,22 +367,32 @@ export async function processBrowserOutput(input) {
|
|
|
343
367
|
if (plannedTabCorrection)
|
|
344
368
|
openResultTabCorrection = await applyOpenResultTabCorrection({ correction: plannedTabCorrection, cwd, namespace: prepared.executionPlan.namespace, sessionName: prepared.executionPlan.sessionName, signal });
|
|
345
369
|
}
|
|
346
|
-
const verifiesCurrentUrl = prepared.executionPlan.commandInfo.command === "get" && prepared.executionPlan.commandInfo.subcommand === "url"
|
|
370
|
+
const verifiesCurrentUrl = prepared.executionPlan.commandInfo.command === "get" && prepared.executionPlan.commandInfo.subcommand === "url"
|
|
371
|
+
|| readConfirmationEvent?.state === "cleared" && prepared.readConfirmation?.source === "native-guarded-action" && extractSessionTabTargetFromCommandData(prepared.commandTokens, presentationEnvelope?.data) !== undefined;
|
|
347
372
|
const trustsReportedPageTarget = !resultingPageState.pageUrlUnknown || verifiesCurrentUrl;
|
|
348
373
|
const pendingWebMcpMutation = isPendingWebMcpMutation(prepared.executionPlan.commandInfo.command, prepared.executionPlan.commandInfo.subcommand, presentationEnvelope?.data) || (prepared.executionPlan.commandInfo.command === "batch" && batchHasPendingWebMcpMutation(presentationEnvelope?.data));
|
|
349
374
|
const failedWebMcpSettlement = prepared.priorSessionTabTargetUnknown === true && ((!succeeded && isWebMcpSettlementCommand(prepared.executionPlan.commandInfo.command, prepared.executionPlan.commandInfo.subcommand))
|
|
350
375
|
|| (prepared.executionPlan.commandInfo.command === "batch" && batchHasFailedWebMcpSettlement(presentationEnvelope?.data)));
|
|
351
376
|
const unsettledWebMcpMutation = pendingWebMcpMutation || failedWebMcpSettlement;
|
|
377
|
+
const helperConfirmation = parseReadConfirmation(state.observedBrowserEffects?.readConfirmation);
|
|
378
|
+
// Main pending installs this object above; only a helper can replace it during post-processing.
|
|
379
|
+
let confirmationFromHelper = state.observedBrowserEffects?.readConfirmation !== readConfirmationEvent;
|
|
380
|
+
if (confirmationFromHelper && helperConfirmation?.state === "pending") {
|
|
381
|
+
readConfirmationEvent = helperConfirmation;
|
|
382
|
+
readConfirmation = helperConfirmation;
|
|
383
|
+
succeeded = false;
|
|
384
|
+
}
|
|
385
|
+
const pageTargetData = textOutput ? undefined : presentationEnvelope?.data;
|
|
352
386
|
let observedSessionTabTarget = unsettledWebMcpMutation || (unobservedMutation && !failedTransitionReverification)
|
|
353
387
|
? undefined
|
|
354
388
|
: normalizeSessionTabTarget(navigationSummary)
|
|
355
|
-
?? (trustsReportedPageTarget ? extractSessionTabTargetFromBatchResults(
|
|
356
|
-
?? (succeeded && trustsReportedPageTarget ? extractSessionTabTargetFromCommandData(prepared.commandTokens,
|
|
357
|
-
if (observedSessionTabTarget && !browserIndependentRead && !nestedBatchClosed && !
|
|
389
|
+
?? (trustsReportedPageTarget ? extractSessionTabTargetFromBatchResults(pageTargetData) : undefined)
|
|
390
|
+
?? (succeeded && trustsReportedPageTarget ? extractSessionTabTargetFromCommandData(prepared.commandTokens, pageTargetData) : undefined);
|
|
391
|
+
if (observedSessionTabTarget && readConfirmation?.state !== "pending" && !browserIndependentRead && !nestedBatchClosed && !directClose) {
|
|
358
392
|
const reportedTarget = prepared.executionPlan.commandInfo.command === "batch"
|
|
359
|
-
? extractSessionTabTargetFromBatchResults(
|
|
393
|
+
? extractSessionTabTargetFromBatchResults(pageTargetData)
|
|
360
394
|
: prepared.commandTokens[0] === "tab" && prepared.commandTokens[1] === "close" ? undefined
|
|
361
|
-
: extractSessionTabTargetFromCommandData(prepared.commandTokens,
|
|
395
|
+
: extractSessionTabTargetFromCommandData(prepared.commandTokens, pageTargetData);
|
|
362
396
|
if (reportedTarget?.targetId)
|
|
363
397
|
observedSessionTabTarget.targetId = reportedTarget.targetId;
|
|
364
398
|
else
|
|
@@ -370,7 +404,7 @@ export async function processBrowserOutput(input) {
|
|
|
370
404
|
// Window/diff responses do not report the final URL; URL2 is intent, not redirect evidence.
|
|
371
405
|
currentSessionTabTarget = resultingPageState.pageTargetMayHaveChanged
|
|
372
406
|
? succeeded && !destinationTransition ? normalizeSessionTabTarget({ url: resultingPageState.currentPageUrl }) : undefined
|
|
373
|
-
: deriveSessionTabTarget({ command: prepared.executionPlan.commandInfo.command, data:
|
|
407
|
+
: deriveSessionTabTarget({ command: directClose ? "close" : prepared.executionPlan.commandInfo.command, data: pageTargetData, navigationSummary, previousTarget: prepared.priorSessionTabTarget, subcommand: prepared.executionPlan.commandInfo.subcommand });
|
|
374
408
|
}
|
|
375
409
|
let aboutBlankSessionMismatch;
|
|
376
410
|
let electronPostCommandHealth;
|
|
@@ -449,11 +483,14 @@ export async function processBrowserOutput(input) {
|
|
|
449
483
|
const comboboxFocusDiagnostic = succeeded ? await collectComboboxFocusDiagnostic({ command: prepared.executionPlan.commandInfo.command, commandTokens: prepared.commandTokens, cwd, namespace: prepared.executionPlan.namespace, semanticAction: prepared.compiledSemanticAction, sessionName: prepared.executionPlan.sessionName, signal }) : undefined;
|
|
450
484
|
const recordingDependencyWarning = await collectRecordingDependencyWarning({ command: prepared.executionPlan.commandInfo.command, commandTokens: prepared.commandTokens, succeeded });
|
|
451
485
|
const scrollNoopDiagnostic = succeeded && prepared.shouldProbeScrollNoop ? buildScrollNoopDiagnostic(prepared.scrollPositionBefore, await collectScrollPositionSnapshot({ cwd, namespace: prepared.executionPlan.namespace, sessionName: prepared.executionPlan.sessionName, signal })) : undefined;
|
|
452
|
-
const batchRefSnapshotState = prepared.executionPlan.commandInfo.command === "batch" ? extractLatestRefSnapshotStateFromBatchResults(presentationEnvelope?.data)
|
|
486
|
+
const batchRefSnapshotState = prepared.executionPlan.commandInfo.command === "batch" ? extractLatestRefSnapshotStateFromBatchResults(Array.isArray(presentationEnvelope?.data) ? presentationEnvelope.data.map((row, index) => {
|
|
487
|
+
const snapshot = confirmedEffects.find(effect => effect.index === index && effect.command === "snapshot");
|
|
488
|
+
return snapshot && isRecord(row) ? { ...row, command: ["snapshot"], result: snapshot.data, success: snapshot.succeeded } : row;
|
|
489
|
+
}) : presentationEnvelope?.data) : undefined;
|
|
453
490
|
let currentRefSnapshot;
|
|
454
491
|
let currentRefSnapshotInvalidation;
|
|
455
492
|
if (sessionStateKey && !browserIndependentRead) {
|
|
456
|
-
const sessionClosed = (
|
|
493
|
+
const sessionClosed = (directClose && succeeded) || nestedBatchClosed;
|
|
457
494
|
if (sessionClosed) {
|
|
458
495
|
state.attachedSessionKeys.delete(sessionStateKey);
|
|
459
496
|
networkRoutesBySession = new Map(networkRoutesBySession);
|
|
@@ -473,7 +510,7 @@ export async function processBrowserOutput(input) {
|
|
|
473
510
|
? buildPageTransitionRefSnapshotInvalidation("A dispatched mutation was interrupted without a final outcome. Verify the current URL and take a fresh snapshot before deciding whether to retry.")
|
|
474
511
|
: unsettledWebMcpMutation
|
|
475
512
|
? buildPageTransitionRefSnapshotInvalidation("A detached WebMCP invocation is still pending or failed to settle and can mutate, rerender, or navigate the page, so prior snapshot refs remain invalid after URL verification. Run webmcp result or cancel, then take a fresh snapshot before using page-scoped refs.")
|
|
476
|
-
: processResult.agentBrowserStarted && (directTransitionInvalidation || plannedBatchTransitionInvalidation)
|
|
513
|
+
: processResult.agentBrowserStarted && nativeCommandMayHaveExecuted && (directTransitionInvalidation || plannedBatchTransitionInvalidation)
|
|
477
514
|
? directTransitionInvalidation ?? plannedBatchTransitionInvalidation
|
|
478
515
|
: failedTransitionReverification
|
|
479
516
|
? buildPageTransitionRefSnapshotInvalidation("A failed page transition may still have changed the page, so the prior snapshot refs were invalidated. Run snapshot -i before using page-scoped refs.")
|
|
@@ -493,13 +530,13 @@ export async function processBrowserOutput(input) {
|
|
|
493
530
|
: prepared.executionPlan.commandInfo.command === "batch"
|
|
494
531
|
? batchRefSnapshotState?.snapshot
|
|
495
532
|
: succeeded
|
|
496
|
-
? prepared.executionPlan.commandInfo.command === "snapshot" ? extractRefSnapshotFromData(presentationEnvelope?.data) : prepared.resolvedSemanticActionRefSnapshot ?? overlayBlockerDiagnostic?.snapshot
|
|
533
|
+
? prepared.executionPlan.commandInfo.command === "snapshot" || confirmedCommand === "snapshot" ? extractRefSnapshotFromData(confirmedData ?? presentationEnvelope?.data) : prepared.resolvedSemanticActionRefSnapshot ?? overlayBlockerDiagnostic?.snapshot
|
|
497
534
|
: undefined;
|
|
498
535
|
const refreshArgs = batchRefSnapshotState?.refreshArgs ?? (succeeded && prepared.commandTokens[0] === "snapshot" && isRecord(presentationEnvelope?.data) && isRecord(presentationEnvelope.data.snapshot) && !refSnapshot
|
|
499
536
|
? prepared.commandTokens.filter((token) => token !== "--delta" && token !== "--full") : undefined);
|
|
500
537
|
// Native owns delta baselines. Read complete refs without advancing that baseline
|
|
501
538
|
// rather than maintaining a second revision/tree cache in the wrapper.
|
|
502
|
-
if (refreshArgs && !unsettledWebMcpMutation && !unobservedMutation) {
|
|
539
|
+
if (refreshArgs && readConfirmation?.state !== "pending" && !unsettledWebMcpMutation && !unobservedMutation) {
|
|
503
540
|
refSnapshot = extractRefSnapshotFromData(await runSessionCommandData({ args: refreshArgs, cwd, namespace: prepared.executionPlan.namespace, sessionName: prepared.executionPlan.sessionName, signal }));
|
|
504
541
|
if (!refSnapshot)
|
|
505
542
|
sessionPageState.applyRefSnapshotInvalidation({ invalidation: buildPageTransitionRefSnapshotInvalidation("The native snapshot delta did not include complete refs and the full ref read failed. Run snapshot -i before using refs."), sessionName: sessionStateKey, update: sessionPageStateUpdate });
|
|
@@ -534,8 +571,8 @@ export async function processBrowserOutput(input) {
|
|
|
534
571
|
&& nestedBatchRemainsActive
|
|
535
572
|
&& sessionStateKey === priorManagedSessionKey;
|
|
536
573
|
const closeAllClosesPriorManagedSession = closeAllTargetsPriorManagedSession && !closeAllRetainsPriorManagedSession;
|
|
537
|
-
const commandClosesSession =
|
|
538
|
-
const closeCommandSucceeded = (
|
|
574
|
+
const commandClosesSession = directClose || nestedBatchClosed || closeAllClosesPriorManagedSession;
|
|
575
|
+
const closeCommandSucceeded = (directClose && succeeded) || nestedBatchClosed || closeAllClosesPriorManagedSession;
|
|
539
576
|
const closeTargetsPriorManagedNamespace = prepared.executionPlan.namespace === priorManagedSessionNamespace;
|
|
540
577
|
const managedCloseSessionName = closeAllClosesPriorManagedSession
|
|
541
578
|
? priorManagedSessionName
|
|
@@ -613,11 +650,8 @@ export async function processBrowserOutput(input) {
|
|
|
613
650
|
}
|
|
614
651
|
if (replacedManagedSessionName) {
|
|
615
652
|
const replacedSessionStateKey = getSessionContextKey(replacedManagedSessionName, priorManagedSessionNamespace);
|
|
616
|
-
networkRoutesBySession = new Map(networkRoutesBySession);
|
|
617
|
-
networkRoutesBySession.delete(replacedSessionStateKey ?? replacedManagedSessionName);
|
|
618
|
-
sessionPageState.clearSession(replacedSessionStateKey ?? replacedManagedSessionName);
|
|
619
653
|
const replacedSessionKey = replacedSessionStateKey ?? replacedManagedSessionName;
|
|
620
|
-
const replacedCloseError = await closeManagedSession({ cwd: priorManagedSessionCwd, headedManagedAutosaveInterval: priorManagedSessionHeadedAutosaveInterval, namespace: priorManagedSessionNamespace, preserveAttachedBrowserSession: state.attachedSessionKeys.has(replacedSessionKey), restoreState: state.managedSessionRestoreState, sessionName: replacedManagedSessionName, socketDir: state.ownedManagedSessions.get(replacedSessionKey)?.socketDir, timeoutMs: implicitSessionCloseTimeoutMs });
|
|
654
|
+
const replacedCloseError = await closeManagedSession({ confirmActions: sessionPageState.get(replacedSessionKey).confirmActions ?? process.env.AGENT_BROWSER_CONFIRM_ACTIONS, cwd: priorManagedSessionCwd, headedManagedAutosaveInterval: priorManagedSessionHeadedAutosaveInterval, namespace: priorManagedSessionNamespace, preserveAttachedBrowserSession: state.attachedSessionKeys.has(replacedSessionKey), restoreState: state.managedSessionRestoreState, sessionName: replacedManagedSessionName, socketDir: state.ownedManagedSessions.get(replacedSessionKey)?.socketDir, timeoutMs: implicitSessionCloseTimeoutMs });
|
|
621
655
|
if (managedSessionOutcome) {
|
|
622
656
|
managedSessionOutcome = {
|
|
623
657
|
...managedSessionOutcome,
|
|
@@ -628,6 +662,9 @@ export async function processBrowserOutput(input) {
|
|
|
628
662
|
};
|
|
629
663
|
}
|
|
630
664
|
if (!replacedCloseError) {
|
|
665
|
+
networkRoutesBySession = new Map(networkRoutesBySession);
|
|
666
|
+
networkRoutesBySession.delete(replacedSessionKey);
|
|
667
|
+
sessionPageState.clearSession(replacedSessionKey);
|
|
631
668
|
state.attachedSessionKeys.delete(replacedSessionKey);
|
|
632
669
|
state.closedManagedSessionNames.add(replacedSessionKey);
|
|
633
670
|
}
|
|
@@ -654,7 +691,7 @@ export async function processBrowserOutput(input) {
|
|
|
654
691
|
if (electronHandoff.error) {
|
|
655
692
|
succeeded = false;
|
|
656
693
|
presentationEnvelope = { error: electronHandoff.error, success: false };
|
|
657
|
-
const closeError = await closeManagedSession({ cwd, headedManagedAutosaveInterval: prepared.ownedManagedSessionContext?.headedManagedAutosaveInterval, namespace: prepared.executionPlan.namespace, policyLock: prepared.managedSessionPolicyLock, preserveAttachedBrowserSession: input.preserveAttachedBrowserSession, restoreState: state.managedSessionRestoreState, sessionName: electronSessionName, timeoutMs: implicitSessionCloseTimeoutMs });
|
|
694
|
+
const closeError = await closeManagedSession({ confirmActions: sessionPageState.get(electronSessionStateKey).confirmActions ?? process.env.AGENT_BROWSER_CONFIRM_ACTIONS, cwd, headedManagedAutosaveInterval: prepared.ownedManagedSessionContext?.headedManagedAutosaveInterval, namespace: prepared.executionPlan.namespace, policyLock: prepared.managedSessionPolicyLock, preserveAttachedBrowserSession: input.preserveAttachedBrowserSession, restoreState: state.managedSessionRestoreState, sessionName: electronSessionName, timeoutMs: implicitSessionCloseTimeoutMs });
|
|
658
695
|
electronFailedConnectCleanup = await cleanupElectronLaunchResources({ child: prepared.electronLaunch.child, record: electronLaunchRecord, timeoutMs: implicitSessionCloseTimeoutMs });
|
|
659
696
|
electronLaunchRecord = electronFailedConnectCleanup.record;
|
|
660
697
|
if (electronFailedConnectCleanup.partial) {
|
|
@@ -707,7 +744,7 @@ export async function processBrowserOutput(input) {
|
|
|
707
744
|
if (presentationEnvelope?.error !== undefined)
|
|
708
745
|
presentationEnvelope = { ...presentationEnvelope, error: redactClipboardPermissionErrorValue(prepared.executionPlan.commandInfo, presentationEnvelope.error, clipboardWritePayloadCandidates) };
|
|
709
746
|
}
|
|
710
|
-
if (plainTextUpgrade && errorText)
|
|
747
|
+
if ((plainTextUpgrade || textOutput) && errorText)
|
|
711
748
|
presentationEnvelope = { ...presentationEnvelope, success: false, error: errorText };
|
|
712
749
|
const resultRetainsPreparedManagedSession = !managedSessionOutcome || (managedSessionOutcome.activeAfter
|
|
713
750
|
&& managedSessionOutcome.attemptedSessionName === managedSessionOutcome.currentSessionName);
|
|
@@ -718,24 +755,33 @@ export async function processBrowserOutput(input) {
|
|
|
718
755
|
? prepared.ownedManagedSessionContext?.headedManagedAutosaveInterval
|
|
719
756
|
: undefined;
|
|
720
757
|
// Commit native lifecycle facts to the execution state before rendering/export can fail.
|
|
758
|
+
const finalHelperConfirmation = parseReadConfirmation(state.observedBrowserEffects?.readConfirmation);
|
|
759
|
+
if (finalHelperConfirmation?.state === "pending") {
|
|
760
|
+
confirmationFromHelper ||= state.observedBrowserEffects?.readConfirmation !== readConfirmationEvent;
|
|
761
|
+
readConfirmationEvent = finalHelperConfirmation;
|
|
762
|
+
succeeded = false;
|
|
763
|
+
}
|
|
721
764
|
applyBrowserRunStatePatch(state, { artifactManifest, freshSessionOrdinal, managedSessionActive, managedSessionCompatibilityWorkaround, managedSessionHeadedAutosaveDisabled, managedSessionHeadedAutosaveInterval, managedSessionCwd, managedSessionName, managedSessionNamespace, networkRoutesBySession });
|
|
722
765
|
state.observedBrowserEffects = {
|
|
723
|
-
args: prepared.redactedArgs, command: prepared.executionPlan.commandInfo.command, subcommand: prepared.executionPlan.commandInfo.subcommand,
|
|
766
|
+
args: prepared.redactedArgs, command: directClose ? "close" : prepared.executionPlan.commandInfo.command, subcommand: directClose ? undefined : prepared.executionPlan.commandInfo.subcommand,
|
|
724
767
|
sessionName: prepared.executionPlan.sessionName, namespace: prepared.executionPlan.namespace, sessionMode: prepared.sessionMode,
|
|
725
768
|
usedImplicitSession: prepared.executionPlan.usedImplicitSession, agentBrowserStarted: processResult.agentBrowserStarted,
|
|
726
769
|
nativeSucceeded: succeeded, managedSessionOutcome, managedSessionCwd: prepared.ownedManagedSessionContext?.cwd ?? managedSessionCwd, compatibilityWorkaround: prepared.compatibilityWorkaround,
|
|
727
770
|
managedSessionHeadedAutosaveDisabled: resultHeadedManagedAutosaveDisabled, managedSessionHeadedAutosaveInterval: resultHeadedManagedAutosaveInterval,
|
|
728
771
|
managedSessionRestoreDisabled: state.managedSessionRestoreState.isDisabled(prepared.executionPlan.sessionName, prepared.executionPlan.namespace),
|
|
729
|
-
closeAllApplied, readConfirmation: readConfirmationEvent ?? prepared.readConfirmation,
|
|
772
|
+
closeAllApplied, readConfirmation: readConfirmationEvent ?? prepared.readConfirmation ?? state.observedBrowserEffects?.readConfirmation,
|
|
730
773
|
electron: electronLaunchRecord ? { launch: electronLaunchRecord, cleanup: electronFailedConnectCleanup } : undefined,
|
|
731
774
|
batchSteps: Array.isArray(presentationEnvelope?.data) ? presentationEnvelope.data.filter(isRecord).map(row => ({
|
|
732
|
-
command: Array.isArray(row.command) && row.command.every(token => typeof token === "string") ? redactInvocationArgs(row.command) : undefined,
|
|
733
|
-
success: row.success, lifecycle: extractAgentBrowserLifecycle(row.result),
|
|
775
|
+
command: Array.isArray(row.command) && row.command.every(token => typeof token === "string") ? isSuccessfulNativeConfirmedClose(row.command, row.result) ? ["close"] : redactInvocationArgs(row.command) : undefined,
|
|
776
|
+
success: row.success === true && !detectConfirmationRequired(row.result), lifecycle: extractAgentBrowserLifecycle(row.result),
|
|
734
777
|
})) : undefined,
|
|
735
778
|
};
|
|
736
|
-
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({ modelVisible: input.modelVisible, args: prepared.redactedProcessArgs, artifactManifest, artifactMaxUpdatedAtMs: Date.now(), artifactMinUpdatedAtMs: input.artifactRunStartedAtMs, artifactRequest: screenshotArtifactRequest, batchArtifactRequests: batchScreenshotArtifactRequests, commandInfo: prepared.executionPlan.commandInfo, compiledSemanticAction: prepared.compiledSemanticAction, cwd: operationCwd, envelope: presentationEnvelope, errorText, namespace: prepared.executionPlan.namespace, networkRouteDiagnostics, networkRoutes: activeNetworkRoutes, persistentArtifactStore, previousRecordingContactSheetPath: sessionStateKey ? state.activeRecordingReservations?.get(sessionStateKey)?.contactSheetPath : undefined, piCleanupOwnership: sessionStateKey && (state.ownedManagedSessions.has(sessionStateKey) || prepared.executionPlan.managedSessionName !== undefined) ? "wrapper-managed" : "caller-owned", sessionName: prepared.executionPlan.sessionName });
|
|
779
|
+
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({ textOutput, stdin: prepared.processStdin, modelVisible: input.modelVisible, args: prepared.redactedProcessArgs, artifactManifest, artifactMaxUpdatedAtMs: Date.now(), artifactMinUpdatedAtMs: input.artifactRunStartedAtMs, artifactRequest: screenshotArtifactRequest, batchArtifactRequests: batchScreenshotArtifactRequests, commandInfo: { ...prepared.executionPlan.commandInfo, commandTokens: prepared.commandTokens }, compiledSemanticAction: prepared.compiledSemanticAction, cwd: operationCwd, envelope: presentationEnvelope, errorText, namespace: prepared.executionPlan.namespace, networkRouteDiagnostics, networkRoutes: activeNetworkRoutes, persistentArtifactStore, previousRecordingContactSheetPath: sessionStateKey ? state.activeRecordingReservations?.get(sessionStateKey)?.contactSheetPath : undefined, piCleanupOwnership: sessionStateKey && (state.ownedManagedSessions.has(sessionStateKey) || prepared.executionPlan.managedSessionName !== undefined) ? "wrapper-managed" : "caller-owned", sessionName: prepared.executionPlan.sessionName });
|
|
737
780
|
if (recordingStopRecovery)
|
|
738
781
|
presentation = mergeRecordingRecoveryPresentation(presentation, recordingStopRecovery);
|
|
782
|
+
if (parseError && processResult.exitCode !== 0 && processResult.stderr.trim() && !processResult.timedOut && !processResult.aborted && !processResult.spawnError) {
|
|
783
|
+
presentation.failureCategory = "upstream-error";
|
|
784
|
+
}
|
|
739
785
|
const confirmation = readConfirmationEvent ?? prepared.readConfirmation;
|
|
740
786
|
if (confirmation) {
|
|
741
787
|
presentation.readConfirmation = confirmation;
|
|
@@ -746,9 +792,11 @@ export async function processBrowserOutput(input) {
|
|
|
746
792
|
presentation.resultCategory = "failure";
|
|
747
793
|
presentation.failureCategory = "confirmation-required";
|
|
748
794
|
presentation.successCategory = undefined;
|
|
795
|
+
if (confirmationFromHelper)
|
|
796
|
+
presentation.content.unshift({ type: "text", text: `Native helper ${confirmation.command ?? "read"} requires confirmation (${confirmation.action ?? "read"}, ${confirmation.id}). The requested command's result, if dispatched, is preserved below.` });
|
|
749
797
|
}
|
|
750
798
|
}
|
|
751
|
-
if (plainTextUpgrade && !succeeded && typeof presentationEnvelope?.data === "string") {
|
|
799
|
+
if (plainTextUpgrade && !textOutput && !succeeded && typeof presentationEnvelope?.data === "string") {
|
|
752
800
|
presentation.data = presentationEnvelope.data;
|
|
753
801
|
const errorContent = presentation.content[0];
|
|
754
802
|
if (errorContent?.type === "text" && presentationEnvelope.data)
|
|
@@ -900,10 +948,20 @@ export async function processBrowserOutput(input) {
|
|
|
900
948
|
state.managedSessionRestoreState.recordDaemonRestoreKey(prepared.executionPlan.sessionName, prepared.executionPlan.namespace, daemon.restoreKey, daemon.generation);
|
|
901
949
|
}
|
|
902
950
|
}
|
|
951
|
+
if (sessionStateKey && currentRefSnapshot && processSucceeded && parseSucceeded && !unobservedMutation && readConfirmationEvent?.state === "cleared"
|
|
952
|
+
&& readConfirmationEvent.command === "snapshot" && readConfirmationEvent.action === "snapshot"
|
|
953
|
+
&& confirmedEffects.some(effect => effect.command === "snapshot" && effect.succeeded
|
|
954
|
+
&& effect.index === (Array.isArray(presentationEnvelope?.data) ? presentationEnvelope.data.length - 1 : 0) && extractRefSnapshotFromData(effect.data))) {
|
|
955
|
+
readConfirmationEvent = { ...readConfirmationEvent, refSnapshotFresh: true };
|
|
956
|
+
presentation.readConfirmation = readConfirmationEvent;
|
|
957
|
+
sessionPageState.applyReadConfirmation(readConfirmationEvent, sessionPageStateUpdate);
|
|
958
|
+
state.observedBrowserEffects = { ...state.observedBrowserEffects, readConfirmation: readConfirmationEvent };
|
|
959
|
+
}
|
|
903
960
|
if (sessionStateKey)
|
|
904
961
|
currentSessionTabTarget = authoritativePageState?.tabTarget;
|
|
905
962
|
const currentSessionTabTargetUnknown = authoritativePageState?.tabTargetUnknown === true ? true : undefined;
|
|
906
|
-
|
|
963
|
+
// ponytail: skip silent ref copies; direct failure receipts need their existing fields until final publication owns those paths.
|
|
964
|
+
const result = buildFinalAgentBrowserToolResult({ modelVisible: input.modelVisible, aboutBlankSessionMismatch, artifactCleanup, categoryDetails: finalRecoveryState.categoryDetails, clickDispatchDiagnostic, commandTokens: prepared.commandTokens, comboboxFocusDiagnostic, compiledNetworkSourceLookup: prepared.compiledNetworkSourceLookup, compiledSemanticAction: prepared.compiledSemanticAction, compatibilityWorkaround: prepared.compatibilityWorkaround, currentRefSnapshot: input.modelVisible === false ? undefined : 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: !prepared.ownedManagedSessionContext?.reuseOnly && resultHeadedManagedAutosaveDisabled || undefined, managedSessionHeadedAutosaveInterval: prepared.ownedManagedSessionContext?.reuseOnly ? undefined : 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 });
|
|
907
965
|
const resultWithCloseAll = closeAllApplied
|
|
908
966
|
? { ...result, details: { ...(isRecord(result.details) ? result.details : {}), closeAllApplied: true } }
|
|
909
967
|
: result;
|
package/dist/extensions/agent-browser/lib/orchestration/browser-run/screenshot-observation.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isRecord } from "../../parsing.js";
|
|
2
|
+
import { LIGHTPANDA_IMAGE_REASON } from "../../results/presentation/common.js";
|
|
2
3
|
import { getScreenshotPathTokenIndex, getScreenshotPositionalIndices } from "./artifact-paths.js";
|
|
3
4
|
import { runSessionCommandData } from "./session-state.js";
|
|
4
5
|
export function getScreenshotCapture(command) {
|
|
@@ -28,7 +29,8 @@ export async function collectScreenshotSample(options) {
|
|
|
28
29
|
element = { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
|
|
29
30
|
}
|
|
30
31
|
} catch {}
|
|
31
|
-
|
|
32
|
+
const textRendered = navigator.userAgentData?.brands?.some(b => b.brand === "Lightpanda") || /\\bLightpanda\\//i.test(navigator.userAgent);
|
|
33
|
+
return { ...(textRendered ? { rendering: "text" } : {}), url: location.href, frame: window === window.top ? "main" : "child", childFrameCount: window.frames.length,
|
|
32
34
|
viewport: { width: innerWidth, height: innerHeight },
|
|
33
35
|
document: { width: Math.max(root.scrollWidth, body?.scrollWidth || 0, innerWidth), height: Math.max(root.scrollHeight, body?.scrollHeight || 0, innerHeight) },
|
|
34
36
|
scroll: { x: scrollX, y: scrollY }, dpr: devicePixelRatio,
|
|
@@ -37,7 +39,7 @@ export async function collectScreenshotSample(options) {
|
|
|
37
39
|
try {
|
|
38
40
|
const data = await runSessionCommandData({ ...options, args: ["eval", "--stdin"], stdin: script, timeoutMs: 2_000 });
|
|
39
41
|
const sample = isRecord(data) ? data.result : undefined;
|
|
40
|
-
if (!isRecord(sample) || typeof sample.url !== "string" || !["main", "child"].includes(String(sample.frame))
|
|
42
|
+
if (!isRecord(sample) || (sample.rendering !== undefined && sample.rendering !== "text") || typeof sample.url !== "string" || !["main", "child"].includes(String(sample.frame))
|
|
41
43
|
|| !finiteRecord(sample.viewport, ["width", "height"]) || !finiteRecord(sample.document, ["width", "height"])
|
|
42
44
|
|| !finiteRecord(sample.scroll, ["x", "y"]) || !finiteRecord(sample.visualViewport, ["x", "y", "scale"])
|
|
43
45
|
|| typeof sample.childFrameCount !== "number" || !Number.isInteger(sample.childFrameCount) || sample.childFrameCount < 0
|
|
@@ -53,6 +55,8 @@ export async function collectScreenshotSample(options) {
|
|
|
53
55
|
export function buildScreenshotGeometry(options) {
|
|
54
56
|
const { before, after, capture, pixels } = options;
|
|
55
57
|
const unknown = (reason) => ({ status: "unknown", reason, before, after });
|
|
58
|
+
if (options.rendering === "text" || before?.rendering === "text" || after?.rendering === "text")
|
|
59
|
+
return unknown(LIGHTPANDA_IMAGE_REASON);
|
|
56
60
|
if (!before || !after)
|
|
57
61
|
return unknown("Capture was not bracketed by browser geometry samples; coordinates are unknown.");
|
|
58
62
|
if (JSON.stringify(before) !== JSON.stringify(after))
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
1
2
|
import { rm } from "node:fs/promises";
|
|
2
3
|
import { getScreenshotPositionalIndices } from "./artifact-paths.js";
|
|
3
4
|
import { observeNativeWebMcp } from "../../webmcp-observation.js";
|
|
@@ -7,6 +8,7 @@ import { isBrowserIndependentRead, needsManagedSession } from "../../command-pol
|
|
|
7
8
|
import { runAgentBrowserProcess } from "../../process.js";
|
|
8
9
|
import { buildAgentBrowserNextActions } from "../../results/action-recommendations.js";
|
|
9
10
|
import { parseAgentBrowserEnvelope } from "../../results/envelope.js";
|
|
11
|
+
import { detectConfirmationRequired } from "../../results/confirmation.js";
|
|
10
12
|
import { buildNextToolAction, withOptionalNamespaceArgs, withOptionalSessionArgs } from "../../results/next-actions.js";
|
|
11
13
|
import { getSessionPageStateKey, isAboutBlankUrl, normalizeComparableUrl, targetsMatch, } from "../../session-page-state.js";
|
|
12
14
|
import { getRecordCommandOperands, isCloseCommand, isElectronPostCommandHealthCommand, isNavigationObservableCommandName, isOpenNavigationCommand, isRefGuardedCommand, isRefInvalidatingBatchCommand, isSessionTabPinningExcludedCommand, isSessionTabPostCommandCorrectionExcludedCommand, isWindowOrDiffPageTransitionCommand, } from "../../command-taxonomy.js";
|
|
@@ -257,6 +259,10 @@ export function extractNavigationSummaryFromData(data) {
|
|
|
257
259
|
export function shouldCaptureNavigationSummary(command, data, subcommand) {
|
|
258
260
|
if (command === "eval")
|
|
259
261
|
return true;
|
|
262
|
+
// A completed compound action may navigate without returning a page URL (e.g. recording restart).
|
|
263
|
+
if (command === "confirm" && isRecord(data) && data.confirmed === true && data.action === "navigate"
|
|
264
|
+
&& isRecord(data.result) && data.result.success === true && extractStringResultField(data.result.data, "url") === undefined)
|
|
265
|
+
return true;
|
|
260
266
|
return (isNavigationObservableCommandName(command, subcommand) &&
|
|
261
267
|
(!isRecord(data) || (typeof data.title !== "string" && typeof data.url !== "string")));
|
|
262
268
|
}
|
|
@@ -399,7 +405,8 @@ export function buildStaleRefPreflight(options) {
|
|
|
399
405
|
}
|
|
400
406
|
if (!options.refSnapshot)
|
|
401
407
|
return undefined;
|
|
402
|
-
if (!targetsMatch(options.refSnapshot.target, options.currentTarget)
|
|
408
|
+
if (!targetsMatch(options.refSnapshot.target, options.currentTarget)
|
|
409
|
+
|| options.requireExactTargetUrl && options.refSnapshot.target && options.currentTarget && options.refSnapshot.target.url !== options.currentTarget.url) {
|
|
403
410
|
return {
|
|
404
411
|
message: `Ref ${usedRefIds.map((refId) => `@${refId}`).join(", ")} came from a snapshot for ${options.refSnapshot.target?.url ?? "a prior page"}, but the current session target is ${options.currentTarget?.url ?? "unknown"}. Run snapshot -i again before using page-scoped refs.`,
|
|
405
412
|
refIds: usedRefIds,
|
|
@@ -492,47 +499,52 @@ function selectAnySessionTargetTab(options) {
|
|
|
492
499
|
const selection = selectedTab ? getTabSelection(selectedTab) : undefined;
|
|
493
500
|
return selection ? { ...selection, ...(targetTitle ? { targetTitle } : {}), targetUrl } : undefined;
|
|
494
501
|
}
|
|
502
|
+
const sessionCommandObservation = new AsyncLocalStorage();
|
|
503
|
+
export function withSessionCommandObservation(observer, run) {
|
|
504
|
+
return sessionCommandObservation.run(observer, run);
|
|
505
|
+
}
|
|
506
|
+
export function nativePolicyDefinitelyUnestablished(result, envelope, commandTokens) {
|
|
507
|
+
if (commandTokens && !needsManagedSession(parseArgvDescriptor(commandTokens)) && !isBrowserIndependentRead(commandTokens))
|
|
508
|
+
return true;
|
|
509
|
+
if (!result.agentBrowserStarted)
|
|
510
|
+
return true;
|
|
511
|
+
if (result.aborted || result.timedOut || result.spawnError || result.exitCode !== 1)
|
|
512
|
+
return false;
|
|
513
|
+
// Native CLI parse/config failures precede ensure_daemon; a failed executed action does not.
|
|
514
|
+
return envelope?.success === false && isRecord(envelope.data) && Object.keys(envelope.data).length === 1
|
|
515
|
+
&& ["unknown_command", "unknown_subcommand", "missing_arguments", "invalid_value", "invalid_session_name"].includes(String(envelope.data.type))
|
|
516
|
+
|| result.stdout.trim() === "" && /config file not found:|failed to load config from /.test(result.stderr);
|
|
517
|
+
}
|
|
495
518
|
export async function runSessionCommandData(options) {
|
|
496
519
|
const { args, cwd, env, namespace, pinNamespace, sessionName, signal, stdin, throwOnFailure, timeoutMs } = options;
|
|
497
|
-
if (!sessionName)
|
|
520
|
+
if (!sessionName || sessionCommandObservation.getStore()?.allow(options) === false)
|
|
498
521
|
return undefined;
|
|
499
522
|
const processResult = await runAgentBrowserProcess({
|
|
500
523
|
args: ["--json", ...(namespace !== undefined || pinNamespace ? ["--namespace", namespace ?? ""] : []), "--session", sessionName, ...args],
|
|
501
|
-
cwd,
|
|
502
|
-
env,
|
|
503
|
-
signal,
|
|
504
|
-
stdin,
|
|
505
|
-
timeoutMs,
|
|
524
|
+
cwd, env, signal, stdin, timeoutMs,
|
|
506
525
|
});
|
|
507
526
|
try {
|
|
508
527
|
options.onProcessResult?.(processResult);
|
|
528
|
+
const parsed = await parseAgentBrowserEnvelope({ stdout: processResult.stdout, stdoutPath: processResult.stdoutSpillPath });
|
|
529
|
+
sessionCommandObservation.getStore()?.observe(options, processResult, parsed.envelope);
|
|
509
530
|
if (processResult.aborted || processResult.spawnError || processResult.exitCode !== 0) {
|
|
510
531
|
if (throwOnFailure) {
|
|
511
|
-
const reason = processResult.aborted
|
|
512
|
-
? "command was aborted"
|
|
513
|
-
: processResult.spawnError
|
|
514
|
-
? "process could not start"
|
|
515
|
-
: `process exited with code ${processResult.exitCode}`;
|
|
532
|
+
const reason = processResult.aborted ? "command was aborted" : processResult.spawnError ? "process could not start" : `process exited with code ${processResult.exitCode}`;
|
|
516
533
|
throw new Error(`agent-browser ${reason}`);
|
|
517
534
|
}
|
|
518
535
|
return undefined;
|
|
519
536
|
}
|
|
520
|
-
|
|
521
|
-
stdout: processResult.stdout,
|
|
522
|
-
stdoutPath: processResult.stdoutSpillPath,
|
|
523
|
-
});
|
|
524
|
-
if (parsed.parseError || parsed.envelope?.success === false) {
|
|
537
|
+
if (parsed.parseError || parsed.envelope?.success === false || detectConfirmationRequired(parsed.envelope?.data)) {
|
|
525
538
|
if (throwOnFailure)
|
|
526
|
-
throw new Error(parsed.parseError ? "agent-browser returned invalid structured output" : "agent-browser reported failure");
|
|
539
|
+
throw new Error(parsed.parseError ? "agent-browser returned invalid structured output" : "agent-browser reported failure or requires confirmation");
|
|
527
540
|
return undefined;
|
|
528
541
|
}
|
|
529
542
|
observeNativeWebMcp(parsed.envelope?.data);
|
|
530
543
|
return parsed.envelope?.data;
|
|
531
544
|
}
|
|
532
545
|
finally {
|
|
533
|
-
if (processResult.stdoutSpillPath)
|
|
546
|
+
if (processResult.stdoutSpillPath)
|
|
534
547
|
await rm(processResult.stdoutSpillPath, { force: true }).catch(() => undefined);
|
|
535
|
-
}
|
|
536
548
|
}
|
|
537
549
|
}
|
|
538
550
|
export async function collectOpenResultTabCorrection(options) {
|