pi-agent-browser-native 0.8.1 → 0.8.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.
Files changed (32) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +16 -7
  3. package/dist/extensions/agent-browser/index.js +440 -163
  4. package/dist/extensions/agent-browser/lib/browser-journal.js +418 -0
  5. package/dist/extensions/agent-browser/lib/browser-session-conversion.js +381 -0
  6. package/dist/extensions/agent-browser/lib/browser-transcript.js +119 -19
  7. package/dist/extensions/agent-browser/lib/input-modes/job.js +49 -2
  8. package/dist/extensions/agent-browser/lib/input-modes/params.js +32 -35
  9. package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +25 -5
  10. package/dist/extensions/agent-browser/lib/managed-session-restore.js +20 -5
  11. package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +97 -78
  12. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +23 -2
  13. package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +52 -17
  14. package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +1 -1
  15. package/dist/extensions/agent-browser/lib/orchestration/script-mode.js +13 -8
  16. package/dist/extensions/agent-browser/lib/playbook.js +9 -2
  17. package/dist/extensions/agent-browser/lib/prompt-policy.js +0 -14
  18. package/dist/extensions/agent-browser/lib/results/presentation/large-output.js +2 -1
  19. package/dist/extensions/agent-browser/lib/session-page-state.js +95 -157
  20. package/dist/extensions/agent-browser/lib/temp.js +14 -6
  21. package/dist/extensions/agent-browser/lib/tool-surface.js +15 -11
  22. package/dist/extensions/agent-browser/lib/web-search.js +0 -8
  23. package/docs/ARCHITECTURE.md +15 -9
  24. package/docs/COMMAND_REFERENCE.md +10 -6
  25. package/docs/RELEASE.md +3 -3
  26. package/docs/REQUIREMENTS.md +1 -1
  27. package/docs/SESSION_STORAGE.md +60 -0
  28. package/docs/SUPPORT_MATRIX.md +8 -5
  29. package/docs/TOOL_CONTRACT.md +19 -14
  30. package/package.json +9 -5
  31. package/scripts/build.mjs +4 -3
  32. package/scripts/convert-browser-session.mjs +22 -0
@@ -1,19 +1,21 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
+ import { randomUUID } from "node:crypto";
2
3
  import { existsSync, readFileSync } from "node:fs";
3
4
  import { dirname, join } from "node:path";
4
5
  import { fileURLToPath } from "node:url";
5
6
  import { Text } from "@earendil-works/pi-tui";
6
7
  import { batchHasSuccessfulCloseAll, getSuccessfulBatchCloseLifecycle } from "./lib/batch-lifecycle.js";
7
- import { PROJECT_RULE_PROMPT, buildBrowserDefaultProfileGuideline, buildBrowserExecutablePathGuideline, buildToolPromptGuidelines, } from "./lib/playbook.js";
8
+ import { PROJECT_RULE_PROMPT, ADVANCED_TOOL_PROMPT_GUIDELINES, QUICK_START_GUIDELINES, SHARED_BROWSER_PLAYBOOK_GUIDELINES, WEB_SEARCH_TOOL_PROMPT_GUIDELINES, WRAPPER_TAB_RECOVERY_BEHAVIOR, buildBrowserDefaultProfileGuideline, buildBrowserExecutablePathGuideline, buildToolPromptGuidelines, } from "./lib/playbook.js";
8
9
  import { SessionPageState } from "./lib/session-page-state.js";
9
- import { appendBrowserTransition, getBrowserResultMessage } from "./lib/browser-transcript.js";
10
+ import { appendBrowserTransition, applyArtifactChanges, artifactChanges, browserStateEffects, getBrowserRecord, getBrowserResultMessage, pageChange, snapshotDefinition } from "./lib/browser-transcript.js";
11
+ import { appendBrowserRecord, captureBrowserBranch, hasPublishedBrowserJournal, readBrowserEntries, requirePublishedBrowserJournal } from "./lib/browser-journal.js";
10
12
  import { buildExecutionPlan, canUseHeadlessCompatibilityUserAgent, createEphemeralSessionSeed, createFreshSessionName, createImplicitSessionName, extractUpstreamCommandTokens, getImplicitSessionCloseTimeoutMs, getImplicitSessionIdleTimeoutMs, isRestorableManagedSessionName, restoreManagedSessionStateFromBranch, validateToolArgs, redactInvocationArgs, redactSensitiveText, redactSensitiveValue, isPlainTextInspectionArgs, } from "./lib/runtime.js";
11
13
  import { deleteIdentityKeysInNamespace, extractExplicitNamespace, extractExplicitSessionName, getAgentBrowserSessionIdentityKey, isAgentBrowserSessionIdentityKeyInNamespace, isUpstreamEnvFlagEnabled, resolveAgentBrowserNamespace, scanUpstreamGlobalFlagOccurrences } from "./lib/argv-grammar.js";
12
14
  import { parseArgvDescriptor } from "./lib/argv-descriptor.js";
13
15
  import { needsManagedSession } from "./lib/command-policy.js";
14
16
  import { ManagedSessionRestoreState, resolveOwnedManagedSessionContext, withOwnedManagedSessionContext } from "./lib/managed-session-restore.js";
15
17
  import { isRecord } from "./lib/parsing.js";
16
- import { getAgentBrowserProcessTimeoutMs, runAgentBrowserProcess } from "./lib/process.js";
18
+ import { getAgentBrowserProcessTimeoutMs, resolveAgentBrowserSocketDir, runAgentBrowserProcess } from "./lib/process.js";
17
19
  import { getCommandAwareProcessTimeoutMs } from "./lib/orchestration/browser-run/prepare/wait-timeouts.js";
18
20
  import { getAgentBrowserProcessEnvironment, withAgentBrowserProcessEnvironment, withIsolatedAgentBrowserEnvironment } from "./lib/process-environment.js";
19
21
  import { withNativeSessionDefaults } from "./lib/orchestration/native-session-defaults.js";
@@ -21,7 +23,7 @@ import { getBrowserCwdError, resolveExecutionCwd, restoreManagedSessionCwd } fro
21
23
  import { resolveOperationPaths } from "./lib/orchestration/operation-paths.js";
22
24
  import { closeManagedSession, inspectManagedSessionDaemon } from "./lib/orchestration/browser-run/managed-session-daemon-policy.js";
23
25
  import { MINIMUM_AGENT_BROWSER_VERSION, SUPPORTED_AGENT_BROWSER_VERSION_LABEL, TARGET_AGENT_BROWSER_VERSION, getAgentBrowserVersionValidationError, parseAgentBrowserVersionOutput, } from "./lib/upstream-version.js";
24
- import { buildPromptPolicy, getLatestUserPrompt, shouldAppendBrowserSystemPrompt } from "./lib/prompt-policy.js";
26
+ import { buildPromptPolicy, getLatestUserPrompt } from "./lib/prompt-policy.js";
25
27
  import { isCloseAllCommand, isCloseCommand } from "./lib/command-taxonomy.js";
26
28
  import { hasLaunchScopedFlagToken } from "./lib/launch-scoped-flags.js";
27
29
  import { cleanupSecureTempArtifacts } from "./lib/temp.js";
@@ -34,7 +36,7 @@ import { buildValidationFailureResult, resolveAgentBrowserInput } from "./lib/or
34
36
  import { applyAgentBrowserOutputPath, canWriteAgentBrowserOutput, normalizeRequestedOutputPath } from "./lib/orchestration/output-file.js";
35
37
  import { appendScriptSessionLease, createBrowserCodeOutput, getScriptSessionLeasesFromBranch } from "./lib/orchestration/script-mode.js";
36
38
  import { resolveBrowserExecutionIdentity, withBrowserExecutionLock, withBrowserExecutionLocks } from "./lib/managed-session-policy-lock.js";
37
- import { registerAgentBrowserToolSurface } from "./lib/tool-surface.js";
39
+ import { AGENT_BROWSER_INSTRUCTION_GROUP, AGENT_BROWSER_TOOL_INVENTORY, registerAgentBrowserToolSurface } from "./lib/tool-surface.js";
38
40
  import { formatSessionArtifactRetentionSummary, getSessionArtifactManifestEntryKey, isPendingRecordingCommand, isSessionArtifactManifest, mergeSessionArtifactManifest, retirePendingRecordingManifestEntries } from "./lib/results/artifact-manifest.js";
39
41
  import { appendUniqueAgentBrowserNextActions, applyNamespaceToNextActions, applySessionToNextActions, buildNextToolAction } from "./lib/results/next-actions.js";
40
42
  import { canRegisterWebSearchTool, loadAgentBrowserConfigSync } from "./lib/config.js";
@@ -174,19 +176,54 @@ function getResultFileArtifacts(result) {
174
176
  const details = isRecord(result.details) ? result.details : undefined;
175
177
  return Array.isArray(details?.artifacts) ? details.artifacts.filter(isResultFileArtifact) : [];
176
178
  }
