pi-agent-browser-native 0.6.6 → 0.6.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +40 -1
  2. package/README.md +21 -6
  3. package/dist/extensions/agent-browser/index.js +138 -75
  4. package/dist/extensions/agent-browser/lib/command-taxonomy.js +6 -5
  5. package/dist/extensions/agent-browser/lib/electron/cleanup.js +10 -1
  6. package/dist/extensions/agent-browser/lib/input-modes/params.js +20 -7
  7. package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +0 -1
  8. package/dist/extensions/agent-browser/lib/managed-session-restore.js +13 -12
  9. package/dist/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.js +6 -25
  10. package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +2 -2
  11. package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +2 -3
  12. package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +10 -3
  13. package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +3 -0
  14. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/wait-timeouts.js +1 -1
  15. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +104 -117
  16. package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +64 -36
  17. package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +100 -135
  18. package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +3 -1
  19. package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +3 -1
  20. package/dist/extensions/agent-browser/lib/page-target-validation.js +10 -10
  21. package/dist/extensions/agent-browser/lib/parsing.js +7 -0
  22. package/dist/extensions/agent-browser/lib/playbook.js +6 -9
  23. package/dist/extensions/agent-browser/lib/process.js +28 -113
  24. package/dist/extensions/agent-browser/lib/recording-reservations.js +3 -1
  25. package/dist/extensions/agent-browser/lib/results/action-recommendations.js +5 -2
  26. package/dist/extensions/agent-browser/lib/results/envelope.js +9 -4
  27. package/dist/extensions/agent-browser/lib/results/next-actions.js +8 -0
  28. package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +45 -43
  29. package/dist/extensions/agent-browser/lib/results/presentation/batch.js +2 -1
  30. package/dist/extensions/agent-browser/lib/results/presentation/errors.js +10 -2
  31. package/dist/extensions/agent-browser/lib/results/presentation.js +9 -2
  32. package/dist/extensions/agent-browser/lib/results/recovery-actions.js +4 -4
  33. package/dist/extensions/agent-browser/lib/runtime.js +18 -2
  34. package/dist/extensions/agent-browser/lib/session-page-state.js +29 -10
  35. package/docs/ARCHITECTURE.md +8 -5
  36. package/docs/COMMAND_REFERENCE.md +25 -16
  37. package/docs/ELECTRON.md +6 -6
  38. package/docs/RELEASE.md +14 -5
  39. package/docs/REQUIREMENTS.md +1 -1
  40. package/docs/SUPPORT_MATRIX.md +18 -5
  41. package/docs/TOOL_CONTRACT.md +38 -25
  42. package/package.json +5 -1
@@ -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.explicitNamespace) ?? options.explicitSessionName))
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 recordingSessionTombstonesToPersist = new Map();
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
- recordingSessionTombstonesToPersist.delete(key);
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
- if (transition.state === "closed")
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 nextActions = Array.isArray(details.nextActions) ? [...details.nextActions] : [];
889
- if (nextActions.some((action) => action.id === "stop-pending-recording"))
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: "stop-pending-recording",
895
- reason: "Stop the active recording so the requested video can be finalized and verified on disk.",
896
- safety: "The file remains pending until record stop succeeds; verify details.artifactVerification afterward.",
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 = "An active recording remains open. Use the exact stop-pending-recording payload in details.nextActions before leaving this session.";
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
- const terminalKey = getAgentBrowserSessionIdentityKey(sessionName, namespace);
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 [key, reservation] of recordingSessionTombstones) {
1111
- if (restoredRecordingState.terminal.has(key))
1112
- recordingSessionTombstonesToPersist.delete(key);
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.keys())
1157
+ for (const [key, reservation] of recordingSessionTombstones) {
1121
1158
  restoredRecordingState.active.delete(key);
1122
- for (const [key, reservation] of activeRecordingReservations)
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(() => recoverScriptSessionLeasesWithinQueue(ctx)));
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
- activeRecordingReservations = new Map();
1288
- recordingSessionTombstones = new Map();
1289
- recordingSessionTombstonesToPersist = new Map();
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(() => closeScriptSessionLeaseWithinQueue(sessionName, ctx.cwd)));
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 explicitNamespace = extractExplicitNamespace(toolArgs);
1629
+ const callerOwnedSessionNamespace = explicitSessionName
1630
+ ? resolveAgentBrowserNamespace(toolArgs, getAgentBrowserProcessEnvironment().AGENT_BROWSER_NAMESPACE)
1631
+ : undefined;
1584
1632
  const serializeBrowserCommand = shouldSerializeBrowserCommand({
1585
- explicitNamespace,
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
- : callerOwnedSessionQueueKey ?? getSessionContextKey(browserRunState.managedSessionName, browserRunState.managedSessionNamespace);
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
- : resolveAgentBrowserNamespace(toolArgs, getAgentBrowserProcessEnvironment().AGENT_BROWSER_NAMESPACE);
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, explicitSessionName ? resolveAgentBrowserNamespace(toolArgs, getAgentBrowserProcessEnvironment().AGENT_BROWSER_NAMESPACE) : browserRunState.managedSessionNamespace)
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(() => callerOwnedSessionExecutionQueues.runExclusive(closeAllNamespace, runBrowserCommand));
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; alternative to value." })),
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
- }, { additionalProperties: false, description: "Stable locator or direct-selector action." })),
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 or assertText target." })),
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 stdin for batch, eval --stdin, or auth save --password-stdin; unavailable with structured modes and electron." })),
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 uses electron.timeoutMs.", minimum: 1 })),
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",