pi-agent-browser-native 0.8.2 → 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 +23 -0
  2. package/README.md +16 -7
  3. package/dist/extensions/agent-browser/index.js +430 -157
  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 +12 -23
  22. package/dist/extensions/agent-browser/lib/web-search.js +0 -8
  23. package/docs/ARCHITECTURE.md +13 -11
  24. package/docs/COMMAND_REFERENCE.md +9 -7
  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 +17 -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 { AGENT_BROWSER_DISCOVERY_GROUP, 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))
@@ -1230,7 +1328,6 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1230
1328
  if (webSearchToolRegistered || !canRegisterWebSearchTool(configState))
1231
1329
  return;
1232
1330
  pi.registerTool({
1233
- ...{ discovery: { group: AGENT_BROWSER_DISCOVERY_GROUP, role: "entry" } },
1234
1331
  ...createAgentBrowserWebSearchTool(configState, {
1235
1332
  loadConfigState(ctx) {
1236
1333
  return loadAgentBrowserConfigSync({
@@ -1243,7 +1340,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1243
1340
  webSearchToolRegistered = true;
1244
1341
  };
1245
1342
  pi.on("session_start", async (_event, ctx) => {
1246
- restoreBranchBackedState(ctx, { resetRuntimeOwnership: true });
1343
+ await restoreBranchBackedState(ctx, { resetRuntimeOwnership: true });
1247
1344
  electronChildProcesses = new Map();
1248
1345
  registerWebSearchToolIfAvailable(loadAgentBrowserConfigSync({
1249
1346
  cwd: ctx.cwd,
@@ -1261,7 +1358,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1261
1358
  controller.abort();
1262
1359
  await Promise.allSettled([...activeScriptExecutions]);
1263
1360
  await managedSessionExecutionQueue.run(() => artifactExecutionQueue.run(async () => {
1264
- restoreBranchBackedState(ctx, { resetRuntimeOwnership: false });
1361
+ await restoreBranchBackedState(ctx, { resetRuntimeOwnership: false });
1265
1362
  await recoverScriptSessionLeasesWithinQueue(ctx);
1266
1363
  flushRecordingReservations();
1267
1364
  notifyRecordingPersistence(ctx);
@@ -1273,6 +1370,8 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1273
1370
  const checkpointPi = pi;
1274
1371
  checkpointPi.on("session_checkpoint", async (event, ctx) => {
1275
1372
  const blocked = (reason) => ({ sleepReady: false, reason });
1373
+ if (browserReplayError)
1374
+ return blocked(browserReplayError);
1276
1375
  if (activeScriptControllers.size || activeScriptExecutions.size)
1277
1376
  return blocked("Browser code execution is pending");
1278
1377
  flushRecordingReservations();
@@ -1280,7 +1379,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1280
1379
  return blocked("Browser recording journal persistence is dirty");
1281
1380
  if (activeRecordingReservations.size)
1282
1381
  return blocked("Browser recording is pending; finish it explicitly before sleep");
1283
- const entries = ctx.sessionManager.getEntries();
1382
+ const entries = await readBrowserEntries(ctx.sessionManager, true);
1284
1383
  if ([...getScriptSessionLeasesFromBranch(entries).values()].some(lease => lease.cleanup !== "closed")
1285
1384
  || [...ownedManagedSessions.values()].some(owner => isAgentBrowserScriptSessionName(owner.sessionName))) {
1286
1385
  return blocked("Browser script cleanup lease is unresolved");
@@ -1331,7 +1430,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1331
1430
  });
1332
1431
  // Owned routing must not mask an explicit caller's same-name ambient daemon.
1333
1432
  for (const inspectionContext of context ? [{ ...context, reuseOnly: true }, undefined] : [undefined]) {
1334
- 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 })));
1335
1434
  if (daemon.status !== "inactive")
1336
1435
  return blocked("Browser daemon is live or unverified; finish browser work explicitly before sleep");
1337
1436
  }
@@ -1351,7 +1450,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1351
1450
  const shutdownCwd = ctx?.cwd ?? managedSessionCwd;
1352
1451
  const quitting = event?.reason === "quit";
1353
1452
  preservedElectronProfileDirs = quitting
1354
- ? []
1453
+ ? getActiveElectronRecords(electronLaunchRecords).filter(record => !ownedElectronLaunchRecords.has(record.launchId)).map(record => record.userDataDir)
1355
1454
  : getActiveElectronRecords(electronLaunchRecords).map((record) => record.userDataDir);
1356
1455
  const electronRecordsToCleanup = quitting
1357
1456
  ? ownedElectronLaunchRecords
@@ -1402,11 +1501,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1402
1501
  ownedManagedSessions.clear();
1403
1502
  await cleanupSecureTempArtifacts({ preservePaths: preservedElectronProfileDirs });
1404
1503
  });
1405
- pi.on("before_agent_start", async (event, ctx) => {
1406
- const sectionTools = event.systemPromptOptions?.sectionTools;
1407
- if (!Object.hasOwn(sectionTools ?? {}, "agent_browser") && !shouldAppendBrowserSystemPrompt(event.prompt)) {
1408
- return undefined;
1409
- }
1504
+ const fullInstructions = (ctx) => {
1410
1505
  const runtimeConfig = loadAgentBrowserConfigSync({
1411
1506
  cwd: ctx.cwd,
1412
1507
  includeProjectConfig: shouldIncludeProjectConfig(ctx),
@@ -1422,7 +1517,35 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1422
1517
  const runtimeConfigPrompt = browserGuidance.length > 0
1423
1518
  ? `\n\nProject agent_browser config guidance:\n${browserGuidance.map((line) => `- ${line}`).join("\n")}`
1424
1519
  : "";
1425
- 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);
1426
1549
  });
1427
1550
  pi.on("tool_call", async (event, ctx) => {
1428
1551
  const promptPolicy = buildPromptPolicy(getLatestUserPrompt(ctx.sessionManager.getBranch()));
@@ -1438,6 +1561,18 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1438
1561
  }
1439
1562
  });
1440
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
+ };
1441
1576
  const agentBrowserTool = {
1442
1577
  renderCall(args, theme, context) {
1443
1578
  const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
@@ -1451,7 +1586,8 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1451
1586
  component.setState(formatAgentBrowserRenderResult(result, options, theme, context.isError), options.expanded, theme);
1452
1587
  return component;
1453
1588
  },
1454
- 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);
1455
1591
  let operationCwd;
1456
1592
  try {
1457
1593
  operationCwd = capturedCwd ?? resolveExecutionCwd(pi, ctx);
@@ -1468,6 +1604,8 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1468
1604
  if (admittedInput.status === "invalid") {
1469
1605
  return buildValidationFailureResult(admittedInput);
1470
1606
  }
1607
+ if (browserReplayError && !isPlainTextInspectionArgs(admittedInput.toolArgs))
1608
+ return browserExecutionFailure(new Error(browserReplayError), signal, "validation-error");
1471
1609
  const explicitConfig = scanUpstreamGlobalFlagOccurrences(admittedInput.toolArgs, "--config").length > 0 || getAgentBrowserProcessEnvironment().AGENT_BROWSER_CONFIG !== undefined;
1472
1610
  // Managed selection must follow earlier queued fresh launches, while file paths keep their admission snapshot.
1473
1611
  const managedAtAdmission = admittedInput.kind !== "electron"
@@ -1533,7 +1671,31 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1533
1671
  branchRecords: electronLaunchRecords,
1534
1672
  compiledElectron,
1535
1673
  ownedRecords: ownedElectronLaunchRecords,
1674
+ ownerSessionId: ctx.sessionManager.getSessionId(),
1536
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
+ }
1537
1699
  let electronHostResult = await handleElectronHostInput({
1538
1700
  attachedSessionKeys,
1539
1701
  compiledElectron,
@@ -1547,7 +1709,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1547
1709
  managedSessionRestoreState,
1548
1710
  ownedManagedSessions,
1549
1711
  redactedCompiledElectron,
1550
- sessionPageState,
1712
+ sessionPageState: workingPageState,
1551
1713
  signal: executionSignal,
1552
1714
  });
1553
1715
  if (electronHostResult && compiledElectron?.action === "cleanup") {
@@ -1575,6 +1737,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1575
1737
  retireRecordingSession(identity.sessionName, identity.namespace);
1576
1738
  const closedSessionKey = getSessionContextKey(identity.sessionName, identity.namespace) ?? identity.sessionName;
1577
1739
  clearSessionScopedBrowserState(closedSessionKey);
1740
+ workingPageState.clearSession(closedSessionKey);
1578
1741
  if (closedSessionKey === (getSessionContextKey(managedSessionName, managedSessionNamespace) ?? managedSessionName)) {
1579
1742
  managedSessionActive = false;
1580
1743
  managedSessionCompatibilityWorkaround = undefined;
@@ -1585,15 +1748,37 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1585
1748
  managedSessionName = createFreshSessionName(managedSessionBaseName, ephemeralSessionSeed, freshSessionOrdinal);
1586
1749
  }
1587
1750
  }
1588
- if (artifactManifest) {
1589
- electronHostResult = {
1590
- ...electronHostResult,
1591
- details: {
1592
- ...(isRecord(electronHostResult.details) ? electronHostResult.details : {}),
1593
- artifactManifest: redactSensitiveValue(artifactManifest),
1594
- artifactRetentionSummary: formatSessionArtifactRetentionSummary(artifactManifest),
1595
- },
1596
- };
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);
1597
1782
  }
1598
1783
  }
1599
1784
  if (electronHostResult && executionSignal?.aborted) {
@@ -1614,7 +1799,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1614
1799
  }) : runElectronHostInput(executionSignal);
1615
1800
  if (!shouldSerializeElectronHostInput(compiledElectron))
1616
1801
  return execute();
1617
- const records = getElectronHostLaunchRecordsForInput({ branchRecords: electronLaunchRecords, compiledElectron, ownedRecords: ownedElectronLaunchRecords });
1802
+ const records = getElectronHostLaunchRecordsForInput({ branchRecords: electronLaunchRecords, compiledElectron, ownedRecords: ownedElectronLaunchRecords, ownerSessionId: ctx.sessionManager.getSessionId() });
1618
1803
  const launchId = compiledElectron && "launchId" in compiledElectron ? compiledElectron.launchId : undefined;
1619
1804
  const selected = launchId ? [records.get(launchId)].filter((record) => record !== undefined) : getActiveElectronRecords(records);
1620
1805
  const sessions = selected.flatMap(record => record.sessionName ? [{ sessionName: record.sessionName, namespace: record.namespace }] : []);
@@ -1655,9 +1840,10 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1655
1840
  // before capturing state. Registration, rendering, and restore stay synchronous/eager.
1656
1841
  const { runAgentBrowserTool } = await import("./lib/orchestration/browser-run/index.js");
1657
1842
  flushRecordingReservations();
1658
- const branchRestoreGenerationAtStart = branchRestoreGeneration;
1659
1843
  const generationAtStart = branchStateGeneration;
1660
- const sessionPageStateUpdate = sessionPageState.beginUpdate();
1844
+ const workingPageState = sessionPageState.fork();
1845
+ const priorPages = workingPageState.views();
1846
+ const sessionPageStateUpdate = workingPageState.beginUpdate();
1661
1847
  const browserRunState = {
1662
1848
  activeRecordingReservations,
1663
1849
  artifactManifest,
@@ -1678,7 +1864,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1678
1864
  managedSessionRestoreState,
1679
1865
  networkRoutesBySession,
1680
1866
  ownedManagedSessions,
1681
- sessionPageState,
1867
+ sessionPageState: workingPageState,
1682
1868
  traceOwners,
1683
1869
  };
1684
1870
  const selectedPlan = buildExecutionPlan(toolArgs, {
@@ -1691,6 +1877,30 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1691
1877
  stdin: resolvedInput.toolStdin,
1692
1878
  browserIndependentReadConfirmation: readConfirmation?.capabilities?.readRequiresConfirmation === true,
1693
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
+ }
1694
1904
  const initialArtifactManifest = browserRunState.artifactManifest;
1695
1905
  const initialNetworkRoutesBySession = browserRunState.networkRoutesBySession;
1696
1906
  const attachedSessionRequested = isAttachedBrowserInvocation(toolArgs)
@@ -1702,28 +1912,37 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1702
1912
  : getSessionContextKey(selectedPlan.sessionName, selectedPlan.namespace)
1703
1913
  ?? getSessionContextKey(browserRunState.managedSessionName, browserRunState.managedSessionNamespace);
1704
1914
  const attachedSessionKnown = reusableSessionKey !== undefined && attachedSessionKeys.has(reusableSessionKey);
1705
- let result = await runAgentBrowserTool({
1706
- daemonInactive,
1707
- ctx,
1708
- cwd: browserCwd,
1709
- operationCwd,
1710
- electronPostCommandStatusSettleMs: ELECTRON_POST_COMMAND_STATUS_SETTLE_MS,
1711
- electronProfileIsolationDetails: ELECTRON_PROFILE_ISOLATION_DETAILS,
1712
- implicitSessionCloseTimeoutMs,
1713
- implicitSessionIdleTimeoutMs,
1714
- input: resolvedInput,
1715
- modelVisible,
1716
- onUpdate,
1717
- params,
1718
- establishAttachedBrowserSession: attachedSessionRequested && !attachedSessionKnown,
1719
- preserveAttachedBrowserSession: attachedSessionRequested || attachedSessionKnown,
1720
- promptPolicy,
1721
- sessionPageStateUpdate,
1722
- signal: executionSignal,
1723
- state: browserRunState,
1724
- });
1725
- 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();
1726
1943
  const resultDetails = isRecord(result.details) ? result.details : undefined;
1944
+ if (resultDetails && begin.event.state.wrapperManaged === true)
1945
+ resultDetails.managedSessionSocketDir = begin.event.state.managedSessionSocketDir;
1727
1946
  if (typeof resultDetails?.sessionName === "string" && browserRunState.managedSessionActive
1728
1947
  && getAgentBrowserSessionIdentityKey(resultDetails.sessionName, typeof resultDetails.namespace === "string" ? resultDetails.namespace : undefined) === getAgentBrowserSessionIdentityKey(browserRunState.managedSessionName, browserRunState.managedSessionNamespace)) {
1729
1948
  resultDetails.managedSessionCwd = browserRunState.managedSessionCwd;
@@ -1780,18 +1999,10 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1780
1999
  if (reservation)
1781
2000
  result = appendActiveRecordingCleanupAction(result, reservation);
1782
2001
  }
1783
- if (artifactManifest) {
1784
- result = {
1785
- ...result,
1786
- details: {
1787
- ...(isRecord(result.details) ? result.details : {}),
1788
- artifactManifest: redactSensitiveValue(artifactManifest),
1789
- artifactRetentionSummary: formatSessionArtifactRetentionSummary(artifactManifest),
1790
- },
1791
- };
1792
- }
2002
+ if (artifactManifest && isRecord(result.details))
2003
+ result.details.artifactRetentionSummary = formatSessionArtifactRetentionSummary(artifactManifest);
1793
2004
  }
1794
- const branchStateStillCurrent = generationAtStart === branchStateGeneration;
2005
+ const branchStateStillCurrent = generationAtStart === branchStateGeneration && branch.isCurrent();
1795
2006
  if (serializeBrowserCommand || branchStateStillCurrent) {
1796
2007
  freshSessionOrdinal = Math.max(freshSessionOrdinal, browserRunState.freshSessionOrdinal);
1797
2008
  managedSessionActive = browserRunState.managedSessionActive;
@@ -1806,6 +2017,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1806
2017
  }
1807
2018
  syncOwnedManagedSessionsFromResult(ownedManagedSessions, result, browserRunState.managedSessionCwd);
1808
2019
  mergeActiveElectronLaunchRecords(ownedElectronLaunchRecords, electronLaunchRecords, {
2020
+ ownerSessionId: ctx.sessionManager.getSessionId(),
1809
2021
  branchOwnedLaunchIds: branchOwnedElectronLaunchIds,
1810
2022
  touchedLaunchIds: !result.isError
1811
2023
  ? getTouchedElectronLaunchIds(explicitSessionName ?? browserRunState.managedSessionName, electronLaunchRecords, resultNamespace)
@@ -1814,6 +2026,39 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1814
2026
  if (serializeBrowserCommand)
1815
2027
  branchStateGeneration += 1;
1816
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
+ }
1817
2062
  return applyAgentBrowserOutputPath({ cwd: operationCwd, outputPath, preserveTextContent: Array.isArray(params.args) && params.args.includes("--json"), result: warnRecordingPersistence(result) });
1818
2063
  };
1819
2064
  const closesAllSessions = commandClosesAllSessions(toolArgs, resolvedInput.toolStdin);
@@ -1825,35 +2070,39 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1825
2070
  sessionMode: compiledElectron?.action === "launch" ? "fresh" : params.sessionMode ?? "auto",
1826
2071
  stdin: resolvedInput.toolStdin,
1827
2072
  });
1828
- const run = (executionSignal = signal) => {
1829
- const execute = () => withLaunchDefaults
1830
- ? withLaunchDefaults(inactive => runBrowserCommand(inactive, executionSignal), executionSignal)
1831
- : runBrowserCommand(undefined, executionSignal);
1832
- if (!commandTouchesArtifactLifecycle(toolArgs, resolvedInput.toolStdin, outputPath))
1833
- return execute();
1834
- return artifactExecutionQueue.run(async () => {
1835
- const error = getArtifactPreflightValidationError({ activeRecordingReservations: activeRecordingReservations.values(), args: toolArgs, cwd: operationCwd, outputPath, stdin: resolvedInput.toolStdin });
1836
- 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))
1837
2080
  return execute();
1838
- flushRecordingReservations();
1839
- return warnRecordingPersistence(buildValidationFailureResult({ ...resolvedInput, attemptedKind: resolvedInput.kind, kind: "invalid", status: "invalid", validationError: error }));
1840
- });
1841
- };
1842
- if (plan.validationError || (!plan.sessionName && plan.commandInfo.command !== "read" && !closesAllSessions))
1843
- return run();
1844
- try {
1845
- const owned = plan.managedSessionName !== undefined || ownedManagedSessions.has(getAgentBrowserSessionIdentityKey(plan.sessionName ?? "default", plan.namespace));
1846
- const identities = [await resolveBrowserExecutionIdentity({ namespace: plan.namespace, ownedManagedSession: owned, sessionName: closesAllSessions ? undefined : plan.sessionName ?? "default" })];
1847
- if (managedSessionActive && (params.sessionMode === "fresh" || compiledElectron?.action === "launch") && !explicitSessionName) {
1848
- 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);
1849
2101
  }
1850
- const activePageUrl = sessionPageState.get(getAgentBrowserSessionIdentityKey(plan.sessionName ?? "default", plan.namespace)).tabTarget?.url;
1851
- const deadline = Date.now() + (params.timeoutMs ?? getCommandAwareProcessTimeoutMs(extractUpstreamCommandTokens(toolArgs), resolvedInput.toolStdin, activePageUrl) ?? getAgentBrowserProcessTimeoutMs());
1852
- return await withBrowserExecutionLocks({ identities, signal, deadline, waitOnly: true }, run);
1853
- }
1854
- catch (error) {
1855
- return browserExecutionFailure(error, signal);
1856
- }
2102
+ catch (error) {
2103
+ return browserExecutionFailure(error, signal);
2104
+ }
2105
+ });
1857
2106
  };
