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.
- package/CHANGELOG.md +40 -1
- package/README.md +21 -6
- package/dist/extensions/agent-browser/index.js +138 -75
- package/dist/extensions/agent-browser/lib/command-taxonomy.js +6 -5
- package/dist/extensions/agent-browser/lib/electron/cleanup.js +10 -1
- package/dist/extensions/agent-browser/lib/input-modes/params.js +20 -7
- package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +0 -1
- package/dist/extensions/agent-browser/lib/managed-session-restore.js +13 -12
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.js +6 -25
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +2 -2
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +2 -3
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +10 -3
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +3 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/wait-timeouts.js +1 -1
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +104 -117
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +64 -36
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +100 -135
- package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +3 -1
- package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +3 -1
- package/dist/extensions/agent-browser/lib/page-target-validation.js +10 -10
- package/dist/extensions/agent-browser/lib/parsing.js +7 -0
- package/dist/extensions/agent-browser/lib/playbook.js +6 -9
- package/dist/extensions/agent-browser/lib/process.js +28 -113
- package/dist/extensions/agent-browser/lib/recording-reservations.js +3 -1
- package/dist/extensions/agent-browser/lib/results/action-recommendations.js +5 -2
- package/dist/extensions/agent-browser/lib/results/envelope.js +9 -4
- package/dist/extensions/agent-browser/lib/results/next-actions.js +8 -0
- package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +45 -43
- package/dist/extensions/agent-browser/lib/results/presentation/batch.js +2 -1
- package/dist/extensions/agent-browser/lib/results/presentation/errors.js +10 -2
- package/dist/extensions/agent-browser/lib/results/presentation.js +9 -2
- package/dist/extensions/agent-browser/lib/results/recovery-actions.js +4 -4
- package/dist/extensions/agent-browser/lib/runtime.js +18 -2
- package/dist/extensions/agent-browser/lib/session-page-state.js +29 -10
- package/docs/ARCHITECTURE.md +8 -5
- package/docs/COMMAND_REFERENCE.md +25 -16
- package/docs/ELECTRON.md +6 -6
- package/docs/RELEASE.md +14 -5
- package/docs/REQUIREMENTS.md +1 -1
- package/docs/SUPPORT_MATRIX.md +18 -5
- package/docs/TOOL_CONTRACT.md +38 -25
- package/package.json +5 -1
|
@@ -5,7 +5,7 @@ import { Text } from "@earendil-works/pi-tui";
|
|
|
5
5
|
import { batchHasSuccessfulCloseAll, getSuccessfulBatchCloseLifecycle } from "./lib/batch-lifecycle.js";
|
|
6
6
|
import { PROJECT_RULE_PROMPT, buildBrowserDefaultProfileGuideline, buildBrowserExecutablePathGuideline, buildToolPromptGuidelines, } from "./lib/playbook.js";
|
|
7
7
|
import { SessionPageState } from "./lib/session-page-state.js";
|
|
8
|
-
import { canUseHeadlessCompatibilityUserAgent, createEphemeralSessionSeed, createFreshSessionName, createImplicitSessionName, extractUpstreamCommandTokens, getImplicitSessionCloseTimeoutMs, getImplicitSessionIdleTimeoutMs, isRestorableManagedSessionName, restoreManagedSessionStateFromBranch, validateToolArgs, redactSensitiveText, isPlainTextInspectionArgs, } from "./lib/runtime.js";
|
|
8
|
+
import { buildExecutionPlan, canUseHeadlessCompatibilityUserAgent, createEphemeralSessionSeed, createFreshSessionName, createImplicitSessionName, extractUpstreamCommandTokens, getImplicitSessionCloseTimeoutMs, getImplicitSessionIdleTimeoutMs, isRestorableManagedSessionName, restoreManagedSessionStateFromBranch, validateToolArgs, redactSensitiveText, isPlainTextInspectionArgs, } from "./lib/runtime.js";
|
|
9
9
|
import { extractExplicitNamespace, extractExplicitSessionName, getAgentBrowserSessionIdentityKey, isAgentBrowserSessionIdentityKeyInNamespace, isUpstreamEnvFlagEnabled, resolveAgentBrowserNamespace } from "./lib/argv-grammar.js";
|
|
10
10
|
import { parseArgvDescriptor } from "./lib/argv-descriptor.js";
|
|
11
11
|
import { needsManagedSession } from "./lib/command-policy.js";
|
|
@@ -680,7 +680,7 @@ function shouldSerializeBrowserCommand(options) {
|
|
|
680
680
|
return true;
|
|
681
681
|
if (options.explicitSessionName === options.managedSessionName)
|
|
682
682
|
return true;
|
|
683
|
-
if (options.ownedManagedSessions.has(getSessionContextKey(options.explicitSessionName, options.
|
|
683
|
+
if (options.ownedManagedSessions.has(getSessionContextKey(options.explicitSessionName, options.namespace) ?? options.explicitSessionName))
|
|
684
684
|
return true;
|
|
685
685
|
return getActiveElectronRecords(options.ownedElectronLaunchRecords).some((record) => record.sessionName === options.explicitSessionName);
|
|
686
686
|
}
|
|
@@ -844,7 +844,7 @@ export default function agentBrowserExtension(pi) {
|
|
|
844
844
|
let artifactManifest;
|
|
845
845
|
let activeRecordingReservations = new Map();
|
|
846
846
|
let recordingSessionTombstones = new Map();
|
|
847
|
-
let
|
|
847
|
+
let recordingReservationsDirty = false;
|
|
848
848
|
let attachedSessionKeys = new Set();
|
|
849
849
|
let networkRoutesBySession = new Map();
|
|
850
850
|
let electronLaunchRecords = new Map();
|
|
@@ -861,23 +861,64 @@ export default function agentBrowserExtension(pi) {
|
|
|
861
861
|
let branchRestoreGeneration = 0;
|
|
862
862
|
let branchStateGeneration = 0;
|
|
863
863
|
const validatedUpstreamPathKeys = new Set();
|
|
864
|
+
const recordingPersistenceWarning = "Recording persistence warning: recording protection could not be saved to the Pi journal. Restart protection is not yet durable; keep recording destinations untouched until exact stop or close. The next browser operation retries journal persistence; cleanup remains available.";
|
|
865
|
+
const flushRecordingReservations = () => {
|
|
866
|
+
if (!recordingReservationsDirty)
|
|
867
|
+
return;
|
|
868
|
+
try {
|
|
869
|
+
// Pi updates branch memory before writing. One failed append can hide an earlier
|
|
870
|
+
// durable reservation after reopen, so republish all current state, not just the failed row.
|
|
871
|
+
for (const reservation of recordingSessionTombstones.values())
|
|
872
|
+
appendRecordingReservationTransition(pi, { reservation, state: "closed" });
|
|
873
|
+
for (const reservation of activeRecordingReservations.values())
|
|
874
|
+
appendRecordingReservationTransition(pi, { reservation, state: "active" });
|
|
875
|
+
recordingReservationsDirty = false;
|
|
876
|
+
}
|
|
877
|
+
catch { }
|
|
878
|
+
};
|
|
879
|
+
const warnRecordingPersistence = (result) => {
|
|
880
|
+
if (!recordingReservationsDirty)
|
|
881
|
+
return result;
|
|
882
|
+
const content = [...result.content];
|
|
883
|
+
const first = content[0];
|
|
884
|
+
let json;
|
|
885
|
+
if (first?.type === "text") {
|
|
886
|
+
try {
|
|
887
|
+
json = JSON.parse(first.text);
|
|
888
|
+
}
|
|
889
|
+
catch { }
|
|
890
|
+
}
|
|
891
|
+
if (isRecord(json) && typeof json.success === "boolean") {
|
|
892
|
+
content[0] = { type: "text", text: JSON.stringify({ ...json, warnings: [...(Array.isArray(json.warnings) ? json.warnings : []), recordingPersistenceWarning] }, null, 2) };
|
|
893
|
+
}
|
|
894
|
+
else if (first?.type === "text")
|
|
895
|
+
content[0] = { ...first, text: `${first.text}\n\n${recordingPersistenceWarning}` };
|
|
896
|
+
else
|
|
897
|
+
content.push({ type: "text", text: recordingPersistenceWarning });
|
|
898
|
+
return { ...result, content, details: { ...(isRecord(result.details) ? result.details : {}), recordingPersistenceWarning } };
|
|
899
|
+
};
|
|
900
|
+
const notifyRecordingPersistence = (ctx) => {
|
|
901
|
+
if (!recordingReservationsDirty)
|
|
902
|
+
return;
|
|
903
|
+
if (ctx.hasUI)
|
|
904
|
+
ctx.ui.notify(recordingPersistenceWarning, "warning");
|
|
905
|
+
else
|
|
906
|
+
console.warn(recordingPersistenceWarning);
|
|
907
|
+
};
|
|
864
908
|
const appendRecordingTransitions = (transitions) => {
|
|
865
909
|
for (const transition of transitions) {
|
|
866
910
|
const key = getAgentBrowserSessionIdentityKey(transition.reservation.sessionName, transition.reservation.namespace);
|
|
867
|
-
if (transition.state === "active")
|
|
911
|
+
if (transition.state === "active")
|
|
868
912
|
recordingSessionTombstones.delete(key);
|
|
869
|
-
|
|
870
|
-
}
|
|
871
|
-
else {
|
|
913
|
+
else
|
|
872
914
|
recordingSessionTombstones.set(key, transition.reservation);
|
|
873
|
-
|
|
915
|
+
if (recordingReservationsDirty)
|
|
916
|
+
continue;
|
|
874
917
|
try {
|
|
875
918
|
appendRecordingReservationTransition(pi, transition);
|
|
876
|
-
recordingSessionTombstonesToPersist.delete(key);
|
|
877
919
|
}
|
|
878
920
|
catch {
|
|
879
|
-
|
|
880
|
-
recordingSessionTombstonesToPersist.set(key, transition.reservation);
|
|
921
|
+
recordingReservationsDirty = true;
|
|
881
922
|
}
|
|
882
923
|
}
|
|
883
924
|
};
|
|
@@ -885,19 +926,28 @@ export default function agentBrowserExtension(pi) {
|
|
|
885
926
|
if (result.isError !== true)
|
|
886
927
|
return result;
|
|
887
928
|
const details = isRecord(result.details) ? result.details : {};
|
|
888
|
-
const
|
|
889
|
-
|
|
929
|
+
const cleanupOnly = details.managedSessionCleanupOnlyReason === "restore-disabled-daemon-without-provenance";
|
|
930
|
+
const nextActions = (Array.isArray(details.nextActions) ? details.nextActions : [])
|
|
931
|
+
.filter((action) => !cleanupOnly || action.id !== "stop-pending-recording");
|
|
932
|
+
const actionId = cleanupOnly ? "close-pending-recording" : "stop-pending-recording";
|
|
933
|
+
if (nextActions.some((action) => action.id === actionId))
|
|
890
934
|
return result;
|
|
891
935
|
const stopActions = applyNamespaceToNextActions(applySessionToNextActions([
|
|
892
936
|
buildNextToolAction({
|
|
893
|
-
args: ["record", "stop"],
|
|
894
|
-
id:
|
|
895
|
-
reason:
|
|
896
|
-
|
|
937
|
+
args: cleanupOnly ? ["close"] : ["record", "stop"],
|
|
938
|
+
id: actionId,
|
|
939
|
+
reason: cleanupOnly
|
|
940
|
+
? "Close this exact session to abandon the recording; its live daemon lacks current-instance provenance."
|
|
941
|
+
: "Stop the active recording so the requested video can be finalized and verified on disk.",
|
|
942
|
+
safety: cleanupOnly
|
|
943
|
+
? "Close does not verify the WebM. The recording is abandoned/unverified, even if close leaves a file on disk."
|
|
944
|
+
: "The file remains pending until record stop succeeds; verify details.artifactVerification afterward.",
|
|
897
945
|
}),
|
|
898
|
-
], reservation.sessionName), reservation.namespace);
|
|
946
|
+
], reservation.sessionName), cleanupOnly ? reservation.namespace ?? "" : reservation.namespace);
|
|
899
947
|
appendUniqueAgentBrowserNextActions(nextActions, stopActions);
|
|
900
|
-
const cleanupNotice =
|
|
948
|
+
const cleanupNotice = cleanupOnly
|
|
949
|
+
? "This recording cannot be stopped through the unproven daemon. Use the exact close-pending-recording payload in details.nextActions; any WebM left by close is abandoned/unverified."
|
|
950
|
+
: "An active recording remains open. Use the exact stop-pending-recording payload in details.nextActions before leaving this session.";
|
|
901
951
|
let noticeAppended = false;
|
|
902
952
|
const content = result.content.map((item) => {
|
|
903
953
|
if (noticeAppended || item.type !== "text")
|
|
@@ -917,18 +967,7 @@ export default function agentBrowserExtension(pi) {
|
|
|
917
967
|
if (!reservation && artifactManifest === previousManifest)
|
|
918
968
|
return;
|
|
919
969
|
const terminalReservation = reservation ?? { absolutePath: "", cwd: managedSessionCwd, namespace, path: "", sessionName };
|
|
920
|
-
|
|
921
|
-
recordingSessionTombstones.set(terminalKey, terminalReservation);
|
|
922
|
-
try {
|
|
923
|
-
appendRecordingReservationTransition(pi, {
|
|
924
|
-
reservation: terminalReservation,
|
|
925
|
-
state: "closed",
|
|
926
|
-
});
|
|
927
|
-
recordingSessionTombstonesToPersist.delete(terminalKey);
|
|
928
|
-
}
|
|
929
|
-
catch {
|
|
930
|
-
recordingSessionTombstonesToPersist.set(terminalKey, terminalReservation);
|
|
931
|
-
}
|
|
970
|
+
appendRecordingTransitions([{ reservation: terminalReservation, state: "closed" }]);
|
|
932
971
|
};
|
|
933
972
|
const syncRecordingReservationsFromResult = (result) => {
|
|
934
973
|
const handledClosedSessionKeys = new Set();
|
|
@@ -1107,20 +1146,25 @@ export default function agentBrowserExtension(pi) {
|
|
|
1107
1146
|
traceOwners = new Map();
|
|
1108
1147
|
artifactManifest = restoreArtifactManifestFromBranch(branch);
|
|
1109
1148
|
const restoredRecordingState = restoreRecordingReservationStateFromBranch(branch);
|
|
1110
|
-
for (const
|
|
1111
|
-
if (restoredRecordingState.terminal.has(key))
|
|
1112
|
-
|
|
1113
|
-
else
|
|
1114
|
-
recordingSessionTombstonesToPersist.set(key, reservation);
|
|
1149
|
+
for (const key of recordingSessionTombstones.keys()) {
|
|
1150
|
+
if (!restoredRecordingState.terminal.has(key))
|
|
1151
|
+
recordingReservationsDirty = true;
|
|
1115
1152
|
}
|
|
1116
1153
|
for (const [key, reservation] of restoredRecordingState.terminal) {
|
|
1117
1154
|
if (!activeRecordingReservations.has(key))
|
|
1118
1155
|
recordingSessionTombstones.set(key, reservation);
|
|
1119
1156
|
}
|
|
1120
|
-
for (const key of recordingSessionTombstones
|
|
1157
|
+
for (const [key, reservation] of recordingSessionTombstones) {
|
|
1121
1158
|
restoredRecordingState.active.delete(key);
|
|
1122
|
-
|
|
1159
|
+
if (artifactManifest)
|
|
1160
|
+
artifactManifest = retirePendingRecordingManifestEntries(artifactManifest, reservation.sessionName, reservation.namespace);
|
|
1161
|
+
}
|
|
1162
|
+
for (const [key, reservation] of activeRecordingReservations) {
|
|
1163
|
+
const restored = restoredRecordingState.active.get(key);
|
|
1164
|
+
if (restored?.absolutePath !== reservation.absolutePath || restored.cwd !== reservation.cwd)
|
|
1165
|
+
recordingReservationsDirty = true;
|
|
1123
1166
|
restoredRecordingState.active.set(key, reservation);
|
|
1167
|
+
}
|
|
1124
1168
|
activeRecordingReservations = restoredRecordingState.active;
|
|
1125
1169
|
attachedSessionKeys = restoreAttachedSessionKeysFromBranch(branch);
|
|
1126
1170
|
networkRoutesBySession = new Map();
|
|
@@ -1214,7 +1258,11 @@ export default function agentBrowserExtension(pi) {
|
|
|
1214
1258
|
cwd: ctx.cwd,
|
|
1215
1259
|
includeProjectConfig: shouldIncludeProjectConfig(ctx),
|
|
1216
1260
|
}));
|
|
1217
|
-
await artifactExecutionQueue.run(() => managedSessionExecutionQueue.run(() =>
|
|
1261
|
+
await artifactExecutionQueue.run(() => managedSessionExecutionQueue.run(async () => {
|
|
1262
|
+
await recoverScriptSessionLeasesWithinQueue(ctx);
|
|
1263
|
+
flushRecordingReservations();
|
|
1264
|
+
notifyRecordingPersistence(ctx);
|
|
1265
|
+
}));
|
|
1218
1266
|
});
|
|
1219
1267
|
pi.on("session_tree", async (_event, ctx) => {
|
|
1220
1268
|
for (const controller of activeScriptControllers)
|
|
@@ -1223,6 +1271,8 @@ export default function agentBrowserExtension(pi) {
|
|
|
1223
1271
|
await artifactExecutionQueue.run(() => managedSessionExecutionQueue.run(async () => {
|
|
1224
1272
|
restoreBranchBackedState(ctx, { resetRuntimeOwnership: false });
|
|
1225
1273
|
await recoverScriptSessionLeasesWithinQueue(ctx);
|
|
1274
|
+
flushRecordingReservations();
|
|
1275
|
+
notifyRecordingPersistence(ctx);
|
|
1226
1276
|
}));
|
|
1227
1277
|
});
|
|
1228
1278
|
pi.on("session_shutdown", async (event, ctx) => {
|
|
@@ -1263,30 +1313,21 @@ export default function agentBrowserExtension(pi) {
|
|
|
1263
1313
|
else {
|
|
1264
1314
|
await closeOwnedManagedSessionsExcept(ownedManagedSessions, managedSessionRestoreState, managedSessionActive ? managedSessionName : undefined, implicitSessionCloseTimeoutMs, attachedSessionKeys, managedSessionActive ? managedSessionNamespace : undefined, (owner) => retireRecordingSession(owner.sessionName, owner.namespace));
|
|
1265
1315
|
}
|
|
1316
|
+
flushRecordingReservations();
|
|
1317
|
+
notifyRecordingPersistence(ctx);
|
|
1266
1318
|
}));
|
|
1267
1319
|
managedSessionActive = false;
|
|
1268
1320
|
managedSessionCompatibilityWorkaround = undefined;
|
|
1269
1321
|
managedSessionHeadedAutosaveDisabled = false;
|
|
1270
1322
|
managedSessionHeadedAutosaveInterval = undefined;
|
|
1271
1323
|
managedSessionNamespace = undefined;
|
|
1272
|
-
for (const reservation of recordingSessionTombstonesToPersist.values()) {
|
|
1273
|
-
try {
|
|
1274
|
-
appendRecordingReservationTransition(pi, { reservation, state: "closed" });
|
|
1275
|
-
}
|
|
1276
|
-
catch { }
|
|
1277
|
-
}
|
|
1278
|
-
for (const reservation of activeRecordingReservations.values()) {
|
|
1279
|
-
try {
|
|
1280
|
-
appendRecordingReservationTransition(pi, { reservation, state: "active" });
|
|
1281
|
-
}
|
|
1282
|
-
catch { }
|
|
1283
|
-
}
|
|
1284
1324
|
sessionPageState.reset();
|
|
1285
1325
|
traceOwners = new Map();
|
|
1286
1326
|
artifactManifest = undefined;
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1327
|
+
if (!recordingReservationsDirty) {
|
|
1328
|
+
activeRecordingReservations = new Map();
|
|
1329
|
+
recordingSessionTombstones = new Map();
|
|
1330
|
+
}
|
|
1290
1331
|
attachedSessionKeys = new Set();
|
|
1291
1332
|
networkRoutesBySession = new Map();
|
|
1292
1333
|
electronLaunchRecords = new Map();
|
|
@@ -1365,8 +1406,9 @@ export default function agentBrowserExtension(pi) {
|
|
|
1365
1406
|
}
|
|
1366
1407
|
const applyUnserializedOutputPath = async (result, preserveTextContent = false) => {
|
|
1367
1408
|
if (!outputPath || result.isError === true || (isRecord(result.details) && result.details.resultCategory === "failure"))
|
|
1368
|
-
return result;
|
|
1409
|
+
return warnRecordingPersistence(result);
|
|
1369
1410
|
return artifactExecutionQueue.run(async () => {
|
|
1411
|
+
flushRecordingReservations();
|
|
1370
1412
|
const reservationError = getArtifactPreflightValidationError({
|
|
1371
1413
|
activeRecordingReservations: activeRecordingReservations.values(),
|
|
1372
1414
|
args: [],
|
|
@@ -1374,9 +1416,9 @@ export default function agentBrowserExtension(pi) {
|
|
|
1374
1416
|
outputPath,
|
|
1375
1417
|
});
|
|
1376
1418
|
if (reservationError) {
|
|
1377
|
-
return buildValidationFailureResult({ attemptedKind: resolvedInput.kind, kind: "invalid", redactedArgs: resolvedInput.redactedArgs, status: "invalid", toolArgs: resolvedInput.toolArgs, toolStdin: resolvedInput.toolStdin, validationError: reservationError });
|
|
1419
|
+
return warnRecordingPersistence(buildValidationFailureResult({ attemptedKind: resolvedInput.kind, kind: "invalid", redactedArgs: resolvedInput.redactedArgs, status: "invalid", toolArgs: resolvedInput.toolArgs, toolStdin: resolvedInput.toolStdin, validationError: reservationError }));
|
|
1378
1420
|
}
|
|
1379
|
-
return applyAgentBrowserOutputPath({ cwd: ctx.cwd, outputPath, preserveTextContent, result });
|
|
1421
|
+
return applyAgentBrowserOutputPath({ cwd: ctx.cwd, outputPath, preserveTextContent, result: warnRecordingPersistence(result) });
|
|
1380
1422
|
});
|
|
1381
1423
|
};
|
|
1382
1424
|
const versionCheckCommand = extractUpstreamCommandTokens(resolvedInput.toolArgs)[0];
|
|
@@ -1459,7 +1501,10 @@ export default function agentBrowserExtension(pi) {
|
|
|
1459
1501
|
signal?.removeEventListener("abort", abortScript);
|
|
1460
1502
|
if (leased) {
|
|
1461
1503
|
try {
|
|
1462
|
-
cleanupError = await artifactExecutionQueue.run(() => managedSessionExecutionQueue.run(() =>
|
|
1504
|
+
cleanupError = await artifactExecutionQueue.run(() => managedSessionExecutionQueue.run(() => {
|
|
1505
|
+
flushRecordingReservations();
|
|
1506
|
+
return closeScriptSessionLeaseWithinQueue(sessionName, ctx.cwd);
|
|
1507
|
+
}));
|
|
1463
1508
|
}
|
|
1464
1509
|
catch {
|
|
1465
1510
|
cleanupError = "The isolated script session cleanup operation failed.";
|
|
@@ -1563,6 +1608,7 @@ export default function agentBrowserExtension(pi) {
|
|
|
1563
1608
|
: runElectronHostInput();
|
|
1564
1609
|
const electronHostResult = compiledElectron?.action === "cleanup"
|
|
1565
1610
|
? await artifactExecutionQueue.run(async () => {
|
|
1611
|
+
flushRecordingReservations();
|
|
1566
1612
|
const reservationError = outputPath ? getArtifactPreflightValidationError({
|
|
1567
1613
|
activeRecordingReservations: activeRecordingReservations.values(),
|
|
1568
1614
|
args: [],
|
|
@@ -1570,31 +1616,31 @@ export default function agentBrowserExtension(pi) {
|
|
|
1570
1616
|
outputPath,
|
|
1571
1617
|
}) : undefined;
|
|
1572
1618
|
if (reservationError) {
|
|
1573
|
-
return buildValidationFailureResult({ attemptedKind: resolvedInput.kind, kind: "invalid", redactedArgs: resolvedInput.redactedArgs, status: "invalid", toolArgs: resolvedInput.toolArgs, toolStdin: resolvedInput.toolStdin, validationError: reservationError });
|
|
1619
|
+
return warnRecordingPersistence(buildValidationFailureResult({ attemptedKind: resolvedInput.kind, kind: "invalid", redactedArgs: resolvedInput.redactedArgs, status: "invalid", toolArgs: resolvedInput.toolArgs, toolStdin: resolvedInput.toolStdin, validationError: reservationError }));
|
|
1574
1620
|
}
|
|
1575
1621
|
const result = await runSerializedElectronHostInput();
|
|
1576
|
-
return result ? applyAgentBrowserOutputPath({ cwd: ctx.cwd, outputPath, result }) : result;
|
|
1622
|
+
return result ? applyAgentBrowserOutputPath({ cwd: ctx.cwd, outputPath, result: warnRecordingPersistence(result) }) : result;
|
|
1577
1623
|
})
|
|
1578
1624
|
: await runSerializedElectronHostInput();
|
|
1579
1625
|
if (electronHostResult) {
|
|
1580
1626
|
return compiledElectron?.action === "cleanup" ? electronHostResult : applyUnserializedOutputPath(electronHostResult);
|
|
1581
1627
|
}
|
|
1582
1628
|
const explicitSessionName = extractExplicitSessionName(toolArgs);
|
|
1583
|
-
const
|
|
1629
|
+
const callerOwnedSessionNamespace = explicitSessionName
|
|
1630
|
+
? resolveAgentBrowserNamespace(toolArgs, getAgentBrowserProcessEnvironment().AGENT_BROWSER_NAMESPACE)
|
|
1631
|
+
: undefined;
|
|
1584
1632
|
const serializeBrowserCommand = shouldSerializeBrowserCommand({
|
|
1585
|
-
|
|
1633
|
+
namespace: callerOwnedSessionNamespace,
|
|
1586
1634
|
explicitSessionName,
|
|
1587
1635
|
managedSessionName,
|
|
1588
1636
|
ownedElectronLaunchRecords,
|
|
1589
1637
|
ownedManagedSessions,
|
|
1590
1638
|
});
|
|
1591
|
-
const callerOwnedSessionNamespace = explicitSessionName
|
|
1592
|
-
? resolveAgentBrowserNamespace(toolArgs, getAgentBrowserProcessEnvironment().AGENT_BROWSER_NAMESPACE)
|
|
1593
|
-
: undefined;
|
|
1594
1639
|
const callerOwnedSessionQueueKey = !serializeBrowserCommand && explicitSessionName
|
|
1595
1640
|
? getSessionContextKey(explicitSessionName, callerOwnedSessionNamespace) ?? explicitSessionName
|
|
1596
1641
|
: undefined;
|
|
1597
1642
|
const runBrowserCommand = async () => {
|
|
1643
|
+
flushRecordingReservations();
|
|
1598
1644
|
const branchRestoreGenerationAtStart = branchRestoreGeneration;
|
|
1599
1645
|
const generationAtStart = branchStateGeneration;
|
|
1600
1646
|
const sessionPageStateUpdate = sessionPageState.beginUpdate();
|
|
@@ -1620,6 +1666,14 @@ export default function agentBrowserExtension(pi) {
|
|
|
1620
1666
|
sessionPageState,
|
|
1621
1667
|
traceOwners,
|
|
1622
1668
|
};
|
|
1669
|
+
const selectedPlan = buildExecutionPlan(toolArgs, {
|
|
1670
|
+
freshSessionName: createFreshSessionName(browserRunState.managedSessionBaseName, browserRunState.ephemeralSessionSeed, browserRunState.freshSessionOrdinal + 1),
|
|
1671
|
+
managedSessionActive: browserRunState.managedSessionActive,
|
|
1672
|
+
managedSessionCompatibilityWorkaround: browserRunState.managedSessionCompatibilityWorkaround,
|
|
1673
|
+
managedSessionName: browserRunState.managedSessionName,
|
|
1674
|
+
managedSessionNamespace: browserRunState.managedSessionNamespace,
|
|
1675
|
+
sessionMode: compiledElectron?.action === "launch" ? "fresh" : params.sessionMode ?? "auto",
|
|
1676
|
+
});
|
|
1623
1677
|
const initialArtifactManifest = browserRunState.artifactManifest;
|
|
1624
1678
|
const initialNetworkRoutesBySession = browserRunState.networkRoutesBySession;
|
|
1625
1679
|
const attachedSessionRequested = isAttachedBrowserInvocation(toolArgs)
|
|
@@ -1628,7 +1682,8 @@ export default function agentBrowserExtension(pi) {
|
|
|
1628
1682
|
&& (params.sessionMode === "fresh" || (resolvedInput.kind === "electron" && resolvedInput.compiledElectron.action === "launch"));
|
|
1629
1683
|
const reusableSessionKey = allocatesFreshManagedSession
|
|
1630
1684
|
? undefined
|
|
1631
|
-
:
|
|
1685
|
+
: getSessionContextKey(selectedPlan.sessionName, selectedPlan.namespace)
|
|
1686
|
+
?? getSessionContextKey(browserRunState.managedSessionName, browserRunState.managedSessionNamespace);
|
|
1632
1687
|
const attachedSessionKnown = reusableSessionKey !== undefined && attachedSessionKeys.has(reusableSessionKey);
|
|
1633
1688
|
let result = await runAgentBrowserTool({
|
|
1634
1689
|
ctx,
|
|
@@ -1654,7 +1709,7 @@ export default function agentBrowserExtension(pi) {
|
|
|
1654
1709
|
: extractExplicitSessionName(toolArgs);
|
|
1655
1710
|
const resultNamespace = typeof resultDetails?.namespace === "string"
|
|
1656
1711
|
? resultDetails.namespace
|
|
1657
|
-
:
|
|
1712
|
+
: selectedPlan.namespace;
|
|
1658
1713
|
if (branchRestoreStillCurrent) {
|
|
1659
1714
|
const resultBatchCloseLifecycle = getSuccessfulBatchCloseLifecycle(resultDetails?.batchSteps);
|
|
1660
1715
|
const resultSessionKey = getSessionContextKey(resultSessionName, resultNamespace) ?? resultSessionName;
|
|
@@ -1732,21 +1787,28 @@ export default function agentBrowserExtension(pi) {
|
|
|
1732
1787
|
mergeActiveElectronLaunchRecords(ownedElectronLaunchRecords, electronLaunchRecords, {
|
|
1733
1788
|
branchOwnedLaunchIds: branchOwnedElectronLaunchIds,
|
|
1734
1789
|
touchedLaunchIds: !result.isError
|
|
1735
|
-
? getTouchedElectronLaunchIds(explicitSessionName ?? browserRunState.managedSessionName, electronLaunchRecords,
|
|
1790
|
+
? getTouchedElectronLaunchIds(explicitSessionName ?? browserRunState.managedSessionName, electronLaunchRecords, resultNamespace)
|
|
1736
1791
|
: undefined,
|
|
1737
1792
|
});
|
|
1738
1793
|
if (serializeBrowserCommand)
|
|
1739
1794
|
branchStateGeneration += 1;
|
|
1740
1795
|
}
|
|
1741
|
-
return applyAgentBrowserOutputPath({ cwd: ctx.cwd, outputPath, preserveTextContent: Array.isArray(params.args) && params.args.includes("--json"), result });
|
|
1796
|
+
return applyAgentBrowserOutputPath({ cwd: ctx.cwd, outputPath, preserveTextContent: Array.isArray(params.args) && params.args.includes("--json"), result: warnRecordingPersistence(result) });
|
|
1742
1797
|
};
|
|
1743
1798
|
const closesAllSessions = commandClosesAllSessions(toolArgs, resolvedInput.toolStdin);
|
|
1744
|
-
const closeAllNamespace = closesAllSessions
|
|
1745
|
-
? resolveAgentBrowserNamespace(toolArgs, getAgentBrowserProcessEnvironment().AGENT_BROWSER_NAMESPACE)
|
|
1746
|
-
: undefined;
|
|
1747
1799
|
const runWithinSessionQueue = () => {
|
|
1748
1800
|
if (closesAllSessions)
|
|
1749
|
-
return managedSessionExecutionQueue.run(() =>
|
|
1801
|
+
return managedSessionExecutionQueue.run(() => {
|
|
1802
|
+
const plan = buildExecutionPlan(toolArgs, {
|
|
1803
|
+
freshSessionName: createFreshSessionName(managedSessionBaseName, ephemeralSessionSeed, freshSessionOrdinal + 1),
|
|
1804
|
+
managedSessionActive,
|
|
1805
|
+
managedSessionCompatibilityWorkaround,
|
|
1806
|
+
managedSessionName,
|
|
1807
|
+
managedSessionNamespace,
|
|
1808
|
+
sessionMode: params.sessionMode ?? "auto",
|
|
1809
|
+
});
|
|
1810
|
+
return callerOwnedSessionExecutionQueues.runExclusive(plan.namespace, runBrowserCommand);
|
|
1811
|
+
});
|
|
1750
1812
|
if (serializeBrowserCommand)
|
|
1751
1813
|
return managedSessionExecutionQueue.run(runBrowserCommand);
|
|
1752
1814
|
return callerOwnedSessionQueueKey
|
|
@@ -1765,10 +1827,11 @@ export default function agentBrowserExtension(pi) {
|
|
|
1765
1827
|
});
|
|
1766
1828
|
if (!artifactValidationError)
|
|
1767
1829
|
return runWithinSessionQueue();
|
|
1830
|
+
flushRecordingReservations();
|
|
1768
1831
|
return applyAgentBrowserOutputPath({
|
|
1769
1832
|
cwd: ctx.cwd,
|
|
1770
1833
|
outputPath,
|
|
1771
|
-
result: buildValidationFailureResult({
|
|
1834
|
+
result: warnRecordingPersistence(buildValidationFailureResult({
|
|
1772
1835
|
attemptedKind: resolvedInput.kind,
|
|
1773
1836
|
kind: "invalid",
|
|
1774
1837
|
redactedArgs: resolvedInput.redactedArgs,
|
|
@@ -1776,7 +1839,7 @@ export default function agentBrowserExtension(pi) {
|
|
|
1776
1839
|
toolArgs: resolvedInput.toolArgs,
|
|
1777
1840
|
toolStdin: resolvedInput.toolStdin,
|
|
1778
1841
|
validationError: artifactValidationError,
|
|
1779
|
-
}),
|
|
1842
|
+
})),
|
|
1780
1843
|
});
|
|
1781
1844
|
});
|
|
1782
1845
|
},
|
|
@@ -143,7 +143,6 @@ const COMMAND_CAPABILITIES = [
|
|
|
143
143
|
command: "keyboard",
|
|
144
144
|
eligibleForElectronHealthProbe: true,
|
|
145
145
|
eligibleForPageChangeSummary: true,
|
|
146
|
-
guardsPageRefs: true,
|
|
147
146
|
invalidatesBatchRefs: true,
|
|
148
147
|
triggersPostMutationSnapshot: true,
|
|
149
148
|
},
|
|
@@ -157,7 +156,6 @@ const COMMAND_CAPABILITIES = [
|
|
|
157
156
|
{
|
|
158
157
|
command: "mouse",
|
|
159
158
|
eligibleForElectronHealthProbe: true,
|
|
160
|
-
guardsPageRefs: true,
|
|
161
159
|
invalidatesBatchRefs: true,
|
|
162
160
|
},
|
|
163
161
|
{
|
|
@@ -181,7 +179,6 @@ const COMMAND_CAPABILITIES = [
|
|
|
181
179
|
command: "press",
|
|
182
180
|
eligibleForElectronHealthProbe: true,
|
|
183
181
|
eligibleForPageChangeSummary: true,
|
|
184
|
-
guardsPageRefs: true,
|
|
185
182
|
invalidatesBatchRefs: true,
|
|
186
183
|
triggersPostMutationSnapshot: true,
|
|
187
184
|
},
|
|
@@ -330,8 +327,11 @@ export function isRecordPageTransitionCommand(tokens) {
|
|
|
330
327
|
export function isWebMcpPageMutationCommand(tokens) {
|
|
331
328
|
return isWebMcpPageMutation(tokens[0], tokens[1]);
|
|
332
329
|
}
|
|
330
|
+
export function isWindowOrDiffPageTransitionCommand(command, subcommand) {
|
|
331
|
+
return (command === "window" && subcommand === "new") || (command === "diff" && subcommand === "url");
|
|
332
|
+
}
|
|
333
333
|
export function isRefInvalidatingBatchCommand(step) {
|
|
334
|
-
return hasCommandCapability(step[0], "invalidatesBatchRefs") || isRecordPageTransitionCommand(step) || isWebMcpPageMutationCommand(step);
|
|
334
|
+
return hasCommandCapability(step[0], "invalidatesBatchRefs") || isRecordPageTransitionCommand(step) || isWebMcpPageMutationCommand(step) || isWindowOrDiffPageTransitionCommand(step[0], step[1]);
|
|
335
335
|
}
|
|
336
336
|
export function isRefGuardedCommand(command) {
|
|
337
337
|
return hasCommandCapability(command, "guardsPageRefs");
|
|
@@ -343,12 +343,13 @@ function isWebMcpPageMutation(command, subcommand) {
|
|
|
343
343
|
return command === "webmcp" && WEBMCP_PAGE_MUTATION_SUBCOMMANDS.has(subcommand ?? "");
|
|
344
344
|
}
|
|
345
345
|
export function isNavigationObservableCommandName(command, subcommand) {
|
|
346
|
-
return hasCommandCapability(command, "navigationObservable") || isWebMcpPageMutation(command, subcommand);
|
|
346
|
+
return hasCommandCapability(command, "navigationObservable") || isWebMcpPageMutation(command, subcommand) || isWindowOrDiffPageTransitionCommand(command, subcommand);
|
|
347
347
|
}
|
|
348
348
|
export function isUnverifiedPageTransitionCommand(command, subcommand) {
|
|
349
349
|
return ["back", "connect", "eval", "forward", "reload"].includes(command ?? "")
|
|
350
350
|
|| (command === "state" && subcommand === "load")
|
|
351
351
|
|| (command === "tab" && subcommand !== undefined && !["list", "new"].includes(subcommand))
|
|
352
|
+
|| isWindowOrDiffPageTransitionCommand(command, subcommand)
|
|
352
353
|
|| isWebMcpPageMutation(command, subcommand);
|
|
353
354
|
}
|
|
354
355
|
export function isPageMutationCommand(command, subcommand) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
-
import { rm } from "node:fs/promises";
|
|
2
|
+
import { lstat, rm } from "node:fs/promises";
|
|
3
3
|
import { promisify } from "node:util";
|
|
4
4
|
import { fetchCdpJson, parseCdpTargets, parseCdpVersion } from "./cdp.js";
|
|
5
5
|
import { ELECTRON_PROFILE_DIR_PREFIX } from "./launch.js";
|
|
@@ -33,6 +33,14 @@ async function isPortAlive(port) {
|
|
|
33
33
|
}
|
|
34
34
|
export async function inspectElectronLaunchStatus(record) {
|
|
35
35
|
const cdp = await isPortAlive(record.port);
|
|
36
|
+
let userDataDirState;
|
|
37
|
+
try {
|
|
38
|
+
await lstat(record.userDataDir);
|
|
39
|
+
userDataDirState = "present";
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
userDataDirState = error.code === "ENOENT" ? "absent" : "unknown";
|
|
43
|
+
}
|
|
36
44
|
return {
|
|
37
45
|
cleanupState: record.cleanupState,
|
|
38
46
|
launchId: record.launchId,
|
|
@@ -41,6 +49,7 @@ export async function inspectElectronLaunchStatus(record) {
|
|
|
41
49
|
port: record.port,
|
|
42
50
|
portAlive: cdp.version !== undefined,
|
|
43
51
|
targets: cdp.targets,
|
|
52
|
+
userDataDirState,
|
|
44
53
|
version: cdp.version,
|
|
45
54
|
};
|
|
46
55
|
}
|
|
@@ -11,7 +11,7 @@ export function createAgentBrowserParamsSchema(Type = JsonSchema, StringEnum = l
|
|
|
11
11
|
maxLength: AGENT_BROWSER_SCRIPT_CODE_MAX_BYTES,
|
|
12
12
|
})),
|
|
13
13
|
args: Type.Optional(Type.Array(Type.String(), {
|
|
14
|
-
description: "Raw agent-browser argv only: no binary, shell operators, or --json. Start with open → snapshot -i → act on current @refs; re-snapshot after page changes.",
|
|
14
|
+
description: "Raw agent-browser argv only: no binary, shell operators, or --json. Start with open → snapshot -i → act on current @refs; re-snapshot after page changes. Input: type <selector> <text>, or keyboard type <text> at current focus. Wait duration: wait <ms> (no --time). Artifacts: screenshot [selector] [path] [--full/-f]; record start <path> [url]; record restart <path> [url]; record stop. Paths are positional (no --path); use --full, not --full-page.",
|
|
15
15
|
minItems: 1,
|
|
16
16
|
})),
|
|
17
17
|
semanticAction: Type.Optional(Type.Object({
|
|
@@ -21,10 +21,23 @@ export function createAgentBrowserParamsSchema(Type = JsonSchema, StringEnum = l
|
|
|
21
21
|
values: Type.Optional(Type.Array(Type.String(), { description: "Select options; required for select by label.", minItems: 1 })),
|
|
22
22
|
selector: Type.Optional(Type.String({ description: "Direct selector or @ref." })),
|
|
23
23
|
text: Type.Optional(Type.String({ description: "Fill text." })),
|
|
24
|
-
role: Type.Optional(Type.String({ description: "Role locator;
|
|
24
|
+
role: Type.Optional(Type.String({ description: "Role for locator=role; select needs combobox or listbox." })),
|
|
25
25
|
name: Type.Optional(Type.String({ description: "Accessible name." })),
|
|
26
26
|
session: Type.Optional(Type.String({ description: "Upstream session name." })),
|
|
27
|
-
}, {
|
|
27
|
+
}, {
|
|
28
|
+
additionalProperties: false,
|
|
29
|
+
// Pi normalizes optional nulls through properties, not union branches.
|
|
30
|
+
anyOf: [
|
|
31
|
+
Type.Object({
|
|
32
|
+
action: StringEnum(["select"]),
|
|
33
|
+
locator: Type.Optional(StringEnum(["role", "label"])),
|
|
34
|
+
}, { not: { required: ["text"] } }),
|
|
35
|
+
Type.Object({
|
|
36
|
+
action: StringEnum(["check", "click", "fill"]),
|
|
37
|
+
}, { not: { required: ["values"] } }),
|
|
38
|
+
],
|
|
39
|
+
description: "Stable locator or direct-selector action. values only with action=select.",
|
|
40
|
+
})),
|
|
28
41
|
qa: Type.Optional(Type.Union([
|
|
29
42
|
Type.Object({
|
|
30
43
|
attached: Type.Literal(true),
|
|
@@ -112,7 +125,7 @@ export function createAgentBrowserParamsSchema(Type = JsonSchema, StringEnum = l
|
|
|
112
125
|
locator: Type.Optional(StringEnum(AGENT_BROWSER_SEMANTIC_LOCATORS, { description: "Locator when selector is omitted." })),
|
|
113
126
|
role: Type.Optional(Type.String({ description: "Role locator." })),
|
|
114
127
|
name: Type.Optional(Type.String({ description: "Accessible name." })),
|
|
115
|
-
text: Type.Optional(Type.String({ description: "Fill text
|
|
128
|
+
text: Type.Optional(Type.String({ description: "Fill/type text; assertText uses only text, not selector/locator fields." })),
|
|
116
129
|
value: Type.Optional(Type.String({ description: "Select option or locator value." })),
|
|
117
130
|
values: Type.Optional(Type.Array(Type.String(), { description: "Select options.", minItems: 1 })),
|
|
118
131
|
path: Type.Optional(Type.String({ description: "Download or screenshot path." })),
|
|
@@ -120,10 +133,10 @@ export function createAgentBrowserParamsSchema(Type = JsonSchema, StringEnum = l
|
|
|
120
133
|
press: Type.Optional(Type.String({ description: "Key to press after typing." })),
|
|
121
134
|
milliseconds: Type.Optional(Type.Number({ description: "Wait duration in milliseconds." })),
|
|
122
135
|
}, { additionalProperties: false }), { minItems: 1 }),
|
|
123
|
-
}, { additionalProperties: false, description: "Constrained multi-step batch." })),
|
|
124
|
-
stdin: Type.Optional(Type.String({ description: "Raw
|
|
136
|
+
}, { additionalProperties: false, description: "Constrained multi-step batch. Clicks can stale later refs; split and re-snapshot before using them." })),
|
|
137
|
+
stdin: Type.Optional(Type.String({ description: "For batch, a JSON array of token arrays, e.g. [[\"get\",\"title\"]]. Raw text only for eval --stdin or auth save --password-stdin; unavailable with structured modes and electron." })),
|
|
125
138
|
outputPath: Type.Optional(Type.String({ description: "Workspace-relative or absolute result-data path; keep it distinct from screenshot, download, recording, and other browser artifact destinations.", minLength: 1 })),
|
|
126
|
-
timeoutMs: Type.Optional(Type.Integer({ description: "Wrapper timeout in ms; exceed explicit waits. Electron
|
|
139
|
+
timeoutMs: Type.Optional(Type.Integer({ description: "Wrapper timeout in ms; exceed explicit waits. electron.list has no configurable timeout; other Electron actions use electron.timeoutMs.", minimum: 1 })),
|
|
127
140
|
sessionMode: Type.Optional(StringEnum(["auto", "fresh"], {
|
|
128
141
|
description: "auto reuses the managed session; fresh starts one for launch-only flags, then makes it the managed session.",
|
|
129
142
|
default: DEFAULT_SESSION_MODE,
|
|
@@ -150,7 +150,6 @@ export const MANAGED_RESTORE_INCOMPATIBLE_ENVS = [
|
|
|
150
150
|
"AGENT_BROWSER_PROFILE",
|
|
151
151
|
"AGENT_BROWSER_STATE",
|
|
152
152
|
"AGENT_BROWSER_CDP",
|
|
153
|
-
"AGENT_BROWSER_NAMESPACE",
|
|
154
153
|
"AGENT_BROWSER_SESSION_NAME",
|
|
155
154
|
"AGENT_BROWSER_PROVIDER",
|
|
156
155
|
"AGENT_BROWSER_EXECUTABLE_PATH",
|