177
- function restoreArtifactManifestFromBranch(branch) {
178
- let restoredManifest;
179
- for (const entry of branch) {
180
- const message = getBrowserResultMessage(entry);
181
- if (!message)
182
- continue;
183
- const details = isRecord(message.details) ? message.details : undefined;
184
- if (isSessionArtifactManifest(details?.artifactManifest) && (!restoredManifest || details.artifactManifest.updatedAtMs >= restoredManifest.updatedAtMs)) {
185
- restoredManifest = details.artifactManifest;
179
+ function invocationArtifactManifest(current, previous, details, retainedReceipts) {
180
+ if (!current)
181
+ return undefined;
182
+ const paths = new Set();
183
+ for (const row of [details, ...(Array.isArray(details.batchSteps) ? details.batchSteps.filter(isRecord) : [])]) {
184
+ if (typeof row.fullOutputPath === "string")
185
+ paths.add(row.fullOutputPath);
186
+ for (const path of Array.isArray(row.fullOutputPaths) ? row.fullOutputPaths : [])
187
+ if (typeof path === "string")
188
+ paths.add(path);
189
+ for (const artifact of Array.isArray(row.artifacts) ? row.artifacts.filter(isRecord) : []) {
190
+ if (typeof artifact.absolutePath === "string")
191
+ paths.add(artifact.absolutePath);
192
+ if (typeof artifact.path === "string")
193
+ paths.add(artifact.path);
186
194
  }
187
195
  }
196
+ const changed = new Set((artifactChanges(previous, current)?.upserts ?? []).map(getSessionArtifactManifestEntryKey));
197
+ for (const row of retainedReceipts?.entries ?? [])
198
+ changed.add(getSessionArtifactManifestEntryKey(row));
199
+ const entries = current.entries.filter(row => changed.has(getSessionArtifactManifestEntryKey(row)) || paths.has(row.path) || (row.absolutePath !== undefined && paths.has(row.absolutePath)));
200
+ return entries.length ? redactSensitiveValue({
201
+ ...current, entries, liveCount: entries.filter(row => row.retentionState === "live").length,
202
+ evictedCount: entries.filter(row => row.retentionState === "evicted").length,
203
+ }) : undefined;
204
+ }
205
+ function restoreArtifactManifestFromBranch(branch) {
206
+ let restoredManifest;
207
+ for (const entry of branch)
208
+ restoredManifest = applyArtifactChanges(restoredManifest, getBrowserRecord(entry)?.event.artifacts);
188
209
  return restoredManifest;
189
210
  }
211
+ function browserPageChanges(working, prior, touched) {
212
+ const pages = working.views();
213
+ const keys = new Set(touched);
214
+ for (const [key, page] of pages) {
215
+ const before = prior.get(key);
216
+ if (JSON.stringify(pageChange(key, page, before?.refSnapshot?.snapshotId)) !== JSON.stringify(pageChange(key, before ?? {}, before?.refSnapshot?.snapshotId)))
217
+ keys.add(key);
218
+ }
219
+ for (const key of prior.keys())
220
+ if (!pages.has(key))
221
+ keys.add(key);
222
+ const changes = [...keys].map(key => pages.has(key) ? pageChange(key, pages.get(key), prior.get(key)?.refSnapshot?.snapshotId) : { key, refs: { kind: "invalidate" }, clear: true });
223
+ const capture = changes.find(page => page.refs.kind === "replace");
224
+ const snapshot = capture ? pages.get(capture.key)?.refSnapshot : undefined;
225
+ return { pages: changes, ...(snapshot ? { snapshot: snapshotDefinition(snapshot) } : {}) };
226
+ }
190
227
  function getRecognizedCompatibilityWorkaround(value) {
191
228
  const workaround = isRecord(value) ? value : undefined;
192
229
  return (workaround?.id === "chatgpt-headless-user-agent" || workaround?.id === "cloudflare-headless-user-agent") && typeof workaround.reason === "string"
@@ -346,7 +383,19 @@ function trackOwnedManagedSession(sessions, sessionName, cwd, options = {}) {
346
383
  : existing?.compatibilityWorkaround;
347
384
  const headedManagedAutosaveDisabled = options.headedManagedAutosaveDisabled ?? existing?.headedManagedAutosaveDisabled;
348
385
  const headedManagedAutosaveInterval = options.headedManagedAutosaveInterval ?? existing?.headedManagedAutosaveInterval;
349
- sessions.set(key, { branchOwned, compatibilityWorkaround, cwd, headedManagedAutosaveDisabled, headedManagedAutosaveInterval, namespace: options.namespace, sessionName });
386
+ sessions.set(key, { branchOwned, compatibilityWorkaround, cwd, headedManagedAutosaveDisabled, headedManagedAutosaveInterval, namespace: options.namespace, sessionName, socketDir: options.socketDir ?? existing?.socketDir });
387
+ }
388
+ function restoreManagedSessionSocketDir(branch, sessionName, namespace) {
389
+ const key = getAgentBrowserSessionIdentityKey(sessionName, namespace);
390
+ for (let index = branch.length - 1; index >= 0; index -= 1) {
391
+ const details = getBrowserResultMessage(branch[index])?.details;
392
+ if (!isRecord(details) || typeof details.sessionName !== "string")
393
+ continue;
394
+ if (getAgentBrowserSessionIdentityKey(details.sessionName, typeof details.namespace === "string" ? details.namespace : undefined) === key
395
+ && typeof details.managedSessionSocketDir === "string")
396
+ return details.managedSessionSocketDir;
397
+ }
398
+ return undefined;
350
399
  }
351
400
  function untrackOwnedManagedSession(sessions, sessionName, namespace) {
352
401
  if (!sessionName)
@@ -382,6 +431,7 @@ function syncOwnedManagedSessionsFromResult(sessions, result, cwd) {
382
431
  headedManagedAutosaveDisabled: details?.managedSessionHeadedAutosaveDisabled === true,
383
432
  headedManagedAutosaveInterval: typeof details?.managedSessionHeadedAutosaveInterval === "string" ? details.managedSessionHeadedAutosaveInterval : undefined,
384
433
  namespace,
434
+ socketDir: typeof details?.managedSessionSocketDir === "string" ? details.managedSessionSocketDir : undefined,
385
435
  });
386
436
  }
387
437
  if (succeeded && status === "closed") {
@@ -394,6 +444,8 @@ function getTouchedElectronLaunchIds(sessionName, records, namespace) {
394
444
  }
395
445
  function mergeActiveElectronLaunchRecords(target, source, options = {}) {
396
446
  for (const record of getActiveElectronRecords(source)) {
447
+ if (options.ownerSessionId !== undefined && record.ownerSessionId !== options.ownerSessionId)
448
+ continue;
397
449
  const alreadyRuntimeOwned = target.has(record.launchId) && options.branchOwnedLaunchIds?.has(record.launchId) === false;
398
450
  target.set(record.launchId, record);
399
451
  if (options.branchOwnedLaunchIds) {
@@ -450,8 +502,10 @@ function shouldSerializeElectronHostInput(compiledElectron) {
450
502
  return compiledElectron?.action === "status" || compiledElectron?.action === "probe" || compiledElectron?.action === "cleanup";
451
503
  }
452
504
  function getElectronHostLaunchRecordsForInput(options) {
505
+ if (options.compiledElectron?.action === "cleanup") {
506
+ return new Map([...mergeElectronLaunchRecordMaps(options.branchRecords, options.ownedRecords)].filter(([, record]) => record.ownerSessionId === options.ownerSessionId));
507
+ }
453
508
  if (options.compiledElectron?.action === "status" ||
454
- options.compiledElectron?.action === "cleanup" ||
455
509
  (options.compiledElectron?.action === "probe" && options.compiledElectron.launchId)) {
456
510
  return mergeElectronLaunchRecordMaps(options.branchRecords, options.ownedRecords);
457
511
  }
@@ -631,7 +685,7 @@ async function closeOwnedManagedSessionsExcept(sessions, restoreState, keepSessi
631
685
  for (const [key, owner] of [...sessions]) {
632
686
  if (key === keepKey)
633
687
  continue;
634
- const error = await closeManagedSession({ cwd: owner.cwd, headedManagedAutosaveInterval: owner.headedManagedAutosaveInterval, namespace: owner.namespace, preserveAttachedBrowserSession: attachedSessionKeys.has(key), restoreState, sessionName: owner.sessionName, timeoutMs });
688
+ const error = await closeManagedSession({ cwd: owner.cwd, headedManagedAutosaveInterval: owner.headedManagedAutosaveInterval, namespace: owner.namespace, preserveAttachedBrowserSession: attachedSessionKeys.has(key), restoreState, sessionName: owner.sessionName, socketDir: owner.socketDir, timeoutMs });
635
689
  if (!error) {
636
690
  sessions.delete(key);
637
691
  onClosed?.(owner);
@@ -812,13 +866,6 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
812
866
  cwd: process.cwd(),
813
867
  includeProjectConfig: false,
814
868
  });
815
- const webSearchToolAvailable = canRegisterWebSearchTool(agentBrowserConfig);
816
- const toolPromptGuidelines = buildToolPromptGuidelines({
817
- browserDefaultProfile: agentBrowserConfig.trustedBrowserDefaultProfile,
818
- browserExecutablePath: agentBrowserConfig.trustedBrowserExecutablePath,
819
- includeWebSearch: webSearchToolAvailable,
820
- docs: getInstalledDocsPaths(),
821
- });
822
869
  const implicitSessionIdleTimeoutMs = String(getImplicitSessionIdleTimeoutMs());
823
870
  const implicitSessionCloseTimeoutMs = getImplicitSessionCloseTimeoutMs();
824
871
  let webSearchToolRegistered = false;
@@ -832,6 +879,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
832
879
  let managedSessionNamespace;
833
880
  let freshSessionOrdinal = 0;
834
881
  let sessionPageState = new SessionPageState();
882
+ let browserReplayError;
835
883
  let traceOwners = new Map();
836
884
  let artifactManifest;
837
885
  let activeRecordingReservations = new Map();
@@ -841,6 +889,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
841
889
  let networkRoutesBySession = new Map();
842
890
  let electronLaunchRecords = new Map();
843
891
  let ownedElectronLaunchRecords = new Map();
892
+ let browserOwnerSessionId = "";
844
893
  let branchOwnedElectronLaunchIds = new Set();
845
894
  let electronChildProcesses = new Map();
846
895
  const managedSessionRestoreState = new ManagedSessionRestoreState();
@@ -1054,13 +1103,13 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1054
1103
  }));
1055
1104
  if (closeError) {
1056
1105
  try {
1057
- appendScriptSessionLease(pi, sessionName, "failed");
1106
+ appendScriptSessionLease(pi, sessionName, "failed", browserOwnerSessionId);
1058
1107
  }
1059
1108
  catch { }
1060
1109
  return redactSensitiveText(closeError);
1061
1110
  }
1062
1111
  try {
1063
- appendScriptSessionLease(pi, sessionName, "closed");
1112
+ appendScriptSessionLease(pi, sessionName, "closed", browserOwnerSessionId);
1064
1113
  }
1065
1114
  catch {
1066
1115
  managedSessionRestoreState.disable(sessionName);
@@ -1076,7 +1125,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1076
1125
  const pendingSessionNames = new Set([...ownedManagedSessions.values()]
1077
1126
  .map((session) => session.sessionName)
1078
1127
  .filter(isAgentBrowserScriptSessionName));
1079
- for (const lease of getScriptSessionLeasesFromBranch(ctx.sessionManager.getBranch()).values()) {
1128
+ for (const lease of getScriptSessionLeasesFromBranch(await readBrowserEntries(ctx.sessionManager), ctx.sessionManager.getSessionId()).values()) {
1080
1129
  if (lease.cleanup !== "closed")
1081
1130
  pendingSessionNames.add(lease.sessionName);
1082
1131
  }
@@ -1086,7 +1135,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1086
1135
  await closeScriptSessionLeaseWithinQueue(sessionName, ctx.cwd);
1087
1136
  }
1088
1137
  };
1089
- const restoreBranchBackedState = (ctx, options) => {
1138
+ const restoreBranchBackedState = async (ctx, options) => {
1090
1139
  branchRestoreGeneration += 1;
1091
1140
  branchStateGeneration += 1;
1092
1141
  const previousManagedSessionActive = managedSessionActive;
@@ -1094,7 +1143,17 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1094
1143
  const previousFreshSessionOrdinal = freshSessionOrdinal;
1095
1144
  const previousAttachedSessionKeys = attachedSessionKeys;
1096
1145
  managedSessionBaseName = createImplicitSessionName(ctx.sessionManager.getSessionId(), ctx.cwd, ephemeralSessionSeed);
1097
- const branch = ctx.sessionManager.getBranch();
1146
+ browserOwnerSessionId = ctx.sessionManager.getSessionId();
1147
+ let branch;
1148
+ try {
1149
+ branch = await readBrowserEntries(ctx.sessionManager);
1150
+ browserReplayError = undefined;
1151
+ }
1152
+ catch (error) {
1153
+ browserReplayError = error instanceof Error ? error.message : String(error);
1154
+ sessionPageState.reset();
1155
+ throw error;
1156
+ }
1098
1157
  const branchResourceEvents = collectBranchManagedResourceEvents(branch);
1099
1158
  const restoredState = restoreManagedSessionStateFromBranch(branch, managedSessionBaseName);
1100
1159
  managedSessionRestoreState.replace(restoredState.managedSessionRestoreDisabledIdentities, {
@@ -1185,6 +1244,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1185
1244
  headedManagedAutosaveDisabled: restoreManagedSessionHeadedAutosaveDisabledFromBranch(branch, identity.sessionName, identity.namespace),
1186
1245
  headedManagedAutosaveInterval: restoreManagedSessionHeadedAutosaveIntervalFromBranch(branch, identity.sessionName, identity.namespace),
1187
1246
  namespace: identity.namespace,
1247
+ socketDir: restoreManagedSessionSocketDir(branch, identity.sessionName, identity.namespace),
1188
1248
  });
1189
1249
  }
1190
1250
  if (restoredState.active) {
@@ -1194,6 +1254,32 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1194
1254
  headedManagedAutosaveDisabled: managedSessionHeadedAutosaveDisabled,
1195
1255
  headedManagedAutosaveInterval: managedSessionHeadedAutosaveInterval,
1196
1256
  namespace: restoredState.namespace,
1257
+ socketDir: restoreManagedSessionSocketDir(branch, restoredState.sessionName, restoredState.namespace),
1258
+ });
1259
+ }
1260
+ const pendingOwnedBegins = new Map();
1261
+ for (const entry of branch) {
1262
+ const event = getBrowserRecord(entry)?.event;
1263
+ if (!event)
1264
+ continue;
1265
+ if (event.phase === "begin" && event.state.ownerSessionId === browserOwnerSessionId && event.state.wrapperManaged === true)
1266
+ pendingOwnedBegins.set(event.operationId, event);
1267
+ else if (event.phase === "finish")
1268
+ pendingOwnedBegins.delete(event.operationId);
1269
+ for (const page of event.pages ?? [])
1270
+ if (page.clear) {
1271
+ for (const [id, begin] of pendingOwnedBegins)
1272
+ if (begin.pages?.some(candidate => candidate.key === page.key))
1273
+ pendingOwnedBegins.delete(id);
1274
+ }
1275
+ }
1276
+ for (const begin of pendingOwnedBegins.values()) {
1277
+ if (typeof begin.state.sessionName !== "string")
1278
+ continue;
1279
+ // The persisted wrapper-selected intent proves cleanup scope, not launch success.
1280
+ trackOwnedManagedSession(ownedManagedSessions, begin.state.sessionName, typeof begin.state.managedSessionCwd === "string" ? begin.state.managedSessionCwd : ctx.cwd, {
1281
+ branchOwned: true, namespace: typeof begin.state.namespace === "string" ? begin.state.namespace : undefined,
1282
+ socketDir: typeof begin.state.managedSessionSocketDir === "string" ? begin.state.managedSessionSocketDir : undefined,
1197
1283
  });
1198
1284
  }
1199
1285
  for (const record of getActiveElectronRecords(electronLaunchRecords)) {
@@ -1208,12 +1294,24 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1208
1294
  branchOwned: true,
1209
1295
  headedManagedAutosaveDisabled: restoreManagedSessionHeadedAutosaveDisabledFromBranch(branch, record.sessionName),
1210
1296
  headedManagedAutosaveInterval: restoreManagedSessionHeadedAutosaveIntervalFromBranch(branch, record.sessionName),
1297
+ socketDir: restoreManagedSessionSocketDir(branch, record.sessionName),
1211
1298
  });
1212
1299
  }
1213
- mergeActiveElectronLaunchRecords(ownedElectronLaunchRecords, electronLaunchRecords, {
1300
+ mergeActiveElectronLaunchRecords(ownedElectronLaunchRecords, new Map([...electronLaunchRecords].filter(([, record]) => record.ownerSessionId === browserOwnerSessionId)), {
1214
1301
  branchOwnedLaunchIds: branchOwnedElectronLaunchIds,
1215
1302
  markBranchOwned: true,
1216
1303
  });
1304
+ for (const entry of branch) {
1305
+ const event = getBrowserRecord(entry)?.event;
1306
+ const state = event?.state;
1307
+ if (event?.phase === "begin" || state?.ownerSessionId !== browserOwnerSessionId || typeof state.sessionName !== "string")
1308
+ continue;
1309
+ const namespace = typeof state.namespace === "string" ? state.namespace : undefined;
1310
+ const key = getAgentBrowserSessionIdentityKey(state.sessionName, namespace);
1311
+ if (ownedManagedSessions.has(key) && !managedSessionRestoreState.hasDaemonRestoreKey(state.sessionName, namespace)) {
1312
+ managedSessionRestoreState.restoreDaemonReceipt(state.sessionName, namespace, state.managedSessionDaemon);
1313
+ }
1314
+ }
1217
1315
  if (!options.resetRuntimeOwnership) {
1218
1316
  for (const sessionKey of previousAttachedSessionKeys) {
1219
1317
  if (ownedManagedSessions.has(sessionKey))
@@ -1229,18 +1327,20 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1229
1327
  const registerWebSearchToolIfAvailable = (configState) => {
1230
1328
  if (webSearchToolRegistered || !canRegisterWebSearchTool(configState))
1231
1329
  return;
1232
- pi.registerTool(createAgentBrowserWebSearchTool(configState, {
1233
- loadConfigState(ctx) {
1234
- return loadAgentBrowserConfigSync({
1235
- cwd: ctx.cwd,
1236
- includeProjectConfig: shouldIncludeProjectConfig(ctx),
1237
- });
1238
- },
1239
- }));
1330
+ pi.registerTool({
1331
+ ...createAgentBrowserWebSearchTool(configState, {
1332
+ loadConfigState(ctx) {
1333
+ return loadAgentBrowserConfigSync({
1334
+ cwd: ctx.cwd,
1335
+ includeProjectConfig: shouldIncludeProjectConfig(ctx),
1336
+ });
1337
+ },
1338
+ }),
1339
+ });
1240
1340
  webSearchToolRegistered = true;
1241
1341
  };
1242
1342
  pi.on("session_start", async (_event, ctx) => {
1243
- restoreBranchBackedState(ctx, { resetRuntimeOwnership: true });
1343
+ await restoreBranchBackedState(ctx, { resetRuntimeOwnership: true });
1244
1344
  electronChildProcesses = new Map();
1245
1345
  registerWebSearchToolIfAvailable(loadAgentBrowserConfigSync({
1246
1346
  cwd: ctx.cwd,
@@ -1258,7 +1358,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1258
1358
  controller.abort();
1259
1359
  await Promise.allSettled([...activeScriptExecutions]);
1260
1360
  await managedSessionExecutionQueue.run(() => artifactExecutionQueue.run(async () => {
1261
- restoreBranchBackedState(ctx, { resetRuntimeOwnership: false });
1361
+ await restoreBranchBackedState(ctx, { resetRuntimeOwnership: false });
1262
1362
  await recoverScriptSessionLeasesWithinQueue(ctx);
1263
1363
  flushRecordingReservations();
1264
1364
  notifyRecordingPersistence(ctx);
@@ -1270,6 +1370,8 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1270
1370
  const checkpointPi = pi;
1271
1371
  checkpointPi.on("session_checkpoint", async (event, ctx) => {
1272
1372
  const blocked = (reason) => ({ sleepReady: false, reason });
1373
+ if (browserReplayError)
1374
+ return blocked(browserReplayError);
1273
1375
  if (activeScriptControllers.size || activeScriptExecutions.size)
1274
1376
  return blocked("Browser code execution is pending");
1275
1377
  flushRecordingReservations();
@@ -1277,7 +1379,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1277
1379
  return blocked("Browser recording journal persistence is dirty");
1278
1380
  if (activeRecordingReservations.size)
1279
1381
  return blocked("Browser recording is pending; finish it explicitly before sleep");
1280
- const entries = ctx.sessionManager.getEntries();
1382
+ const entries = await readBrowserEntries(ctx.sessionManager, true);
1281
1383
  if ([...getScriptSessionLeasesFromBranch(entries).values()].some(lease => lease.cleanup !== "closed")
1282
1384
  || [...ownedManagedSessions.values()].some(owner => isAgentBrowserScriptSessionName(owner.sessionName))) {
1283
1385
  return blocked("Browser script cleanup lease is unresolved");
@@ -1328,7 +1430,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1328
1430
  });
1329
1431
  // Owned routing must not mask an explicit caller's same-name ambient daemon.
1330
1432
  for (const inspectionContext of context ? [{ ...context, reuseOnly: true }, undefined] : [undefined]) {
1331
- const daemon = await withOwnedManagedSessionContext(inspectionContext, () => inspectManagedSessionDaemon({ ...owner, signal: event.signal, timeoutMs: 2_000 }));
1433
+ const daemon = await withAgentBrowserProcessEnvironment(inspectionContext && owner.socketDir ? { PI_AGENT_BROWSER_SOCKET_DIR: owner.socketDir } : {}, () => withOwnedManagedSessionContext(inspectionContext, () => inspectManagedSessionDaemon({ ...owner, signal: event.signal, timeoutMs: 2_000 })));
1332
1434
  if (daemon.status !== "inactive")
1333
1435
  return blocked("Browser daemon is live or unverified; finish browser work explicitly before sleep");
1334
1436
  }
@@ -1348,7 +1450,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1348
1450
  const shutdownCwd = ctx?.cwd ?? managedSessionCwd;
1349
1451
  const quitting = event?.reason === "quit";
1350
1452
  preservedElectronProfileDirs = quitting
1351
- ? []
1453
+ ? getActiveElectronRecords(electronLaunchRecords).filter(record => !ownedElectronLaunchRecords.has(record.launchId)).map(record => record.userDataDir)
1352
1454
  : getActiveElectronRecords(electronLaunchRecords).map((record) => record.userDataDir);
1353
1455
  const electronRecordsToCleanup = quitting
1354
1456
  ? ownedElectronLaunchRecords
@@ -1399,10 +1501,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1399
1501
  ownedManagedSessions.clear();
1400
1502
  await cleanupSecureTempArtifacts({ preservePaths: preservedElectronProfileDirs });
1401
1503
  });
1402
- pi.on("before_agent_start", async (event, ctx) => {
1403
- if (!shouldAppendBrowserSystemPrompt(event.prompt)) {
1404
- return undefined;
1405
- }
1504
+ const fullInstructions = (ctx) => {
1406
1505
  const runtimeConfig = loadAgentBrowserConfigSync({
1407
1506
  cwd: ctx.cwd,
1408
1507
  includeProjectConfig: shouldIncludeProjectConfig(ctx),
@@ -1418,7 +1517,35 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1418
1517
  const runtimeConfigPrompt = browserGuidance.length > 0
1419
1518
  ? `\n\nProject agent_browser config guidance:\n${browserGuidance.map((line) => `- ${line}`).join("\n")}`
1420
1519
  : "";
1421
- event.systemPromptOptions.sections.agent_browser = `${PROJECT_RULE_PROMPT}${runtimeConfigPrompt}`;
1520
+ const guidelines = [
1521
+ ...buildToolPromptGuidelines({
1522
+ browserDefaultProfile: runtimeConfig.trustedBrowserDefaultProfile,
1523
+ browserExecutablePath: runtimeConfig.trustedBrowserExecutablePath,
1524
+ includeWebSearch: webSearchToolRegistered,
1525
+ docs: getInstalledDocsPaths(),
1526
+ }),
1527
+ ...QUICK_START_GUIDELINES,
1528
+ ...SHARED_BROWSER_PLAYBOOK_GUIDELINES,
1529
+ ...WRAPPER_TAB_RECOVERY_BEHAVIOR,
1530
+ ...Object.values(ADVANCED_TOOL_PROMPT_GUIDELINES).flat(),
1531
+ ...(webSearchToolRegistered ? WEB_SEARCH_TOOL_PROMPT_GUIDELINES : []),
1532
+ ];
1533
+ return `${PROJECT_RULE_PROMPT}\n\n${[...new Set(guidelines)].map(line => `- ${line}`).join("\n")}${runtimeConfigPrompt}`;
1534
+ };
1535
+ let isInstructionsManaged = () => false;
1536
+ pi.events.on("pi:instruction-groups", (data) => {
1537
+ const collector = data;
1538
+ collector.register({
1539
+ ...AGENT_BROWSER_INSTRUCTION_GROUP,
1540
+ tools: ["agent_browser", "agent_browser_code", "agent_browser_tools", "agent_browser_web_search",
1541
+ ...Object.values(AGENT_BROWSER_TOOL_INVENTORY).map(({ name }) => name)],
1542
+ instructions: fullInstructions,
1543
+ });
1544
+ isInstructionsManaged = collector.isManaged;
1545
+ });
1546
+ pi.on("before_agent_start", async (event, ctx) => {
1547
+ if (!isInstructionsManaged())
1548
+ event.systemPromptOptions.sections.agent_browser = fullInstructions(ctx);
1422
1549
  });
1423
1550
  pi.on("tool_call", async (event, ctx) => {
1424
1551
  const promptPolicy = buildPromptPolicy(getLatestUserPrompt(ctx.sessionManager.getBranch()));
@@ -1434,6 +1561,18 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1434
1561
  }
1435
1562
  });
1436
1563
  pi.on("tool_result", async (event) => buildAgentBrowserToolResultPatch(event));
1564
+ let emptyBrowserBranch;
1565
+ const captureCurrentBrowserBranch = (ctx) => {
1566
+ const generation = branchRestoreGeneration;
1567
+ const empty = ctx.sessionManager.getLeafId() === null;
1568
+ // Concurrent empty-root callers share only their own first synchronous append.
1569
+ if (empty && emptyBrowserBranch?.anchorId === null
1570
+ && emptyBrowserBranch.sessionId === ctx.sessionManager.getSessionId() && emptyBrowserBranch.isCurrent())
1571
+ return emptyBrowserBranch;
1572
+ const branch = captureBrowserBranch(ctx.sessionManager, () => generation === branchRestoreGeneration);
1573
+ emptyBrowserBranch = empty ? branch : undefined;
1574
+ return branch;
1575
+ };
1437
1576
  const agentBrowserTool = {
1438
1577
  renderCall(args, theme, context) {
1439
1578
  const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
@@ -1447,7 +1586,8 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1447
1586
  component.setState(formatAgentBrowserRenderResult(result, options, theme, context.isError), options.expanded, theme);
1448
1587
  return component;
1449
1588
  },
1450
- async execute(toolCallId, params, signal, onUpdate, ctx, nativeToolCallId = toolCallId, capturedCwd, modelVisible = true) {
1589
+ async execute(toolCallId, params, signal, onUpdate, ctx, nativeToolCallId = toolCallId, capturedCwd, modelVisible = true, commandIndex, selectedBranch) {
1590
+ const branch = selectedBranch ?? captureCurrentBrowserBranch(ctx);
1451
1591
  let operationCwd;
1452
1592
  try {
1453
1593
  operationCwd = capturedCwd ?? resolveExecutionCwd(pi, ctx);
@@ -1464,6 +1604,8 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1464
1604
  if (admittedInput.status === "invalid") {
1465
1605
  return buildValidationFailureResult(admittedInput);
1466
1606
  }
1607
+ if (browserReplayError && !isPlainTextInspectionArgs(admittedInput.toolArgs))
1608
+ return browserExecutionFailure(new Error(browserReplayError), signal, "validation-error");
1467
1609
  const explicitConfig = scanUpstreamGlobalFlagOccurrences(admittedInput.toolArgs, "--config").length > 0 || getAgentBrowserProcessEnvironment().AGENT_BROWSER_CONFIG !== undefined;
1468
1610
  // Managed selection must follow earlier queued fresh launches, while file paths keep their admission snapshot.
1469
1611
  const managedAtAdmission = admittedInput.kind !== "electron"
@@ -1529,7 +1671,31 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1529
1671
  branchRecords: electronLaunchRecords,
1530
1672
  compiledElectron,
1531
1673
  ownedRecords: ownedElectronLaunchRecords,
1674
+ ownerSessionId: ctx.sessionManager.getSessionId(),
1532
1675
  });
1676
+ const hostAffecting = compiledElectron && ["cleanup", "probe"].includes(compiledElectron.action);
1677
+ const workingPageState = sessionPageState.fork();
1678
+ const priorPages = workingPageState.views();
1679
+ const priorManifest = artifactManifest;
1680
+ const operationId = randomUUID();
1681
+ const launchId = compiledElectron && "launchId" in compiledElectron ? compiledElectron.launchId : undefined;
1682
+ const selected = launchId ? [electronHostLaunchRecords.get(launchId)].filter((record) => record !== undefined) : getActiveElectronRecords(electronHostLaunchRecords);
1683
+ const keys = new Set(selected.flatMap(record => record.sessionName ? [getAgentBrowserSessionIdentityKey(record.sessionName, record.namespace)] : []));
1684
+ if (compiledElectron?.action === "probe" && !launchId && managedSessionActive)
1685
+ keys.add(getAgentBrowserSessionIdentityKey(managedSessionName, managedSessionNamespace));
1686
+ const begin = { event: { version: 1, phase: "begin", operationId, toolCallId, commandIndex: commandIndex ?? 0, isError: true,
1687
+ state: {}, pages: [...keys].map(key => ({ key, refs: { kind: "unknown" }, unknown: true })) } };
1688
+ if (hostAffecting) {
1689
+ try {
1690
+ if (!await appendBrowserRecord(ctx.sessionManager, () => appendBrowserTransition(pi, begin), begin, branch)
1691
+ || !branch.isCurrent())
1692
+ throw new Error("The selected Pi branch changed before host-operation admission.");
1693
+ }
1694
+ catch (error) {
1695
+ return browserExecutionFailure(new Error("Could not persist Electron/browser-state begin; the host operation was not run.", { cause: error }), executionSignal);
1696
+ }
1697
+ sessionPageState.applyBrowserRecord(begin);
1698
+ }
1533
1699
  let electronHostResult = await handleElectronHostInput({
1534
1700
  attachedSessionKeys,
1535
1701
  compiledElectron,
@@ -1543,7 +1709,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1543
1709
  managedSessionRestoreState,
1544
1710
  ownedManagedSessions,
1545
1711
  redactedCompiledElectron,
1546
- sessionPageState,
1712
+ sessionPageState: workingPageState,
1547
1713
  signal: executionSignal,
1548
1714
  });
1549
1715
  if (electronHostResult && compiledElectron?.action === "cleanup") {
@@ -1571,6 +1737,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1571
1737
  retireRecordingSession(identity.sessionName, identity.namespace);
1572
1738
  const closedSessionKey = getSessionContextKey(identity.sessionName, identity.namespace) ?? identity.sessionName;
1573
1739
  clearSessionScopedBrowserState(closedSessionKey);
1740
+ workingPageState.clearSession(closedSessionKey);
1574
1741
  if (closedSessionKey === (getSessionContextKey(managedSessionName, managedSessionNamespace) ?? managedSessionName)) {
1575
1742
  managedSessionActive = false;
1576
1743
  managedSessionCompatibilityWorkaround = undefined;
@@ -1581,15 +1748,37 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1581
1748
  managedSessionName = createFreshSessionName(managedSessionBaseName, ephemeralSessionSeed, freshSessionOrdinal);
1582
1749
  }
1583
1750
  }
1584
- if (artifactManifest) {
1585
- electronHostResult = {
1586
- ...electronHostResult,
1587
- details: {
1588
- ...(isRecord(electronHostResult.details) ? electronHostResult.details : {}),
1589
- artifactManifest: redactSensitiveValue(artifactManifest),
1590
- artifactRetentionSummary: formatSessionArtifactRetentionSummary(artifactManifest),
1591
- },
1592
- };
1751
+ if (artifactManifest && isRecord(electronHostResult.details))
1752
+ electronHostResult.details.artifactRetentionSummary = formatSessionArtifactRetentionSummary(artifactManifest);
1753
+ }
1754
+ if (electronHostResult && hostAffecting && branch.isCurrent()) {
1755
+ for (const key of keys)
1756
+ if (workingPageState.get(key).refSnapshot && workingPageState.get(key).refSnapshot?.snapshotId !== priorPages.get(key)?.refSnapshot?.snapshotId) {
1757
+ const record = selected.find(record => record.sessionName && getAgentBrowserSessionIdentityKey(record.sessionName, record.namespace) === key);
1758
+ const owner = ownedManagedSessions.get(key);
1759
+ const daemon = await inspectManagedSessionDaemon({ cwd: ctx.cwd, sessionName: record?.sessionName ?? managedSessionName, namespace: record?.namespace ?? managedSessionNamespace, signal: executionSignal, includeGeneration: true, headedManagedAutosaveInterval: owner?.headedManagedAutosaveInterval ?? (owner?.headedManagedAutosaveDisabled ? "0" : undefined) });
1760
+ workingPageState.bindSnapshotGeneration(key, daemon.status === "active" ? daemon.generation : undefined);
1761
+ }
1762
+ const changes = browserPageChanges(workingPageState, priorPages, keys);
1763
+ const finish = { event: { version: 1, phase: hostAffecting ? "finish" : "state", operationId, toolCallId, commandIndex: commandIndex ?? 0, isError: electronHostResult.isError === true,
1764
+ state: browserStateEffects(isRecord(electronHostResult.details) ? electronHostResult.details : {}), pages: changes.pages, artifacts: artifactChanges(priorManifest, artifactManifest) },
1765
+ ...(changes.snapshot ? { snapshot: changes.snapshot } : {}) };
1766
+ try {
1767
+ if (!await appendBrowserRecord(ctx.sessionManager, () => appendBrowserTransition(pi, finish), finish, branch)
1768
+ || !branch.isCurrent())
1769
+ throw new Error("The selected Pi branch changed before host-state confirmation.");
1770
+ }
1771
+ catch (error) {
1772
+ if (branch.isCurrent())
1773
+ sessionPageState.applyBrowserRecord(begin);
1774
+ return { ...browserExecutionFailure(new Error("The host operation finished, but its state could not be persisted. Inspect before retrying; changes may already have happened.", { cause: error }), executionSignal),
1775
+ details: { ...(isRecord(electronHostResult.details) ? electronHostResult.details : {}), browserStatePersistence: "finish-unconfirmed", resultCategory: "failure", failureCategory: "upstream-error", nextActions: undefined } };
1776
+ }
1777
+ sessionPageState.applyBrowserRecord(finish);
1778
+ artifactManifest = applyArtifactChanges(artifactManifest, finish.event.artifacts);
1779
+ if (isRecord(electronHostResult.details)) {
1780
+ electronHostResult.details.browserEventVersion = 1;
1781
+ electronHostResult.details.artifactManifest = invocationArtifactManifest(artifactManifest, priorManifest, electronHostResult.details);
1593
1782
  }
1594
1783
  }
1595
1784
  if (electronHostResult && executionSignal?.aborted) {
@@ -1610,7 +1799,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1610
1799
  }) : runElectronHostInput(executionSignal);
1611
1800
  if (!shouldSerializeElectronHostInput(compiledElectron))
1612
1801
  return execute();
1613
- const records = getElectronHostLaunchRecordsForInput({ branchRecords: electronLaunchRecords, compiledElectron, ownedRecords: ownedElectronLaunchRecords });
1802
+ const records = getElectronHostLaunchRecordsForInput({ branchRecords: electronLaunchRecords, compiledElectron, ownedRecords: ownedElectronLaunchRecords, ownerSessionId: ctx.sessionManager.getSessionId() });
1614
1803
  const launchId = compiledElectron && "launchId" in compiledElectron ? compiledElectron.launchId : undefined;
1615
1804
  const selected = launchId ? [records.get(launchId)].filter((record) => record !== undefined) : getActiveElectronRecords(records);
1616
1805
  const sessions = selected.flatMap(record => record.sessionName ? [{ sessionName: record.sessionName, namespace: record.namespace }] : []);
@@ -1651,9 +1840,10 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1651
1840
  // before capturing state. Registration, rendering, and restore stay synchronous/eager.
1652
1841
  const { runAgentBrowserTool } = await import("./lib/orchestration/browser-run/index.js");
1653
1842
  flushRecordingReservations();
1654
- const branchRestoreGenerationAtStart = branchRestoreGeneration;
1655
1843
  const generationAtStart = branchStateGeneration;
1656
- const sessionPageStateUpdate = sessionPageState.beginUpdate();
1844
+ const workingPageState = sessionPageState.fork();
1845
+ const priorPages = workingPageState.views();
1846
+ const sessionPageStateUpdate = workingPageState.beginUpdate();
1657
1847
  const browserRunState = {
1658
1848
  activeRecordingReservations,
1659
1849
  artifactManifest,
@@ -1674,7 +1864,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1674
1864
  managedSessionRestoreState,
1675
1865
  networkRoutesBySession,
1676
1866
  ownedManagedSessions,
1677
- sessionPageState,
1867
+ sessionPageState: workingPageState,
1678
1868
  traceOwners,
1679
1869
  };
1680
1870
  const selectedPlan = buildExecutionPlan(toolArgs, {
@@ -1687,6 +1877,30 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1687
1877
  stdin: resolvedInput.toolStdin,
1688
1878
  browserIndependentReadConfirmation: readConfirmation?.capabilities?.readRequiresConfirmation === true,
1689
1879
  });
1880
+ const browserAffecting = !selectedPlan.plainTextInspection && !(selectedPlan.commandInfo.command === "session" && selectedPlan.commandInfo.subcommand === "info")
1881
+ && (needsManagedSession(parseArgvDescriptor(toolArgs), resolvedInput.toolStdin) || (selectedPlan.startupScopedFlags?.length ?? 0) > 0);
1882
+ const operationId = randomUUID();
1883
+ const selectedKey = getAgentBrowserSessionIdentityKey(selectedPlan.sessionName ?? "default", selectedPlan.namespace);
1884
+ const begin = { event: {
1885
+ version: 1, phase: "begin", operationId, toolCallId, commandIndex: commandIndex ?? 0, isError: true,
1886
+ state: { args: redactInvocationArgs(toolArgs), sessionName: selectedPlan.sessionName, namespace: selectedPlan.namespace,
1887
+ ownerSessionId: ctx.sessionManager.getSessionId(), usedImplicitSession: selectedPlan.usedImplicitSession,
1888
+ wrapperManaged: selectedPlan.managedSessionName !== undefined || ownedManagedSessions.has(selectedKey), managedSessionCwd: browserCwd,
1889
+ managedSessionSocketDir: resolveAgentBrowserSocketDir({ ownedManagedSession: selectedPlan.managedSessionName !== undefined || ownedManagedSessions.has(selectedKey) }) },
1890
+ pages: [{ key: selectedKey, refs: { kind: "unknown" }, unknown: true }],
1891
+ } };
1892
+ if (browserAffecting) {
1893
+ try {
1894
+ if (!await appendBrowserRecord(ctx.sessionManager, () => appendBrowserTransition(pi, begin), begin, branch)
1895
+ || !branch.isCurrent())
1896
+ throw new Error("The selected Pi branch changed before browser-command admission.");
1897
+ }
1898
+ catch (error) {
1899
+ const failure = browserExecutionFailure(new Error("Could not persist browser-state begin; the browser command was not run.", { cause: error }), executionSignal);
1900
+ return { ...failure, details: { ...(isRecord(failure.details) ? failure.details : {}), browserStatePersistence: "begin-unconfirmed" } };
1901
+ }
1902
+ sessionPageState.applyBrowserRecord(begin);
1903
+ }
1690
1904
  const initialArtifactManifest = browserRunState.artifactManifest;
1691
1905
  const initialNetworkRoutesBySession = browserRunState.networkRoutesBySession;
1692
1906
  const attachedSessionRequested = isAttachedBrowserInvocation(toolArgs)
@@ -1698,28 +1912,37 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1698
1912
  : getSessionContextKey(selectedPlan.sessionName, selectedPlan.namespace)
1699
1913
  ?? getSessionContextKey(browserRunState.managedSessionName, browserRunState.managedSessionNamespace);
1700
1914
  const attachedSessionKnown = reusableSessionKey !== undefined && attachedSessionKeys.has(reusableSessionKey);
1701
- let result = await runAgentBrowserTool({
1702
- daemonInactive,
1703
- ctx,
1704
- cwd: browserCwd,
1705
- operationCwd,
1706
- electronPostCommandStatusSettleMs: ELECTRON_POST_COMMAND_STATUS_SETTLE_MS,
1707
- electronProfileIsolationDetails: ELECTRON_PROFILE_ISOLATION_DETAILS,
1708
- implicitSessionCloseTimeoutMs,
1709
- implicitSessionIdleTimeoutMs,
1710
- input: resolvedInput,
1711
- modelVisible,
1712
- onUpdate,
1713
- params,
1714
- establishAttachedBrowserSession: attachedSessionRequested && !attachedSessionKnown,
1715
- preserveAttachedBrowserSession: attachedSessionRequested || attachedSessionKnown,
1716
- promptPolicy,
1717
- sessionPageStateUpdate,
1718
- signal: executionSignal,
1719
- state: browserRunState,
1720
- });
1721
- const branchRestoreStillCurrent = branchRestoreGenerationAtStart === branchRestoreGeneration;
1915
+ let result;
1916
+ try {
1917
+ result = await runAgentBrowserTool({
1918
+ daemonInactive,
1919
+ ctx,
1920
+ cwd: browserCwd,
1921
+ operationCwd,
1922
+ electronPostCommandStatusSettleMs: ELECTRON_POST_COMMAND_STATUS_SETTLE_MS,
1923
+ electronProfileIsolationDetails: ELECTRON_PROFILE_ISOLATION_DETAILS,
1924
+ implicitSessionCloseTimeoutMs,
1925
+ implicitSessionIdleTimeoutMs,
1926
+ input: resolvedInput,
1927
+ modelVisible,
1928
+ onUpdate,
1929
+ params,
1930
+ establishAttachedBrowserSession: attachedSessionRequested && !attachedSessionKnown,
1931
+ preserveAttachedBrowserSession: attachedSessionRequested || attachedSessionKnown,
1932
+ promptPolicy,
1933
+ sessionPageStateUpdate,
1934
+ signal: executionSignal,
1935
+ state: browserRunState,
1936
+ });
1937
+ }
1938
+ catch (error) {
1939
+ const failure = browserExecutionFailure(error, executionSignal);
1940
+ result = { ...failure, details: { ...browserRunState.observedBrowserEffects, ...(isRecord(failure.details) ? failure.details : {}) } };
1941
+ }
1942
+ const branchRestoreStillCurrent = branch.isCurrent();
1722
1943
  const resultDetails = isRecord(result.details) ? result.details : undefined;
1944
+ if (resultDetails && begin.event.state.wrapperManaged === true)
1945
+ resultDetails.managedSessionSocketDir = begin.event.state.managedSessionSocketDir;
1723
1946
  if (typeof resultDetails?.sessionName === "string" && browserRunState.managedSessionActive
1724
1947
  && getAgentBrowserSessionIdentityKey(resultDetails.sessionName, typeof resultDetails.namespace === "string" ? resultDetails.namespace : undefined) === getAgentBrowserSessionIdentityKey(browserRunState.managedSessionName, browserRunState.managedSessionNamespace)) {
1725
1948
  resultDetails.managedSessionCwd = browserRunState.managedSessionCwd;
@@ -1776,18 +1999,10 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1776
1999
  if (reservation)
1777
2000
  result = appendActiveRecordingCleanupAction(result, reservation);
1778
2001
  }
1779
- if (artifactManifest) {
1780
- result = {
1781
- ...result,
1782
- details: {
1783
- ...(isRecord(result.details) ? result.details : {}),
1784
- artifactManifest: redactSensitiveValue(artifactManifest),
1785
- artifactRetentionSummary: formatSessionArtifactRetentionSummary(artifactManifest),
1786
- },
1787
- };
1788
- }
2002
+ if (artifactManifest && isRecord(result.details))
2003
+ result.details.artifactRetentionSummary = formatSessionArtifactRetentionSummary(artifactManifest);
1789
2004
  }
1790
- const branchStateStillCurrent = generationAtStart === branchStateGeneration;
2005
+ const branchStateStillCurrent = generationAtStart === branchStateGeneration && branch.isCurrent();
1791
2006
  if (serializeBrowserCommand || branchStateStillCurrent) {
1792
2007
  freshSessionOrdinal = Math.max(freshSessionOrdinal, browserRunState.freshSessionOrdinal);
1793
2008
  managedSessionActive = browserRunState.managedSessionActive;
@@ -1802,6 +2017,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1802
2017
  }
1803
2018
  syncOwnedManagedSessionsFromResult(ownedManagedSessions, result, browserRunState.managedSessionCwd);
1804
2019
  mergeActiveElectronLaunchRecords(ownedElectronLaunchRecords, electronLaunchRecords, {
2020
+ ownerSessionId: ctx.sessionManager.getSessionId(),
1805
2021
  branchOwnedLaunchIds: branchOwnedElectronLaunchIds,
1806
2022
  touchedLaunchIds: !result.isError
1807
2023
  ? getTouchedElectronLaunchIds(explicitSessionName ?? browserRunState.managedSessionName, electronLaunchRecords, resultNamespace)
@@ -1810,6 +2026,39 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1810
2026
  if (serializeBrowserCommand)
1811
2027
  branchStateGeneration += 1;
1812
2028
  }
2029
+ if (!selectedPlan.plainTextInspection && branchRestoreStillCurrent) {
2030
+ const changes = browserPageChanges(workingPageState, priorPages, [...browserRunState.closedManagedSessionNames, ...(browserAffecting ? [selectedKey] : [])]);
2031
+ const artifacts = artifactChanges(initialArtifactManifest, artifactManifest);
2032
+ if (browserAffecting || changes.pages.length > 0 || artifacts && hasPublishedBrowserJournal(ctx.sessionManager) || resultDetails?.readConfirmation !== undefined || browserRunState.observedBrowserEffects?.readConfirmation !== undefined) {
2033
+ const finish = { event: {
2034
+ version: 1, phase: browserAffecting ? "finish" : "state", operationId, toolCallId, commandIndex: commandIndex ?? 0, isError: result.isError === true,
2035
+ state: { ...browserStateEffects(isRecord(result.details) ? result.details : {}), ...browserRunState.observedBrowserEffects,
2036
+ ownerSessionId: ctx.sessionManager.getSessionId(),
2037
+ ...(begin.event.state.wrapperManaged === true ? { managedSessionDaemon: managedSessionRestoreState.getDaemonReceipt(resultSessionName, resultNamespace) } : {}) }, pages: changes.pages,
2038
+ artifacts,
2039
+ }, ...(changes.snapshot ? { snapshot: changes.snapshot } : {}) };
2040
+ try {
2041
+ if (!await appendBrowserRecord(ctx.sessionManager, () => appendBrowserTransition(pi, finish), finish, branch)
2042
+ || !branch.isCurrent())
2043
+ throw new Error("The selected Pi branch changed before browser-state confirmation.");
2044
+ }
2045
+ catch (error) {
2046
+ return { ...result, ...browserExecutionFailure(new Error("The browser command finished, but its state could not be persisted. Changes may already have happened; inspect the current page before retrying.", { cause: error }), executionSignal), details: { ...(isRecord(result.details) ? result.details : {}), resultCategory: "failure", failureCategory: "upstream-error", browserStatePersistence: "finish-unconfirmed", nextActions: [{
2047
+ id: "inspect-after-browser-state-persistence-failure", tool: "agent_browser", params: { args: ["--session", selectedPlan.sessionName ?? "default", "--namespace", selectedPlan.namespace ?? "", "batch", "--bail"], stdin: JSON.stringify([["get", "url"], ["snapshot", "-i"]]) },
2048
+ reason: "The effect may already have happened. Verify the current URL and inspect before deciding whether to retry.", safety: "Read-only inspection; no mutation is replayed.",
2049
+ }] } };
2050
+ }
2051
+ sessionPageState.applyBrowserRecord(finish);
2052
+ artifactManifest = applyArtifactChanges(artifactManifest, finish.event.artifacts);
2053
+ }
2054
+ }
2055
+ if (isRecord(result.details)) {
2056
+ result.details.browserEventVersion = 1;
2057
+ if (!(compiledElectron?.action === "launch" && compiledElectron.handoff === "snapshot")
2058
+ && !getArtifactCommandSteps(toolArgs, resolvedInput.toolStdin).steps.some(step => step[0] === "snapshot"))
2059
+ delete result.details.refSnapshot;
2060
+ result.details.artifactManifest = invocationArtifactManifest(branch.isCurrent() ? artifactManifest : browserRunState.artifactManifest, initialArtifactManifest, result.details);
2061
+ }
1813
2062
  return applyAgentBrowserOutputPath({ cwd: operationCwd, outputPath, preserveTextContent: Array.isArray(params.args) && params.args.includes("--json"), result: warnRecordingPersistence(result) });
1814
2063
  };
1815
2064
  const closesAllSessions = commandClosesAllSessions(toolArgs, resolvedInput.toolStdin);
@@ -1821,35 +2070,39 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1821
2070
  sessionMode: compiledElectron?.action === "launch" ? "fresh" : params.sessionMode ?? "auto",
1822
2071
  stdin: resolvedInput.toolStdin,
1823
2072
  });
1824
- const run = (executionSignal = signal) => {
1825
- const execute = () => withLaunchDefaults
1826
- ? withLaunchDefaults(inactive => runBrowserCommand(inactive, executionSignal), executionSignal)
1827
- : runBrowserCommand(undefined, executionSignal);
1828
- if (!commandTouchesArtifactLifecycle(toolArgs, resolvedInput.toolStdin, outputPath))
1829
- return execute();
1830
- return artifactExecutionQueue.run(async () => {
1831
- const error = getArtifactPreflightValidationError({ activeRecordingReservations: activeRecordingReservations.values(), args: toolArgs, cwd: operationCwd, outputPath, stdin: resolvedInput.toolStdin });
1832
- if (!error)
2073
+ const owner = ownedManagedSessions.get(getAgentBrowserSessionIdentityKey(plan.sessionName ?? "default", plan.namespace));
2074
+ return withAgentBrowserProcessEnvironment(owner?.socketDir ? { PI_AGENT_BROWSER_SOCKET_DIR: owner.socketDir } : {}, async () => {
2075
+ const run = (executionSignal = signal) => {
2076
+ const execute = () => withLaunchDefaults
2077
+ ? withLaunchDefaults(inactive => runBrowserCommand(inactive, executionSignal), executionSignal)
2078
+ : runBrowserCommand(undefined, executionSignal);
2079
+ if (!commandTouchesArtifactLifecycle(toolArgs, resolvedInput.toolStdin, outputPath))
1833
2080
  return execute();
1834
- flushRecordingReservations();
1835
- return warnRecordingPersistence(buildValidationFailureResult({ ...resolvedInput, attemptedKind: resolvedInput.kind, kind: "invalid", status: "invalid", validationError: error }));
1836
- });
1837
- };
1838
- if (plan.validationError || (!plan.sessionName && plan.commandInfo.command !== "read" && !closesAllSessions))
1839
- return run();
1840
- try {
1841
- const owned = plan.managedSessionName !== undefined || ownedManagedSessions.has(getAgentBrowserSessionIdentityKey(plan.sessionName ?? "default", plan.namespace));
1842
- const identities = [await resolveBrowserExecutionIdentity({ namespace: plan.namespace, ownedManagedSession: owned, sessionName: closesAllSessions ? undefined : plan.sessionName ?? "default" })];
1843
- if (managedSessionActive && (params.sessionMode === "fresh" || compiledElectron?.action === "launch") && !explicitSessionName) {
1844
- identities.push(await resolveBrowserExecutionIdentity({ namespace: managedSessionNamespace, ownedManagedSession: true, sessionName: managedSessionName }));
2081
+ return artifactExecutionQueue.run(async () => {
2082
+ const error = getArtifactPreflightValidationError({ activeRecordingReservations: activeRecordingReservations.values(), args: toolArgs, cwd: operationCwd, outputPath, stdin: resolvedInput.toolStdin });
2083
+ if (!error)
2084
+ return execute();
2085
+ flushRecordingReservations();
2086
+ return warnRecordingPersistence(buildValidationFailureResult({ ...resolvedInput, attemptedKind: resolvedInput.kind, kind: "invalid", status: "invalid", validationError: error }));
2087
+ });
2088
+ };
2089
+ if (plan.validationError || (!plan.sessionName && plan.commandInfo.command !== "read" && !closesAllSessions))
2090
+ return run();
2091
+ try {
2092
+ const owned = plan.managedSessionName !== undefined || ownedManagedSessions.has(getAgentBrowserSessionIdentityKey(plan.sessionName ?? "default", plan.namespace));
2093
+ const identities = [await resolveBrowserExecutionIdentity({ namespace: plan.namespace, ownedManagedSession: owned, sessionName: closesAllSessions ? undefined : plan.sessionName ?? "default" })];
2094
+ if (managedSessionActive && (params.sessionMode === "fresh" || compiledElectron?.action === "launch") && !explicitSessionName) {
2095
+ const previousOwner = ownedManagedSessions.get(getAgentBrowserSessionIdentityKey(managedSessionName, managedSessionNamespace));
2096
+ identities.push(await resolveBrowserExecutionIdentity({ namespace: managedSessionNamespace, ownedManagedSession: true, sessionName: managedSessionName, env: previousOwner?.socketDir ? { AGENT_BROWSER_SOCKET_DIR: previousOwner.socketDir } : undefined }));
2097
+ }
2098
+ const activePageUrl = sessionPageState.get(getAgentBrowserSessionIdentityKey(plan.sessionName ?? "default", plan.namespace)).tabTarget?.url;
2099
+ const deadline = Date.now() + (params.timeoutMs ?? getCommandAwareProcessTimeoutMs(extractUpstreamCommandTokens(toolArgs), resolvedInput.toolStdin, activePageUrl) ?? getAgentBrowserProcessTimeoutMs());
2100
+ return await withBrowserExecutionLocks({ identities, signal, deadline, waitOnly: true }, run);
1845
2101
  }
1846
- const activePageUrl = sessionPageState.get(getAgentBrowserSessionIdentityKey(plan.sessionName ?? "default", plan.namespace)).tabTarget?.url;
1847
- const deadline = Date.now() + (params.timeoutMs ?? getCommandAwareProcessTimeoutMs(extractUpstreamCommandTokens(toolArgs), resolvedInput.toolStdin, activePageUrl) ?? getAgentBrowserProcessTimeoutMs());
1848
- return await withBrowserExecutionLocks({ identities, signal, deadline, waitOnly: true }, run);
1849
- }
1850
- catch (error) {
1851
- return browserExecutionFailure(error, signal);
1852
- }
2102
+ catch (error) {
2103
+ return browserExecutionFailure(error, signal);
2104
+ }
2105
+ });
1853
2106
  };
1854
2107
  const runWithinSessionQueue = () => {
1855
2108
  if (closesAllSessions)
@@ -1883,9 +2136,15 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1883
2136
  return managedSessionExecutionQueue.run(executeAdmitted, signal);
1884
2137
  },
1885
2138
  };
1886
- const executeCode = async (toolCallId, params, signal, _onUpdate, ctx) => {
1887
- if (!ctx.sessionManager.getSessionFile())
1888
- return browserExecutionFailure(new Error("agent_browser_code requires a persisted Pi session for ordered browser-state recovery; relaunch without --no-session."), signal, "validation-error");
2139
+ const executeCode = async (toolCallId, params, signal, _onUpdate, ctx, branch) => {
2140
+ if (browserReplayError)
2141
+ return browserExecutionFailure(new Error(browserReplayError), signal, "validation-error");
2142
+ try {
2143
+ requirePublishedBrowserJournal(ctx.sessionManager);
2144
+ }
2145
+ catch (error) {
2146
+ return browserExecutionFailure(error, signal, "validation-error");
2147
+ }
1889
2148
  let operationCwd;
1890
2149
  try {
1891
2150
  operationCwd = resolveExecutionCwd(pi, ctx);
@@ -1899,12 +2158,8 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1899
2158
  if (signal?.aborted)
1900
2159
  abort();
1901
2160
  activeScriptControllers.add(controller);
1902
- let finish;
1903
- const execution = new Promise(resolve => { finish = resolve; });
1904
- activeScriptExecutions.add(execution);
1905
2161
  const deadline = Date.now() + (params.timeoutMs ?? AGENT_BROWSER_SCRIPT_DEFAULT_TIMEOUT_MS);
1906
2162
  const deadlineTimer = setTimeout(() => controller.abort(new DOMException("Browser code deadline exceeded.", "TimeoutError")), Math.max(0, deadline - Date.now()));
1907
- const generation = branchRestoreGeneration;
1908
2163
  const output = createBrowserCodeOutput();
1909
2164
  const nativeEnvironment = getAgentBrowserProcessEnvironment();
1910
2165
  try {
@@ -1933,9 +2188,10 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1933
2188
  const namespace = plan.namespace || undefined;
1934
2189
  const key = getAgentBrowserSessionIdentityKey(sessionName, namespace);
1935
2190
  const owned = plan.managedSessionName !== undefined || ownedManagedSessions.has(key);
1936
- const run = async () => {
2191
+ const run = async () => withAgentBrowserProcessEnvironment(ownedManagedSessions.get(key)?.socketDir ? { PI_AGENT_BROWSER_SOCKET_DIR: ownedManagedSessions.get(key).socketDir } : {}, async () => {
1937
2192
  const identity = await resolveBrowserExecutionIdentity({ sessionName, namespace, ownedManagedSession: owned });
1938
2193
  return withBrowserExecutionLock({ identity, deadline, signal: controller.signal }, async (executionSignal) => {
2194
+ let commandIndex = 0;
1939
2195
  let journalFailed = false;
1940
2196
  const run = await runAgentBrowserScript({
1941
2197
  code: params.code, signal: executionSignal, timeoutMs: Math.max(1, deadline - Date.now()),
@@ -1946,33 +2202,24 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1946
2202
  const { args } = bindBrowserCodeCall(inner, { sessionName, namespace });
1947
2203
  if (plan.usedImplicitSession && !managedSessionActive && managedSessionName === sessionName && extractExplicitSessionName(inner.args) === undefined)
1948
2204
  args.splice(args.indexOf("--session"), 2);
1949
- const pending = { args: redactInvocationArgs(args), sessionName, namespace, resultCategory: "failure", sessionTabTargetUnknown: true };
1950
- try {
1951
- appendBrowserTransition(pi, toolCallId, pending, true);
1952
- }
1953
- catch {
2205
+ // The ordinary executor journals direct, code and hidden-helper effects alike.
2206
+ const result = await withAgentBrowserProcessEnvironment({ AGENT_BROWSER_SESSION: nativeEnvironment.AGENT_BROWSER_SESSION }, () => agentBrowserTool.execute(toolCallId, { args, stdin: inner.stdin, timeoutMs: Math.min(inner.timeoutMs ?? deadline - Date.now(), Math.max(1, deadline - Date.now())) }, innerSignal, undefined, ctx, toolCallId, operationCwd, false, commandIndex++, branch));
2207
+ if (isRecord(result.details) && result.details.browserStatePersistence !== undefined) {
1954
2208
  journalFailed = true;
1955
- throw new Error("Could not persist browser-state intent; the browser command was not run.");
1956
- }
1957
- // Resolve launch defaults from the real command, never the routing-only get-url probe.
1958
- const result = await withAgentBrowserProcessEnvironment({ AGENT_BROWSER_SESSION: nativeEnvironment.AGENT_BROWSER_SESSION }, () => agentBrowserTool.execute(toolCallId, { args, stdin: inner.stdin, timeoutMs: Math.min(inner.timeoutMs ?? deadline - Date.now(), Math.max(1, deadline - Date.now())) }, innerSignal, undefined, ctx, toolCallId, operationCwd, false));
1959
- const page = sessionPageState.get(key);
1960
- const details = { ...(isRecord(result.details) ? result.details : {}), sessionName, namespace, sessionTabTarget: page.tabTarget, sessionTabTargetUnknown: page.tabTargetUnknown, sessionTabReopenPending: page.tabReopenPending, refSnapshot: page.refSnapshot, refSnapshotInvalidation: page.refSnapshotInvalidation };
1961
- if (generation === branchRestoreGeneration) {
1962
- try {
1963
- appendBrowserTransition(pi, toolCallId, details, result.isError === true);
1964
- }
1965
- catch {
1966
- journalFailed = true;
1967
- throw new Error("The browser command finished, but its state could not be persisted. Inspect before retrying; changes may already have happened.");
1968
- }
2209
+ throw new Error("The browser effect may already have happened and its finish could not be persisted. Inspect before continuing; dependent commands were stopped.");
1969
2210
  }
1970
2211
  return output.observe(result);
1971
2212
  },
1972
2213
  });
1973
2214
  const result = await output.finish(run, sessionName, namespace);
1974
- if (artifactManifest && isRecord(result.details))
1975
- result.details.artifactManifest = redactSensitiveValue(artifactManifest);
2215
+ if (journalFailed && isRecord(result.details)) {
2216
+ result.isError = true;
2217
+ result.details.resultCategory = "failure";
2218
+ result.details.failureCategory = "upstream-error";
2219
+ result.details.error = "Browser-state persistence failed. Dependent calls were stopped; inspect before continuing because already-dispatched effects may have happened.";
2220
+ }
2221
+ if (isRecord(result.details))
2222
+ result.details.browserEventVersion = 1;
1976
2223
  if (!params.outputPath)
1977
2224
  return result;
1978
2225
  return artifactExecutionQueue.run(async () => {
@@ -1984,7 +2231,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1984
2231
  return applyAgentBrowserOutputPath({ cwd: operationCwd, outputPath: params.outputPath, preserveTextContent: true, result });
1985
2232
  });
1986
2233
  });
1987
- };
2234
+ });
1988
2235
  return shouldSerializeBrowserCommand({ namespace, explicitSessionName: sessionName, managedSessionName, ownedElectronLaunchRecords, ownedManagedSessions })
1989
2236
  ? managedSessionExecutionQueue.run(run, controller.signal)
1990
2237
  : callerOwnedSessionExecutionQueues.run(key, namespace, run, controller.signal);
@@ -2000,11 +2247,9 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
2000
2247
  clearTimeout(deadlineTimer);
2001
2248
  activeScriptControllers.delete(controller);
2002
2249
  signal?.removeEventListener("abort", abort);
2003
- activeScriptExecutions.delete(execution);
2004
- finish();
2005
2250
  }
2006
2251
  };
2007
- const finalizeObservation = async (result, params, ctx) => {
2252
+ const finalizeObservation = async (result, params, ctx, branch) => {
2008
2253
  if ("args" in params && params.args && isPlainTextInspectionArgs(params.args))
2009
2254
  return result;
2010
2255
  const { renderAgentBrowserObservation } = await import("./lib/results/presentation/large-output.js");
@@ -2012,27 +2257,59 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
2012
2257
  if (result.isError && details.error === undefined)
2013
2258
  details.error = details.validationError ?? details.summary ?? result.content.filter(item => item.type === "text").map(item => item.text).join("\n");
2014
2259
  const priorManifest = isSessionArtifactManifest(details.artifactManifest) ? details.artifactManifest : undefined;
2015
- const rendered = await renderAgentBrowserObservation({ content: result.content, details, json: "code" in params || params.args?.includes("--json") === true, succeeded: result.isError !== true, persistentArtifactStore: getPersistentSessionArtifactStore(ctx) });
2016
- artifactManifest = mergeBrowserRunArtifactManifest(artifactManifest, priorManifest, rendered.artifactManifest);
2017
- return { ...result, content: rendered.content, details: { ...details, ...(artifactManifest ? { artifactManifest: redactSensitiveValue(artifactManifest) } : {}) } };
2260
+ const renderOptions = { content: result.content, details, json: "code" in params || params.args?.includes("--json") === true, succeeded: result.isError !== true,
2261
+ persistentArtifactStore: branch.isCurrent() ? getPersistentSessionArtifactStore(ctx) : undefined,
2262
+ withArtifactWrite: (write) => artifactExecutionQueue.run(() => {
2263
+ if (!branch.isCurrent())
2264
+ renderOptions.persistentArtifactStore = undefined;
2265
+ return write();
2266
+ }) };
2267
+ const rendered = await renderAgentBrowserObservation(renderOptions);
2268
+ if (!priorManifest && !rendered.artifactManifest)
2269
+ return { ...result, content: rendered.content, details: { ...details, browserEventVersion: 1 } };
2270
+ return artifactExecutionQueue.run(async () => {
2271
+ if (!branch.isCurrent())
2272
+ return { ...result, content: rendered.content, details: { ...details, browserEventVersion: 1, artifactManifest: rendered.artifactManifest } };
2273
+ const before = artifactManifest;
2274
+ const merged = mergeBrowserRunArtifactManifest(artifactManifest, priorManifest, rendered.artifactManifest);
2275
+ const changes = artifactChanges(before, merged);
2276
+ if (changes && hasPublishedBrowserJournal(ctx.sessionManager)) {
2277
+ const record = { event: { version: 1, phase: "state", operationId: randomUUID(), toolCallId: "observation", commandIndex: 0, isError: result.isError === true, state: {}, artifacts: changes } };
2278
+ if (!await appendBrowserRecord(ctx.sessionManager, () => appendBrowserTransition(pi, record), record, branch)
2279
+ || !branch.isCurrent())
2280
+ return { ...result, content: rendered.content, details: { ...details, browserEventVersion: 1, artifactManifest: rendered.artifactManifest } };
2281
+ }
2282
+ artifactManifest = mergeBrowserRunArtifactManifest(artifactManifest, priorManifest, rendered.artifactManifest);
2283
+ return { ...result, content: rendered.content, details: { ...details, browserEventVersion: 1, artifactManifest: invocationArtifactManifest(artifactManifest, before, details, priorManifest) } };
2284
+ });
2018
2285
  };
2019
2286
  registerAgentBrowserToolSurface(pi, {
2020
2287
  async execute(id, params, signal, onUpdate, ctx) {
2288
+ const branch = captureCurrentBrowserBranch(ctx);
2021
2289
  let result;
2022
2290
  try {
2023
- result = await agentBrowserTool.execute(id, params, signal, onUpdate, ctx);
2291
+ result = await agentBrowserTool.execute(id, params, signal, onUpdate, ctx, id, undefined, true, undefined, branch);
2024
2292
  }
2025
2293
  catch (error) {
2026
2294
  result = browserExecutionFailure(error, signal);
2027
2295
  }
2028
- return finalizeObservation(result, params, ctx);
2296
+ return finalizeObservation(result, params, ctx, branch);
2029
2297
  },
2030
2298
  async executeCode(id, params, signal, onUpdate, ctx) {
2031
- const result = await executeCode(id, params, signal, onUpdate, ctx);
2032
- return finalizeObservation(result, params, ctx);
2299
+ const branch = captureCurrentBrowserBranch(ctx);
2300
+ let finish;
2301
+ const execution = new Promise(resolve => { finish = resolve; });
2302
+ activeScriptExecutions.add(execution);
2303
+ try {
2304
+ const result = await executeCode(id, params, signal, onUpdate, ctx, branch);
2305
+ return await finalizeObservation(result, params, ctx, branch);
2306
+ }
2307
+ finally {
2308
+ activeScriptExecutions.delete(execution);
2309
+ finish();
2310
+ }
2033
2311
  },
2034
2312
  executionMode: beforeExecute ? "sequential" : undefined,
2035
- promptGuidelines: toolPromptGuidelines,
2036
2313
  renderCall: agentBrowserTool.renderCall, renderResult: agentBrowserTool.renderResult,
2037
2314
  });
2038
2315
  registerWebSearchToolIfAvailable(agentBrowserConfig);