1858
2107
  const runWithinSessionQueue = () => {
1859
2108
  if (closesAllSessions)
@@ -1887,9 +2136,15 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1887
2136
  return managedSessionExecutionQueue.run(executeAdmitted, signal);
1888
2137
  },
1889
2138
  };
1890
- const executeCode = async (toolCallId, params, signal, _onUpdate, ctx) => {
1891
- if (!ctx.sessionManager.getSessionFile())
1892
- 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
+ }
1893
2148
  let operationCwd;
1894
2149
  try {
1895
2150
  operationCwd = resolveExecutionCwd(pi, ctx);
@@ -1903,12 +2158,8 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1903
2158
  if (signal?.aborted)
1904
2159
  abort();
1905
2160
  activeScriptControllers.add(controller);
1906
- let finish;
1907
- const execution = new Promise(resolve => { finish = resolve; });
1908
- activeScriptExecutions.add(execution);
1909
2161
  const deadline = Date.now() + (params.timeoutMs ?? AGENT_BROWSER_SCRIPT_DEFAULT_TIMEOUT_MS);
1910
2162
  const deadlineTimer = setTimeout(() => controller.abort(new DOMException("Browser code deadline exceeded.", "TimeoutError")), Math.max(0, deadline - Date.now()));
1911
- const generation = branchRestoreGeneration;
1912
2163
  const output = createBrowserCodeOutput();
1913
2164
  const nativeEnvironment = getAgentBrowserProcessEnvironment();
1914
2165
  try {
@@ -1937,9 +2188,10 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1937
2188
  const namespace = plan.namespace || undefined;
1938
2189
  const key = getAgentBrowserSessionIdentityKey(sessionName, namespace);
1939
2190
  const owned = plan.managedSessionName !== undefined || ownedManagedSessions.has(key);
1940
- const run = async () => {
2191
+ const run = async () => withAgentBrowserProcessEnvironment(ownedManagedSessions.get(key)?.socketDir ? { PI_AGENT_BROWSER_SOCKET_DIR: ownedManagedSessions.get(key).socketDir } : {}, async () => {
1941
2192
  const identity = await resolveBrowserExecutionIdentity({ sessionName, namespace, ownedManagedSession: owned });
1942
2193
  return withBrowserExecutionLock({ identity, deadline, signal: controller.signal }, async (executionSignal) => {
2194
+ let commandIndex = 0;
1943
2195
  let journalFailed = false;
1944
2196
  const run = await runAgentBrowserScript({
1945
2197
  code: params.code, signal: executionSignal, timeoutMs: Math.max(1, deadline - Date.now()),
@@ -1950,33 +2202,24 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1950
2202
  const { args } = bindBrowserCodeCall(inner, { sessionName, namespace });
1951
2203
  if (plan.usedImplicitSession && !managedSessionActive && managedSessionName === sessionName && extractExplicitSessionName(inner.args) === undefined)
1952
2204
  args.splice(args.indexOf("--session"), 2);
1953
- const pending = { args: redactInvocationArgs(args), sessionName, namespace, resultCategory: "failure", sessionTabTargetUnknown: true };
1954
- try {
1955
- appendBrowserTransition(pi, toolCallId, pending, true);
1956
- }
1957
- 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) {
1958
2208
  journalFailed = true;
1959
- throw new Error("Could not persist browser-state intent; the browser command was not run.");
1960
- }
1961
- // Resolve launch defaults from the real command, never the routing-only get-url probe.
1962
- 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));
1963
- const page = sessionPageState.get(key);
1964
- const details = { ...(isRecord(result.details) ? result.details : {}), sessionName, namespace, sessionTabTarget: page.tabTarget, sessionTabTargetUnknown: page.tabTargetUnknown, sessionTabReopenPending: page.tabReopenPending, refSnapshot: page.refSnapshot, refSnapshotInvalidation: page.refSnapshotInvalidation };
1965
- if (generation === branchRestoreGeneration) {
1966
- try {
1967
- appendBrowserTransition(pi, toolCallId, details, result.isError === true);
1968
- }
1969
- catch {
1970
- journalFailed = true;
1971
- throw new Error("The browser command finished, but its state could not be persisted. Inspect before retrying; changes may already have happened.");
1972
- }
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.");
1973
2210
  }
1974
2211
  return output.observe(result);
1975
2212
  },
1976
2213
  });
1977
2214
  const result = await output.finish(run, sessionName, namespace);
1978
- if (artifactManifest && isRecord(result.details))
1979
- 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;
1980
2223
  if (!params.outputPath)
1981
2224
  return result;
1982
2225
  return artifactExecutionQueue.run(async () => {
@@ -1988,7 +2231,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1988
2231
  return applyAgentBrowserOutputPath({ cwd: operationCwd, outputPath: params.outputPath, preserveTextContent: true, result });
1989
2232
  });
1990
2233
  });
1991
- };
2234
+ });
1992
2235
  return shouldSerializeBrowserCommand({ namespace, explicitSessionName: sessionName, managedSessionName, ownedElectronLaunchRecords, ownedManagedSessions })
1993
2236
  ? managedSessionExecutionQueue.run(run, controller.signal)
1994
2237
  : callerOwnedSessionExecutionQueues.run(key, namespace, run, controller.signal);
@@ -2004,11 +2247,9 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
2004
2247
  clearTimeout(deadlineTimer);
2005
2248
  activeScriptControllers.delete(controller);
2006
2249
  signal?.removeEventListener("abort", abort);
2007
- activeScriptExecutions.delete(execution);
2008
- finish();
2009
2250
  }
2010
2251
  };
2011
- const finalizeObservation = async (result, params, ctx) => {
2252
+ const finalizeObservation = async (result, params, ctx, branch) => {
2012
2253
  if ("args" in params && params.args && isPlainTextInspectionArgs(params.args))
2013
2254
  return result;
2014
2255
  const { renderAgentBrowserObservation } = await import("./lib/results/presentation/large-output.js");
@@ -2016,27 +2257,59 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
2016
2257
  if (result.isError && details.error === undefined)
2017
2258
  details.error = details.validationError ?? details.summary ?? result.content.filter(item => item.type === "text").map(item => item.text).join("\n");
2018
2259
  const priorManifest = isSessionArtifactManifest(details.artifactManifest) ? details.artifactManifest : undefined;
2019
- const rendered = await renderAgentBrowserObservation({ content: result.content, details, json: "code" in params || params.args?.includes("--json") === true, succeeded: result.isError !== true, persistentArtifactStore: getPersistentSessionArtifactStore(ctx) });
2020
- artifactManifest = mergeBrowserRunArtifactManifest(artifactManifest, priorManifest, rendered.artifactManifest);
2021
- 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
+ });
2022
2285
  };
2023
2286
  registerAgentBrowserToolSurface(pi, {
2024
2287
  async execute(id, params, signal, onUpdate, ctx) {
2288
+ const branch = captureCurrentBrowserBranch(ctx);
2025
2289
  let result;
2026
2290
  try {
2027
- result = await agentBrowserTool.execute(id, params, signal, onUpdate, ctx);
2291
+ result = await agentBrowserTool.execute(id, params, signal, onUpdate, ctx, id, undefined, true, undefined, branch);
2028
2292
  }
2029
2293
  catch (error) {
2030
2294
  result = browserExecutionFailure(error, signal);
2031
2295
  }
2032
- return finalizeObservation(result, params, ctx);
2296
+ return finalizeObservation(result, params, ctx, branch);
2033
2297
  },
2034
2298
  async executeCode(id, params, signal, onUpdate, ctx) {
2035
- const result = await executeCode(id, params, signal, onUpdate, ctx);
2036
- 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
+ }
2037
2311
  },
2038
2312
  executionMode: beforeExecute ? "sequential" : undefined,
2039
- promptGuidelines: toolPromptGuidelines,
2040
2313
  renderCall: agentBrowserTool.renderCall, renderResult: agentBrowserTool.renderResult,
2041
2314
  });
2042
2315
  registerWebSearchToolIfAvailable(agentBrowserConfig);