pi-agent-browser-native 0.8.2 → 0.9.0

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 (37) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +24 -15
  3. package/dist/extensions/agent-browser/index.js +496 -343
  4. package/dist/extensions/agent-browser/lib/browser-journal.js +439 -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/execution-cwd.js +0 -16
  8. package/dist/extensions/agent-browser/lib/input-modes/job.js +49 -2
  9. package/dist/extensions/agent-browser/lib/input-modes/params.js +32 -35
  10. package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +25 -5
  11. package/dist/extensions/agent-browser/lib/managed-session-restore.js +20 -5
  12. package/dist/extensions/agent-browser/lib/native-output.js +46 -0
  13. package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +97 -78
  14. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +23 -2
  15. package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +52 -17
  16. package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +1 -1
  17. package/dist/extensions/agent-browser/lib/orchestration/script-mode.js +13 -8
  18. package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +30 -16
  19. package/dist/extensions/agent-browser/lib/playbook.js +9 -2
  20. package/dist/extensions/agent-browser/lib/prompt-policy.js +16 -26
  21. package/dist/extensions/agent-browser/lib/results/presentation/content.js +4 -0
  22. package/dist/extensions/agent-browser/lib/results/presentation/large-output.js +17 -9
  23. package/dist/extensions/agent-browser/lib/session-page-state.js +95 -157
  24. package/dist/extensions/agent-browser/lib/temp.js +14 -6
  25. package/dist/extensions/agent-browser/lib/tool-surface.js +50 -33
  26. package/dist/extensions/agent-browser/lib/web-search.js +85 -72
  27. package/docs/ARCHITECTURE.md +25 -25
  28. package/docs/COMMAND_REFERENCE.md +13 -9
  29. package/docs/RELEASE.md +25 -19
  30. package/docs/REQUIREMENTS.md +6 -6
  31. package/docs/SESSION_STORAGE.md +60 -0
  32. package/docs/SUPPORT_MATRIX.md +16 -11
  33. package/docs/TOOL_CONTRACT.md +26 -19
  34. package/package.json +9 -5
  35. package/scripts/build.mjs +4 -3
  36. package/scripts/convert-browser-session.mjs +22 -0
  37. package/scripts/doctor.mjs +2 -2
@@ -1,27 +1,29 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
+ import { randomUUID } from "node:crypto";
2
3
  import { existsSync, readFileSync } from "node:fs";
3
- import { dirname, join } from "node:path";
4
+ import { dirname, isAbsolute, 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
- import { ManagedSessionRestoreState, resolveOwnedManagedSessionContext, withOwnedManagedSessionContext } from "./lib/managed-session-restore.js";
16
+ import { ManagedSessionRestoreState } 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";
20
- import { getBrowserCwdError, resolveExecutionCwd, restoreManagedSessionCwd } from "./lib/execution-cwd.js";
22
+ import { getBrowserCwdError, resolveExecutionCwd } from "./lib/execution-cwd.js";
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, getLatestUserMessage, getMessageText } 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";
@@ -42,7 +44,7 @@ import { appendRecordingReservationTransition, applyRecordingArtifactsToReservat
42
44
  import { createAgentBrowserWebSearchTool } from "./lib/web-search.js";
43
45
  import { scopeReadConfirmationArgs } from "./lib/read-confirmation.js";
44
46
  import { isDirectAgentBrowserBashAllowed, isHarmlessAgentBrowserInspectionCommand, looksLikeDirectAgentBrowserBash, } from "./lib/bash-guard.js";
45
- import { AgentBrowserResultComponent, buildAgentBrowserToolResultPatch, formatAgentBrowserRenderCall, formatAgentBrowserRenderResult, } from "./lib/pi-tool-rendering.js";
47
+ import { AgentBrowserResultComponent, formatAgentBrowserRenderCall, } from "./lib/pi-tool-rendering.js";
46
48
  function browserExecutionFailure(error, signal, fallbackCategory = "upstream-error") {
47
49
  const timeout = error instanceof Error && error.name === "TimeoutError";
48
50
  const summary = redactSensitiveText(error instanceof Error ? error.message : String(error));
@@ -174,103 +176,60 @@ 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"
193
230
  ? { id: workaround.id, reason: workaround.reason }
194
231
  : undefined;
195
232
  }
196
- function restoreManagedSessionCompatibilityWorkaroundFromBranch(branch, sessionName, namespace) {
197
- let restored;
198
- const targetKey = getSessionContextKey(sessionName, namespace);
199
- for (const entry of branch) {
200
- const message = getBrowserResultMessage(entry);
201
- if (!message)
202
- continue;
203
- const details = isRecord(message.details) ? message.details : undefined;
204
- if (!details)
205
- continue;
206
- if (getSessionContextKey(typeof details.sessionName === "string" ? details.sessionName : undefined, typeof details.namespace === "string" ? details.namespace : undefined) !== targetKey)
207
- continue;
208
- const recognizedWorkaround = getRecognizedCompatibilityWorkaround(details.compatibilityWorkaround);
209
- const succeeded = getSuccessfulToolResult(details, message);
210
- const outcome = getManagedSessionOutcome(details);
211
- const activeAfterFailure = recognizedWorkaround
212
- && outcome?.activeAfter === true
213
- && typeof outcome.currentSessionName === "string"
214
- && getSessionContextKey(outcome.currentSessionName, typeof outcome.currentSessionNamespace === "string" ? outcome.currentSessionNamespace : undefined) === targetKey
215
- && (outcome.status === "created" || outcome.status === "replaced" || outcome.status === "unchanged");
216
- if (!succeeded && !activeAfterFailure)
217
- continue;
218
- if (recognizedWorkaround) {
219
- restored = recognizedWorkaround;
220
- }
221
- else if (!canUseHeadlessCompatibilityUserAgent(getToolResultArgs(details))) {
222
- restored = undefined;
223
- }
224
- }
225
- return restored;
226
- }
227
- function restoreManagedSessionHeadedAutosaveDisabledFromBranch(branch, sessionName, namespace) {
228
- let restored = false;
229
- const targetKey = getSessionContextKey(sessionName, namespace);
230
- for (const entry of branch) {
231
- const message = getBrowserResultMessage(entry);
232
- if (!message)
233
- continue;
234
- const details = isRecord(message.details) ? message.details : undefined;
235
- if (!details)
236
- continue;
237
- if (getSessionContextKey(typeof details.sessionName === "string" ? details.sessionName : undefined, typeof details.namespace === "string" ? details.namespace : undefined) !== targetKey)
238
- continue;
239
- const outcome = getManagedSessionOutcome(details);
240
- const activeAfterFailure = outcome?.activeAfter === true
241
- && typeof outcome.currentSessionName === "string"
242
- && getSessionContextKey(outcome.currentSessionName, typeof outcome.currentSessionNamespace === "string" ? outcome.currentSessionNamespace : undefined) === targetKey;
243
- if ((getSuccessfulToolResult(details, message) || activeAfterFailure) && typeof details.managedSessionHeadedAutosaveDisabled === "boolean") {
244
- restored = details.managedSessionHeadedAutosaveDisabled;
245
- }
246
- }
247
- return restored;
248
- }
249
- function restoreManagedSessionHeadedAutosaveIntervalFromBranch(branch, sessionName, namespace) {
250
- let restored;
251
- const targetKey = getSessionContextKey(sessionName, namespace);
252
- for (const entry of branch) {
253
- const message = getBrowserResultMessage(entry);
254
- if (!message)
255
- continue;
256
- const details = isRecord(message.details) ? message.details : undefined;
257
- if (!details)
258
- continue;
259
- if (getSessionContextKey(typeof details.sessionName === "string" ? details.sessionName : undefined, typeof details.namespace === "string" ? details.namespace : undefined) !== targetKey)
260
- continue;
261
- const outcome = getManagedSessionOutcome(details);
262
- const activeAfterFailure = outcome?.activeAfter === true
263
- && typeof outcome.currentSessionName === "string"
264
- && getSessionContextKey(outcome.currentSessionName, typeof outcome.currentSessionNamespace === "string" ? outcome.currentSessionNamespace : undefined) === targetKey;
265
- if (!getSuccessfulToolResult(details, message) && !activeAfterFailure)
266
- continue;
267
- if (typeof details.managedSessionHeadedAutosaveInterval === "string")
268
- restored = details.managedSessionHeadedAutosaveInterval;
269
- else if (details.managedSessionHeadedAutosaveDisabled === true)
270
- restored = "0";
271
- }
272
- return restored;
273
- }
274
233
  function getToolResultArgs(details) {
275
234
  if (Array.isArray(details.args) && details.args.every((arg) => typeof arg === "string"))
276
235
  return details.args;
@@ -346,7 +305,7 @@ function trackOwnedManagedSession(sessions, sessionName, cwd, options = {}) {
346
305
  : existing?.compatibilityWorkaround;
347
306
  const headedManagedAutosaveDisabled = options.headedManagedAutosaveDisabled ?? existing?.headedManagedAutosaveDisabled;
348
307
  const headedManagedAutosaveInterval = options.headedManagedAutosaveInterval ?? existing?.headedManagedAutosaveInterval;
349
- sessions.set(key, { branchOwned, compatibilityWorkaround, cwd, headedManagedAutosaveDisabled, headedManagedAutosaveInterval, namespace: options.namespace, sessionName });
308
+ sessions.set(key, { branchOwned, compatibilityWorkaround, cwd, headedManagedAutosaveDisabled, headedManagedAutosaveInterval, namespace: options.namespace, sessionName, socketDir: options.socketDir ?? existing?.socketDir });
350
309
  }
351
310
  function untrackOwnedManagedSession(sessions, sessionName, namespace) {
352
311
  if (!sessionName)
@@ -382,6 +341,7 @@ function syncOwnedManagedSessionsFromResult(sessions, result, cwd) {
382
341
  headedManagedAutosaveDisabled: details?.managedSessionHeadedAutosaveDisabled === true,
383
342
  headedManagedAutosaveInterval: typeof details?.managedSessionHeadedAutosaveInterval === "string" ? details.managedSessionHeadedAutosaveInterval : undefined,
384
343
  namespace,
344
+ socketDir: typeof details?.managedSessionSocketDir === "string" ? details.managedSessionSocketDir : undefined,
385
345
  });
386
346
  }
387
347
  if (succeeded && status === "closed") {
@@ -394,6 +354,8 @@ function getTouchedElectronLaunchIds(sessionName, records, namespace) {
394
354
  }
395
355
  function mergeActiveElectronLaunchRecords(target, source, options = {}) {
396
356
  for (const record of getActiveElectronRecords(source)) {
357
+ if (options.ownerSessionId !== undefined && record.ownerSessionId !== options.ownerSessionId)
358
+ continue;
397
359
  const alreadyRuntimeOwned = target.has(record.launchId) && options.branchOwnedLaunchIds?.has(record.launchId) === false;
398
360
  target.set(record.launchId, record);
399
361
  if (options.branchOwnedLaunchIds) {
@@ -450,8 +412,10 @@ function shouldSerializeElectronHostInput(compiledElectron) {
450
412
  return compiledElectron?.action === "status" || compiledElectron?.action === "probe" || compiledElectron?.action === "cleanup";
451
413
  }
452
414
  function getElectronHostLaunchRecordsForInput(options) {
415
+ if (options.compiledElectron?.action === "cleanup") {
416
+ return new Map([...mergeElectronLaunchRecordMaps(options.branchRecords, options.ownedRecords)].filter(([, record]) => record.ownerSessionId === options.ownerSessionId));
417
+ }
453
418
  if (options.compiledElectron?.action === "status" ||
454
- options.compiledElectron?.action === "cleanup" ||
455
419
  (options.compiledElectron?.action === "probe" && options.compiledElectron.launchId)) {
456
420
  return mergeElectronLaunchRecordMaps(options.branchRecords, options.ownedRecords);
457
421
  }
@@ -536,6 +500,7 @@ function collectBranchManagedResourceEvents(branch) {
536
500
  managedSessionActiveIdentities: new Map(),
537
501
  managedSessionActiveRanks: new Map(),
538
502
  managedSessionCloseRanks: new Map(),
503
+ managedSessionLaunchState: new Map(),
539
504
  };
540
505
  let eventRank = 0;
541
506
  for (const entry of branch) {
@@ -557,6 +522,32 @@ function collectBranchManagedResourceEvents(branch) {
557
522
  const batchCloseLifecycle = getSuccessfulBatchCloseLifecycle(details.batchSteps);
558
523
  const closeAllApplied = detailsReportCloseAllApplied(details, succeeded);
559
524
  const outcome = getManagedSessionOutcome(details);
525
+ if (sessionName) {
526
+ const key = getSessionContextKey(sessionName, namespace) ?? sessionName;
527
+ const launch = events.managedSessionLaunchState.get(key) ?? { compatibilityWorkaround: undefined, headedManagedAutosaveDisabled: false };
528
+ if (typeof details.managedSessionCwd === "string" && isAbsolute(details.managedSessionCwd))
529
+ launch.cwd = details.managedSessionCwd;
530
+ if (typeof details.managedSessionSocketDir === "string")
531
+ launch.socketDir = details.managedSessionSocketDir;
532
+ const retainedAfterFailure = outcome?.activeAfter === true && typeof outcome.currentSessionName === "string"
533
+ && getSessionContextKey(outcome.currentSessionName, typeof outcome.currentSessionNamespace === "string" ? outcome.currentSessionNamespace : undefined) === key;
534
+ const workaround = getRecognizedCompatibilityWorkaround(details.compatibilityWorkaround);
535
+ if (succeeded || workaround && retainedAfterFailure && ["created", "replaced", "unchanged"].includes(String(outcome?.status))) {
536
+ if (workaround)
537
+ launch.compatibilityWorkaround = workaround;
538
+ else if (!canUseHeadlessCompatibilityUserAgent(getToolResultArgs(details)))
539
+ launch.compatibilityWorkaround = undefined;
540
+ }
541
+ if (succeeded || retainedAfterFailure) {
542
+ if (typeof details.managedSessionHeadedAutosaveDisabled === "boolean")
543
+ launch.headedManagedAutosaveDisabled = details.managedSessionHeadedAutosaveDisabled;
544
+ if (typeof details.managedSessionHeadedAutosaveInterval === "string")
545
+ launch.headedManagedAutosaveInterval = details.managedSessionHeadedAutosaveInterval;
546
+ else if (details.managedSessionHeadedAutosaveDisabled === true)
547
+ launch.headedManagedAutosaveInterval = "0";
548
+ }
549
+ events.managedSessionLaunchState.set(key, launch);
550
+ }
560
551
  const outcomeSucceeded = outcome?.succeeded === true;
561
552
  const outcomeStatus = typeof outcome?.status === "string" ? outcome.status : undefined;
562
553
  const outcomeCurrentSessionName = typeof outcome?.currentSessionName === "string" ? outcome.currentSessionName : undefined;
@@ -631,7 +622,7 @@ async function closeOwnedManagedSessionsExcept(sessions, restoreState, keepSessi
631
622
  for (const [key, owner] of [...sessions]) {
632
623
  if (key === keepKey)
633
624
  continue;
634
- const error = await closeManagedSession({ cwd: owner.cwd, headedManagedAutosaveInterval: owner.headedManagedAutosaveInterval, namespace: owner.namespace, preserveAttachedBrowserSession: attachedSessionKeys.has(key), restoreState, sessionName: owner.sessionName, timeoutMs });
625
+ 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
626
  if (!error) {
636
627
  sessions.delete(key);
637
628
  onClosed?.(owner);
@@ -804,7 +795,7 @@ function hasArgvFlag(argv, longFlag, shortFlag) {
804
795
  function shouldIncludeProjectConfig(ctx, argv = process.argv) {
805
796
  if (hasArgvFlag(argv, "--no-approve", "-na"))
806
797
  return false;
807
- return ctx?.isProjectTrusted?.() ?? true;
798
+ return ctx.isProjectTrusted();
808
799
  }
809
800
  export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
810
801
  const ephemeralSessionSeed = createEphemeralSessionSeed();
@@ -812,15 +803,23 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
812
803
  cwd: process.cwd(),
813
804
  includeProjectConfig: false,
814
805
  });
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
806
  const implicitSessionIdleTimeoutMs = String(getImplicitSessionIdleTimeoutMs());
823
807
  const implicitSessionCloseTimeoutMs = getImplicitSessionCloseTimeoutMs();
808
+ let promptState;
809
+ const restorePromptState = (ctx) => {
810
+ return promptState = { sessionId: ctx.sessionManager.getSessionId(), message: getLatestUserMessage(ctx.sessionManager) };
811
+ };
812
+ const getPromptPolicy = (ctx) => {
813
+ const state = promptState?.sessionId === ctx.sessionManager.getSessionId() ? promptState : restorePromptState(ctx);
814
+ // Native message_end precedes later transforms/persistence; retain the message
815
+ // reference so replacement user text cannot leave policy on the earlier text.
816
+ const text = getMessageText(state.message?.content);
817
+ if (!state.policy || state.text !== text) {
818
+ state.text = text;
819
+ state.policy = buildPromptPolicy(text);
820
+ }
821
+ return state.policy;
822
+ };
824
823
  let webSearchToolRegistered = false;
825
824
  let managedSessionActive = false;
826
825
  let managedSessionBaseName = createImplicitSessionName(undefined, process.cwd(), ephemeralSessionSeed);
@@ -832,6 +831,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
832
831
  let managedSessionNamespace;
833
832
  let freshSessionOrdinal = 0;
834
833
  let sessionPageState = new SessionPageState();
834
+ let browserReplayError;
835
835
  let traceOwners = new Map();
836
836
  let artifactManifest;
837
837
  let activeRecordingReservations = new Map();
@@ -841,6 +841,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
841
841
  let networkRoutesBySession = new Map();
842
842
  let electronLaunchRecords = new Map();
843
843
  let ownedElectronLaunchRecords = new Map();
844
+ let browserOwnerSessionId = "";
844
845
  let branchOwnedElectronLaunchIds = new Set();
845
846
  let electronChildProcesses = new Map();
846
847
  const managedSessionRestoreState = new ManagedSessionRestoreState();
@@ -1054,13 +1055,13 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1054
1055
  }));
1055
1056
  if (closeError) {
1056
1057
  try {
1057
- appendScriptSessionLease(pi, sessionName, "failed");
1058
+ appendScriptSessionLease(pi, sessionName, "failed", browserOwnerSessionId);
1058
1059
  }
1059
1060
  catch { }
1060
1061
  return redactSensitiveText(closeError);
1061
1062
  }
1062
1063
  try {
1063
- appendScriptSessionLease(pi, sessionName, "closed");
1064
+ appendScriptSessionLease(pi, sessionName, "closed", browserOwnerSessionId);
1064
1065
  }
1065
1066
  catch {
1066
1067
  managedSessionRestoreState.disable(sessionName);
@@ -1072,11 +1073,11 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1072
1073
  clearSessionScopedBrowserState(sessionName, AGENT_BROWSER_SCRIPT_NAMESPACE);
1073
1074
  return undefined;
1074
1075
  };
1075
- const recoverScriptSessionLeasesWithinQueue = async (ctx) => {
1076
+ const recoverScriptSessionLeasesWithinQueue = async (ctx, branch) => {
1076
1077
  const pendingSessionNames = new Set([...ownedManagedSessions.values()]
1077
1078
  .map((session) => session.sessionName)
1078
1079
  .filter(isAgentBrowserScriptSessionName));
1079
- for (const lease of getScriptSessionLeasesFromBranch(ctx.sessionManager.getBranch()).values()) {
1080
+ for (const lease of getScriptSessionLeasesFromBranch(branch, ctx.sessionManager.getSessionId()).values()) {
1080
1081
  if (lease.cleanup !== "closed")
1081
1082
  pendingSessionNames.add(lease.sessionName);
1082
1083
  }
@@ -1086,15 +1087,26 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1086
1087
  await closeScriptSessionLeaseWithinQueue(sessionName, ctx.cwd);
1087
1088
  }
1088
1089
  };
1089
- const restoreBranchBackedState = (ctx, options) => {
1090
+ const restoreBranchBackedState = async (ctx, options) => {
1090
1091
  branchRestoreGeneration += 1;
1091
1092
  branchStateGeneration += 1;
1093
+ restorePromptState(ctx);
1092
1094
  const previousManagedSessionActive = managedSessionActive;
1093
1095
  const previousManagedSessionName = managedSessionName;
1094
1096
  const previousFreshSessionOrdinal = freshSessionOrdinal;
1095
1097
  const previousAttachedSessionKeys = attachedSessionKeys;
1096
1098
  managedSessionBaseName = createImplicitSessionName(ctx.sessionManager.getSessionId(), ctx.cwd, ephemeralSessionSeed);
1097
- const branch = ctx.sessionManager.getBranch();
1099
+ browserOwnerSessionId = ctx.sessionManager.getSessionId();
1100
+ let branch;
1101
+ try {
1102
+ branch = await readBrowserEntries(ctx.sessionManager);
1103
+ browserReplayError = undefined;
1104
+ }
1105
+ catch (error) {
1106
+ browserReplayError = error instanceof Error ? error.message : String(error);
1107
+ sessionPageState.reset();
1108
+ throw error;
1109
+ }
1098
1110
  const branchResourceEvents = collectBranchManagedResourceEvents(branch);
1099
1111
  const restoredState = restoreManagedSessionStateFromBranch(branch, managedSessionBaseName);
1100
1112
  managedSessionRestoreState.replace(restoredState.managedSessionRestoreDisabledIdentities, {
@@ -1120,15 +1132,11 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1120
1132
  : createFreshSessionName(managedSessionBaseName, ephemeralSessionSeed, nextFreshSessionOrdinal)
1121
1133
  : restoredState.sessionName;
1122
1134
  managedSessionNamespace = shouldReservePostCloseSession ? undefined : restoredState.namespace;
1123
- managedSessionCompatibilityWorkaround = managedSessionActive
1124
- ? restoreManagedSessionCompatibilityWorkaroundFromBranch(branch, managedSessionName, managedSessionNamespace)
1125
- : undefined;
1126
- managedSessionHeadedAutosaveDisabled = managedSessionActive
1127
- && restoreManagedSessionHeadedAutosaveDisabledFromBranch(branch, managedSessionName, managedSessionNamespace);
1128
- managedSessionHeadedAutosaveInterval = managedSessionActive
1129
- ? restoreManagedSessionHeadedAutosaveIntervalFromBranch(branch, managedSessionName, managedSessionNamespace)
1130
- : undefined;
1131
- managedSessionCwd = restoreManagedSessionCwd(branch, managedSessionName, managedSessionNamespace, ctx.cwd);
1135
+ const currentLaunch = branchResourceEvents.managedSessionLaunchState.get(getSessionContextKey(managedSessionName, managedSessionNamespace) ?? managedSessionName);
1136
+ managedSessionCompatibilityWorkaround = managedSessionActive ? currentLaunch?.compatibilityWorkaround : undefined;
1137
+ managedSessionHeadedAutosaveDisabled = managedSessionActive && currentLaunch?.headedManagedAutosaveDisabled === true;
1138
+ managedSessionHeadedAutosaveInterval = managedSessionActive ? currentLaunch?.headedManagedAutosaveInterval : undefined;
1139
+ managedSessionCwd = currentLaunch?.cwd ?? ctx.cwd;
1132
1140
  freshSessionOrdinal = nextFreshSessionOrdinal;
1133
1141
  sessionPageState = SessionPageState.fromBranch(branch);
1134
1142
  traceOwners = new Map();
@@ -1179,12 +1187,14 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1179
1187
  continue;
1180
1188
  if (!isRestorableManagedSessionName(identity.sessionName, managedSessionBaseName))
1181
1189
  continue;
1182
- trackOwnedManagedSession(ownedManagedSessions, identity.sessionName, restoreManagedSessionCwd(branch, identity.sessionName, identity.namespace, ctx.cwd), {
1190
+ const launch = branchResourceEvents.managedSessionLaunchState.get(sessionKey);
1191
+ trackOwnedManagedSession(ownedManagedSessions, identity.sessionName, launch?.cwd ?? ctx.cwd, {
1183
1192
  branchOwned: true,
1184
- compatibilityWorkaround: restoreManagedSessionCompatibilityWorkaroundFromBranch(branch, identity.sessionName, identity.namespace),
1185
- headedManagedAutosaveDisabled: restoreManagedSessionHeadedAutosaveDisabledFromBranch(branch, identity.sessionName, identity.namespace),
1186
- headedManagedAutosaveInterval: restoreManagedSessionHeadedAutosaveIntervalFromBranch(branch, identity.sessionName, identity.namespace),
1193
+ compatibilityWorkaround: launch?.compatibilityWorkaround,
1194
+ headedManagedAutosaveDisabled: launch?.headedManagedAutosaveDisabled ?? false,
1195
+ headedManagedAutosaveInterval: launch?.headedManagedAutosaveInterval,
1187
1196
  namespace: identity.namespace,
1197
+ socketDir: launch?.socketDir,
1188
1198
  });
1189
1199
  }
1190
1200
  if (restoredState.active) {
@@ -1194,6 +1204,32 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1194
1204
  headedManagedAutosaveDisabled: managedSessionHeadedAutosaveDisabled,
1195
1205
  headedManagedAutosaveInterval: managedSessionHeadedAutosaveInterval,
1196
1206
  namespace: restoredState.namespace,
1207
+ socketDir: currentLaunch?.socketDir,
1208
+ });
1209
+ }
1210
+ const pendingOwnedBegins = new Map();
1211
+ for (const entry of branch) {
1212
+ const event = getBrowserRecord(entry)?.event;
1213
+ if (!event)
1214
+ continue;
1215
+ if (event.phase === "begin" && event.state.ownerSessionId === browserOwnerSessionId && event.state.wrapperManaged === true)
1216
+ pendingOwnedBegins.set(event.operationId, event);
1217
+ else if (event.phase === "finish")
1218
+ pendingOwnedBegins.delete(event.operationId);
1219
+ for (const page of event.pages ?? [])
1220
+ if (page.clear) {
1221
+ for (const [id, begin] of pendingOwnedBegins)
1222
+ if (begin.pages?.some(candidate => candidate.key === page.key))
1223
+ pendingOwnedBegins.delete(id);
1224
+ }
1225
+ }
1226
+ for (const begin of pendingOwnedBegins.values()) {
1227
+ if (typeof begin.state.sessionName !== "string")
1228
+ continue;
1229
+ // The persisted wrapper-selected intent proves cleanup scope, not launch success.
1230
+ trackOwnedManagedSession(ownedManagedSessions, begin.state.sessionName, typeof begin.state.managedSessionCwd === "string" ? begin.state.managedSessionCwd : ctx.cwd, {
1231
+ branchOwned: true, namespace: typeof begin.state.namespace === "string" ? begin.state.namespace : undefined,
1232
+ socketDir: typeof begin.state.managedSessionSocketDir === "string" ? begin.state.managedSessionSocketDir : undefined,
1197
1233
  });
1198
1234
  }
1199
1235
  for (const record of getActiveElectronRecords(electronLaunchRecords)) {
@@ -1204,16 +1240,29 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1204
1240
  const closeRank = branchResourceEvents.managedSessionCloseRanks.get(sessionKey);
1205
1241
  if (activeRank === undefined || (closeRank !== undefined && closeRank >= activeRank))
1206
1242
  continue;
1207
- trackOwnedManagedSession(ownedManagedSessions, record.sessionName, restoreManagedSessionCwd(branch, record.sessionName, undefined, ctx.cwd), {
1243
+ const launch = branchResourceEvents.managedSessionLaunchState.get(sessionKey);
1244
+ trackOwnedManagedSession(ownedManagedSessions, record.sessionName, launch?.cwd ?? ctx.cwd, {
1208
1245
  branchOwned: true,
1209
- headedManagedAutosaveDisabled: restoreManagedSessionHeadedAutosaveDisabledFromBranch(branch, record.sessionName),
1210
- headedManagedAutosaveInterval: restoreManagedSessionHeadedAutosaveIntervalFromBranch(branch, record.sessionName),
1246
+ headedManagedAutosaveDisabled: launch?.headedManagedAutosaveDisabled ?? false,
1247
+ headedManagedAutosaveInterval: launch?.headedManagedAutosaveInterval,
1248
+ socketDir: launch?.socketDir,
1211
1249
  });
1212
1250
  }
1213
- mergeActiveElectronLaunchRecords(ownedElectronLaunchRecords, electronLaunchRecords, {
1251
+ mergeActiveElectronLaunchRecords(ownedElectronLaunchRecords, new Map([...electronLaunchRecords].filter(([, record]) => record.ownerSessionId === browserOwnerSessionId)), {
1214
1252
  branchOwnedLaunchIds: branchOwnedElectronLaunchIds,
1215
1253
  markBranchOwned: true,
1216
1254
  });
1255
+ for (const entry of branch) {
1256
+ const event = getBrowserRecord(entry)?.event;
1257
+ const state = event?.state;
1258
+ if (event?.phase === "begin" || state?.ownerSessionId !== browserOwnerSessionId || typeof state.sessionName !== "string")
1259
+ continue;
1260
+ const namespace = typeof state.namespace === "string" ? state.namespace : undefined;
1261
+ const key = getAgentBrowserSessionIdentityKey(state.sessionName, namespace);
1262
+ if (ownedManagedSessions.has(key) && !managedSessionRestoreState.hasDaemonRestoreKey(state.sessionName, namespace)) {
1263
+ managedSessionRestoreState.restoreDaemonReceipt(state.sessionName, namespace, state.managedSessionDaemon);
1264
+ }
1265
+ }
1217
1266
  if (!options.resetRuntimeOwnership) {
1218
1267
  for (const sessionKey of previousAttachedSessionKeys) {
1219
1268
  if (ownedManagedSessions.has(sessionKey))
@@ -1225,12 +1274,12 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1225
1274
  attachedSessionKeys.add(sessionKey);
1226
1275
  }
1227
1276
  }
1277
+ return branch;
1228
1278
  };
1229
1279
  const registerWebSearchToolIfAvailable = (configState) => {
1230
1280
  if (webSearchToolRegistered || !canRegisterWebSearchTool(configState))
1231
1281
  return;
1232
1282
  pi.registerTool({
1233
- ...{ discovery: { group: AGENT_BROWSER_DISCOVERY_GROUP, role: "entry" } },
1234
1283
  ...createAgentBrowserWebSearchTool(configState, {
1235
1284
  loadConfigState(ctx) {
1236
1285
  return loadAgentBrowserConfigSync({
@@ -1242,15 +1291,19 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1242
1291
  });
1243
1292
  webSearchToolRegistered = true;
1244
1293
  };
1294
+ pi.on("message_end", (event, ctx) => {
1295
+ if (event.message.role === "user")
1296
+ promptState = { sessionId: ctx.sessionManager.getSessionId(), message: event.message };
1297
+ });
1245
1298
  pi.on("session_start", async (_event, ctx) => {
1246
- restoreBranchBackedState(ctx, { resetRuntimeOwnership: true });
1247
- electronChildProcesses = new Map();
1248
- registerWebSearchToolIfAvailable(loadAgentBrowserConfigSync({
1249
- cwd: ctx.cwd,
1250
- includeProjectConfig: shouldIncludeProjectConfig(ctx),
1251
- }));
1252
1299
  await managedSessionExecutionQueue.run(() => artifactExecutionQueue.run(async () => {
1253
- await recoverScriptSessionLeasesWithinQueue(ctx);
1300
+ const branch = await restoreBranchBackedState(ctx, { resetRuntimeOwnership: true });
1301
+ electronChildProcesses = new Map();
1302
+ registerWebSearchToolIfAvailable(loadAgentBrowserConfigSync({
1303
+ cwd: ctx.cwd,
1304
+ includeProjectConfig: shouldIncludeProjectConfig(ctx),
1305
+ }));
1306
+ await recoverScriptSessionLeasesWithinQueue(ctx, branch);
1254
1307
  flushRecordingReservations();
1255
1308
  notifyRecordingPersistence(ctx);
1256
1309
  }));
@@ -1261,85 +1314,12 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1261
1314
  controller.abort();
1262
1315
  await Promise.allSettled([...activeScriptExecutions]);
1263
1316
  await managedSessionExecutionQueue.run(() => artifactExecutionQueue.run(async () => {
1264
- restoreBranchBackedState(ctx, { resetRuntimeOwnership: false });
1265
- await recoverScriptSessionLeasesWithinQueue(ctx);
1317
+ const branch = await restoreBranchBackedState(ctx, { resetRuntimeOwnership: false });
1318
+ await recoverScriptSessionLeasesWithinQueue(ctx, branch);
1266
1319
  flushRecordingReservations();
1267
1320
  notifyRecordingPersistence(ctx);
1268
1321
  }));
1269
1322
  });
1270
- // Fork-only checkpoint event; official Pi validation types do not declare it.
1271
- // Pi owns awaited tools/events (including their execution queues). Detached code
1272
- // children and retained browser/recording resources still need extension-side checks.
1273
- const checkpointPi = pi;
1274
- checkpointPi.on("session_checkpoint", async (event, ctx) => {
1275
- const blocked = (reason) => ({ sleepReady: false, reason });
1276
- if (activeScriptControllers.size || activeScriptExecutions.size)
1277
- return blocked("Browser code execution is pending");
1278
- flushRecordingReservations();
1279
- if (recordingReservationsDirty)
1280
- return blocked("Browser recording journal persistence is dirty");
1281
- if (activeRecordingReservations.size)
1282
- return blocked("Browser recording is pending; finish it explicitly before sleep");
1283
- const entries = ctx.sessionManager.getEntries();
1284
- if ([...getScriptSessionLeasesFromBranch(entries).values()].some(lease => lease.cleanup !== "closed")
1285
- || [...ownedManagedSessions.values()].some(owner => isAgentBrowserScriptSessionName(owner.sessionName))) {
1286
- return blocked("Browser script cleanup lease is unresolved");
1287
- }
1288
- if (getActiveElectronRecords(ownedElectronLaunchRecords).length || getActiveElectronRecords(electronLaunchRecords).length
1289
- || [...electronChildProcesses.values()].some(child => child.exitCode === null && child.signalCode === null)) {
1290
- return blocked("Electron launch is still active");
1291
- }
1292
- if (attachedSessionKeys.size || restoreAttachedSessionKeysFromBranch(entries).size)
1293
- return blocked("Attached browser state is caller-owned and not checkpointed");
1294
- if (traceOwners.size || [...networkRoutesBySession.values()].some(routes => routes.length))
1295
- return blocked("Browser trace or network routes are still active");
1296
- // Include off-branch and caller-owned/root identities, without acquiring
1297
- // cleanup ownership. Transcript page/ref details do not serialize a browser.
1298
- const historicalResources = collectBranchManagedResourceEvents(entries);
1299
- const sessions = new Map(ownedManagedSessions);
1300
- if (managedSessionActive)
1301
- trackOwnedManagedSession(sessions, managedSessionName, managedSessionCwd, { namespace: managedSessionNamespace });
1302
- for (const entry of entries) {
1303
- const message = getBrowserResultMessage(entry);
1304
- if (!message)
1305
- continue;
1306
- const details = isRecord(message.details) ? message.details : undefined;
1307
- // Helpers can launch before a main-process failure. Conversely, native
1308
- // launch flags on sessionless reads can open the unnamed default browser.
1309
- const sessionName = typeof details?.sessionName === "string" ? details.sessionName
1310
- : details?.agentBrowserStarted === true ? "default" : undefined;
1311
- if (!sessionName)
1312
- continue;
1313
- const namespace = typeof details?.namespace === "string" ? details.namespace : undefined;
1314
- const key = getSessionContextKey(sessionName, namespace) ?? sessionName;
1315
- if (!sessions.has(key))
1316
- trackOwnedManagedSession(sessions, sessionName, ctx.cwd, { namespace });
1317
- }
1318
- for (const [key, owner] of sessions) {
1319
- event.signal.throwIfAborted();
1320
- // An abnormal restart can leave a wrapper-created browser only off-branch.
1321
- // Reuse native ownership events for inspection, never cleanup ownership.
1322
- // A later close can refer to the same identity in a different socket root.
1323
- const historicalIdentity = historicalResources.managedSessionActiveIdentities.get(key);
1324
- const historicallyOwned = historicalIdentity && isRestorableManagedSessionName(historicalIdentity.sessionName, managedSessionBaseName);
1325
- const context = resolveOwnedManagedSessionContext({
1326
- ...owner,
1327
- currentManagedSessionName: managedSessionActive ? managedSessionName : undefined,
1328
- currentManagedSessionNamespace: managedSessionNamespace,
1329
- recordedOwnedSession: ownedManagedSessions.get(key) ?? (historicallyOwned ? { ...historicalIdentity, cwd: ctx.cwd } : undefined),
1330
- restoreState: managedSessionRestoreState,
1331
- });
1332
- // Owned routing must not mask an explicit caller's same-name ambient daemon.
1333
- for (const inspectionContext of context ? [{ ...context, reuseOnly: true }, undefined] : [undefined]) {
1334
- const daemon = await withOwnedManagedSessionContext(inspectionContext, () => inspectManagedSessionDaemon({ ...owner, signal: event.signal, timeoutMs: 2_000 }));
1335
- if (daemon.status !== "inactive")
1336
- return blocked("Browser daemon is live or unverified; finish browser work explicitly before sleep");
1337
- }
1338
- }
1339
- // No detached writer remains to pause/resume. Native ingress stays held;
1340
- // checkpoint never closes a browser or changes ordinary shutdown ownership.
1341
- return { sleepReady: true };
1342
- });
1343
1323
  pi.on("session_shutdown", async (event, ctx) => {
1344
1324
  for (const controller of activeScriptControllers)
1345
1325
  controller.abort();
@@ -1351,7 +1331,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1351
1331
  const shutdownCwd = ctx?.cwd ?? managedSessionCwd;
1352
1332
  const quitting = event?.reason === "quit";
1353
1333
  preservedElectronProfileDirs = quitting
1354
- ? []
1334
+ ? getActiveElectronRecords(electronLaunchRecords).filter(record => !ownedElectronLaunchRecords.has(record.launchId)).map(record => record.userDataDir)
1355
1335
  : getActiveElectronRecords(electronLaunchRecords).map((record) => record.userDataDir);
1356
1336
  const electronRecordsToCleanup = quitting
1357
1337
  ? ownedElectronLaunchRecords
@@ -1402,11 +1382,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1402
1382
  ownedManagedSessions.clear();
1403
1383
  await cleanupSecureTempArtifacts({ preservePaths: preservedElectronProfileDirs });
1404
1384
  });
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
- }
1385
+ const fullInstructions = (ctx) => {
1410
1386
  const runtimeConfig = loadAgentBrowserConfigSync({
1411
1387
  cwd: ctx.cwd,
1412
1388
  includeProjectConfig: shouldIncludeProjectConfig(ctx),
@@ -1422,14 +1398,42 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1422
1398
  const runtimeConfigPrompt = browserGuidance.length > 0
1423
1399
  ? `\n\nProject agent_browser config guidance:\n${browserGuidance.map((line) => `- ${line}`).join("\n")}`
1424
1400
  : "";
1425
- event.systemPromptOptions.sections.agent_browser = `${PROJECT_RULE_PROMPT}${runtimeConfigPrompt}`;
1401
+ const guidelines = [
1402
+ ...buildToolPromptGuidelines({
1403
+ browserDefaultProfile: runtimeConfig.trustedBrowserDefaultProfile,
1404
+ browserExecutablePath: runtimeConfig.trustedBrowserExecutablePath,
1405
+ includeWebSearch: webSearchToolRegistered,
1406
+ docs: getInstalledDocsPaths(),
1407
+ }),
1408
+ ...QUICK_START_GUIDELINES,
1409
+ ...SHARED_BROWSER_PLAYBOOK_GUIDELINES,
1410
+ ...WRAPPER_TAB_RECOVERY_BEHAVIOR,
1411
+ ...Object.values(ADVANCED_TOOL_PROMPT_GUIDELINES).flat(),
1412
+ ...(webSearchToolRegistered ? WEB_SEARCH_TOOL_PROMPT_GUIDELINES : []),
1413
+ ];
1414
+ return `${PROJECT_RULE_PROMPT}\n\n${[...new Set(guidelines)].map(line => `- ${line}`).join("\n")}${runtimeConfigPrompt}`;
1415
+ };
1416
+ let isInstructionsManaged = () => false;
1417
+ pi.events.on("pi:instruction-groups", (data) => {
1418
+ const collector = data;
1419
+ collector.register({
1420
+ ...AGENT_BROWSER_INSTRUCTION_GROUP,
1421
+ tools: ["agent_browser", "agent_browser_code", "agent_browser_tools", "agent_browser_web_search",
1422
+ ...Object.values(AGENT_BROWSER_TOOL_INVENTORY).map(({ name }) => name)],
1423
+ instructions: fullInstructions,
1424
+ });
1425
+ isInstructionsManaged = collector.isManaged;
1426
+ });
1427
+ pi.on("before_agent_start", async (event, ctx) => {
1428
+ if (!isInstructionsManaged())
1429
+ event.systemPromptOptions.sections.agent_browser = fullInstructions(ctx);
1426
1430
  });
1427
1431
  pi.on("tool_call", async (event, ctx) => {
1428
- const promptPolicy = buildPromptPolicy(getLatestUserPrompt(ctx.sessionManager.getBranch()));
1429
- if (isBashToolCallEvent(event) &&
1430
- !promptPolicy.allowLegacyAgentBrowserBash &&
1431
- looksLikeDirectAgentBrowserBash(event.input.command) &&
1432
- !isHarmlessAgentBrowserInspectionCommand(event.input.command) &&
1432
+ if (!isBashToolCallEvent(event) || !looksLikeDirectAgentBrowserBash(event.input.command)
1433
+ || isHarmlessAgentBrowserInspectionCommand(event.input.command))
1434
+ return;
1435
+ const promptPolicy = getPromptPolicy(ctx);
1436
+ if (!promptPolicy.allowLegacyAgentBrowserBash &&
1433
1437
  !(await isDirectAgentBrowserBashAllowed(ctx.cwd))) {
1434
1438
  return {
1435
1439
  block: true,
@@ -1437,7 +1441,18 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1437
1441
  };
1438
1442
  }
1439
1443
  });
1440
- pi.on("tool_result", async (event) => buildAgentBrowserToolResultPatch(event));
1444
+ let emptyBrowserBranch;
1445
+ const captureCurrentBrowserBranch = (ctx) => {
1446
+ const generation = branchRestoreGeneration;
1447
+ const empty = ctx.sessionManager.getLeafId() === null;
1448
+ // Concurrent empty-root callers share only their own first synchronous append.
1449
+ if (empty && emptyBrowserBranch?.anchorId === null
1450
+ && emptyBrowserBranch.sessionId === ctx.sessionManager.getSessionId() && emptyBrowserBranch.isCurrent())
1451
+ return emptyBrowserBranch;
1452
+ const branch = captureBrowserBranch(ctx.sessionManager, () => generation === branchRestoreGeneration);
1453
+ emptyBrowserBranch = empty ? branch : undefined;
1454
+ return branch;
1455
+ };
1441
1456
  const agentBrowserTool = {
1442
1457
  renderCall(args, theme, context) {
1443
1458
  const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
@@ -1448,10 +1463,11 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1448
1463
  const component = context.lastComponent instanceof AgentBrowserResultComponent
1449
1464
  ? context.lastComponent
1450
1465
  : new AgentBrowserResultComponent();
1451
- component.setState(formatAgentBrowserRenderResult(result, options, theme, context.isError), options.expanded, theme);
1466
+ component.setResult(result, options, theme, context.isError);
1452
1467
  return component;
1453
1468
  },
1454
- async execute(toolCallId, params, signal, onUpdate, ctx, nativeToolCallId = toolCallId, capturedCwd, modelVisible = true) {
1469
+ async execute(toolCallId, params, signal, onUpdate, ctx, nativeToolCallId = toolCallId, capturedCwd, modelVisible = true, commandIndex, selectedBranch) {
1470
+ const branch = selectedBranch ?? captureCurrentBrowserBranch(ctx);
1455
1471
  let operationCwd;
1456
1472
  try {
1457
1473
  operationCwd = capturedCwd ?? resolveExecutionCwd(pi, ctx);
@@ -1459,7 +1475,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1459
1475
  catch (error) {
1460
1476
  return buildValidationFailureResult({ kind: "invalid", status: "invalid", redactedArgs: [], toolArgs: [], validationError: error instanceof Error ? error.message : String(error) });
1461
1477
  }
1462
- const promptPolicy = buildPromptPolicy(getLatestUserPrompt(ctx.sessionManager.getBranch()));
1478
+ const promptPolicy = getPromptPolicy(ctx);
1463
1479
  const outputPath = isRecord(params) && typeof params.outputPath === "string" ? params.outputPath : undefined;
1464
1480
  const admittedInput = resolveAgentBrowserInput({
1465
1481
  getBatchPreflightValidationError: (args, stdin) => getArtifactPreflightValidationError({ args, cwd: operationCwd, outputPath, stdin }),
@@ -1468,6 +1484,8 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1468
1484
  if (admittedInput.status === "invalid") {
1469
1485
  return buildValidationFailureResult(admittedInput);
1470
1486
  }
1487
+ if (browserReplayError && !isPlainTextInspectionArgs(admittedInput.toolArgs))
1488
+ return browserExecutionFailure(new Error(browserReplayError), signal, "validation-error");
1471
1489
  const explicitConfig = scanUpstreamGlobalFlagOccurrences(admittedInput.toolArgs, "--config").length > 0 || getAgentBrowserProcessEnvironment().AGENT_BROWSER_CONFIG !== undefined;
1472
1490
  // Managed selection must follow earlier queued fresh launches, while file paths keep their admission snapshot.
1473
1491
  const managedAtAdmission = admittedInput.kind !== "electron"
@@ -1533,7 +1551,31 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1533
1551
  branchRecords: electronLaunchRecords,
1534
1552
  compiledElectron,
1535
1553
  ownedRecords: ownedElectronLaunchRecords,
1554
+ ownerSessionId: ctx.sessionManager.getSessionId(),
1536
1555
  });
1556
+ const hostAffecting = compiledElectron && ["cleanup", "probe"].includes(compiledElectron.action);
1557
+ const workingPageState = sessionPageState.fork();
1558
+ const priorPages = workingPageState.views();
1559
+ const priorManifest = artifactManifest;
1560
+ const operationId = randomUUID();
1561
+ const launchId = compiledElectron && "launchId" in compiledElectron ? compiledElectron.launchId : undefined;
1562
+ const selected = launchId ? [electronHostLaunchRecords.get(launchId)].filter((record) => record !== undefined) : getActiveElectronRecords(electronHostLaunchRecords);
1563
+ const keys = new Set(selected.flatMap(record => record.sessionName ? [getAgentBrowserSessionIdentityKey(record.sessionName, record.namespace)] : []));
1564
+ if (compiledElectron?.action === "probe" && !launchId && managedSessionActive)
1565
+ keys.add(getAgentBrowserSessionIdentityKey(managedSessionName, managedSessionNamespace));
1566
+ const begin = { event: { version: 1, phase: "begin", operationId, toolCallId, commandIndex: commandIndex ?? 0, isError: true,
1567
+ state: {}, pages: [...keys].map(key => ({ key, refs: { kind: "unknown" }, unknown: true })) } };
1568
+ if (hostAffecting) {
1569
+ try {
1570
+ if (!await appendBrowserRecord(ctx.sessionManager, () => appendBrowserTransition(pi, begin), begin, branch)
1571
+ || !branch.isCurrent())
1572
+ throw new Error("The selected Pi branch changed before host-operation admission.");
1573
+ }
1574
+ catch (error) {
1575
+ return browserExecutionFailure(new Error("Could not persist Electron/browser-state begin; the host operation was not run.", { cause: error }), executionSignal);
1576
+ }
1577
+ sessionPageState.applyBrowserRecord(begin);
1578
+ }
1537
1579
  let electronHostResult = await handleElectronHostInput({
1538
1580
  attachedSessionKeys,
1539
1581
  compiledElectron,
@@ -1547,7 +1589,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1547
1589
  managedSessionRestoreState,
1548
1590
  ownedManagedSessions,
1549
1591
  redactedCompiledElectron,
1550
- sessionPageState,
1592
+ sessionPageState: workingPageState,
1551
1593
  signal: executionSignal,
1552
1594
  });
1553
1595
  if (electronHostResult && compiledElectron?.action === "cleanup") {
@@ -1575,6 +1617,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1575
1617
  retireRecordingSession(identity.sessionName, identity.namespace);
1576
1618
  const closedSessionKey = getSessionContextKey(identity.sessionName, identity.namespace) ?? identity.sessionName;
1577
1619
  clearSessionScopedBrowserState(closedSessionKey);
1620
+ workingPageState.clearSession(closedSessionKey);
1578
1621
  if (closedSessionKey === (getSessionContextKey(managedSessionName, managedSessionNamespace) ?? managedSessionName)) {
1579
1622
  managedSessionActive = false;
1580
1623
  managedSessionCompatibilityWorkaround = undefined;
@@ -1585,15 +1628,37 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1585
1628
  managedSessionName = createFreshSessionName(managedSessionBaseName, ephemeralSessionSeed, freshSessionOrdinal);
1586
1629
  }
1587
1630
  }
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
- };
1631
+ if (artifactManifest && isRecord(electronHostResult.details))
1632
+ electronHostResult.details.artifactRetentionSummary = formatSessionArtifactRetentionSummary(artifactManifest);
1633
+ }
1634
+ if (electronHostResult && hostAffecting && branch.isCurrent()) {
1635
+ for (const key of keys)
1636
+ if (workingPageState.get(key).refSnapshot && workingPageState.get(key).refSnapshot?.snapshotId !== priorPages.get(key)?.refSnapshot?.snapshotId) {
1637
+ const record = selected.find(record => record.sessionName && getAgentBrowserSessionIdentityKey(record.sessionName, record.namespace) === key);
1638
+ const owner = ownedManagedSessions.get(key);
1639
+ 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) });
1640
+ workingPageState.bindSnapshotGeneration(key, daemon.status === "active" ? daemon.generation : undefined);
1641
+ }
1642
+ const changes = browserPageChanges(workingPageState, priorPages, keys);
1643
+ const finish = { event: { version: 1, phase: hostAffecting ? "finish" : "state", operationId, toolCallId, commandIndex: commandIndex ?? 0, isError: electronHostResult.isError === true,
1644
+ state: browserStateEffects(isRecord(electronHostResult.details) ? electronHostResult.details : {}), pages: changes.pages, artifacts: artifactChanges(priorManifest, artifactManifest) },
1645
+ ...(changes.snapshot ? { snapshot: changes.snapshot } : {}) };
1646
+ try {
1647
+ if (!await appendBrowserRecord(ctx.sessionManager, () => appendBrowserTransition(pi, finish), finish, branch)
1648
+ || !branch.isCurrent())
1649
+ throw new Error("The selected Pi branch changed before host-state confirmation.");
1650
+ }
1651
+ catch (error) {
1652
+ if (branch.isCurrent())
1653
+ sessionPageState.applyBrowserRecord(begin);
1654
+ 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),
1655
+ details: { ...(isRecord(electronHostResult.details) ? electronHostResult.details : {}), browserStatePersistence: "finish-unconfirmed", resultCategory: "failure", failureCategory: "upstream-error", nextActions: undefined } };
1656
+ }
1657
+ sessionPageState.applyBrowserRecord(finish);
1658
+ artifactManifest = applyArtifactChanges(artifactManifest, finish.event.artifacts);
1659
+ if (isRecord(electronHostResult.details)) {
1660
+ electronHostResult.details.browserEventVersion = 1;
1661
+ electronHostResult.details.artifactManifest = invocationArtifactManifest(artifactManifest, priorManifest, electronHostResult.details);
1597
1662
  }
1598
1663
  }
1599
1664
  if (electronHostResult && executionSignal?.aborted) {
@@ -1614,7 +1679,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1614
1679
  }) : runElectronHostInput(executionSignal);
1615
1680
  if (!shouldSerializeElectronHostInput(compiledElectron))
1616
1681
  return execute();
1617
- const records = getElectronHostLaunchRecordsForInput({ branchRecords: electronLaunchRecords, compiledElectron, ownedRecords: ownedElectronLaunchRecords });
1682
+ const records = getElectronHostLaunchRecordsForInput({ branchRecords: electronLaunchRecords, compiledElectron, ownedRecords: ownedElectronLaunchRecords, ownerSessionId: ctx.sessionManager.getSessionId() });
1618
1683
  const launchId = compiledElectron && "launchId" in compiledElectron ? compiledElectron.launchId : undefined;
1619
1684
  const selected = launchId ? [records.get(launchId)].filter((record) => record !== undefined) : getActiveElectronRecords(records);
1620
1685
  const sessions = selected.flatMap(record => record.sessionName ? [{ sessionName: record.sessionName, namespace: record.namespace }] : []);
@@ -1655,9 +1720,10 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1655
1720
  // before capturing state. Registration, rendering, and restore stay synchronous/eager.
1656
1721
  const { runAgentBrowserTool } = await import("./lib/orchestration/browser-run/index.js");
1657
1722
  flushRecordingReservations();
1658
- const branchRestoreGenerationAtStart = branchRestoreGeneration;
1659
1723
  const generationAtStart = branchStateGeneration;
1660
- const sessionPageStateUpdate = sessionPageState.beginUpdate();
1724
+ const workingPageState = sessionPageState.fork();
1725
+ const priorPages = workingPageState.views();
1726
+ const sessionPageStateUpdate = workingPageState.beginUpdate();
1661
1727
  const browserRunState = {
1662
1728
  activeRecordingReservations,
1663
1729
  artifactManifest,
@@ -1678,7 +1744,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1678
1744
  managedSessionRestoreState,
1679
1745
  networkRoutesBySession,
1680
1746
  ownedManagedSessions,
1681
- sessionPageState,
1747
+ sessionPageState: workingPageState,
1682
1748
  traceOwners,
1683
1749
  };
1684
1750
  const selectedPlan = buildExecutionPlan(toolArgs, {
@@ -1691,6 +1757,30 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1691
1757
  stdin: resolvedInput.toolStdin,
1692
1758
  browserIndependentReadConfirmation: readConfirmation?.capabilities?.readRequiresConfirmation === true,
1693
1759
  });
1760
+ const browserAffecting = !selectedPlan.plainTextInspection && !(selectedPlan.commandInfo.command === "session" && selectedPlan.commandInfo.subcommand === "info")
1761
+ && (needsManagedSession(parseArgvDescriptor(toolArgs), resolvedInput.toolStdin) || (selectedPlan.startupScopedFlags?.length ?? 0) > 0);
1762
+ const operationId = randomUUID();
1763
+ const selectedKey = getAgentBrowserSessionIdentityKey(selectedPlan.sessionName ?? "default", selectedPlan.namespace);
1764
+ const begin = { event: {
1765
+ version: 1, phase: "begin", operationId, toolCallId, commandIndex: commandIndex ?? 0, isError: true,
1766
+ state: { args: redactInvocationArgs(toolArgs), sessionName: selectedPlan.sessionName, namespace: selectedPlan.namespace,
1767
+ ownerSessionId: ctx.sessionManager.getSessionId(), usedImplicitSession: selectedPlan.usedImplicitSession,
1768
+ wrapperManaged: selectedPlan.managedSessionName !== undefined || ownedManagedSessions.has(selectedKey), managedSessionCwd: browserCwd,
1769
+ managedSessionSocketDir: resolveAgentBrowserSocketDir({ ownedManagedSession: selectedPlan.managedSessionName !== undefined || ownedManagedSessions.has(selectedKey) }) },
1770
+ pages: [{ key: selectedKey, refs: { kind: "unknown" }, unknown: true }],
1771
+ } };
1772
+ if (browserAffecting) {
1773
+ try {
1774
+ if (!await appendBrowserRecord(ctx.sessionManager, () => appendBrowserTransition(pi, begin), begin, branch)
1775
+ || !branch.isCurrent())
1776
+ throw new Error("The selected Pi branch changed before browser-command admission.");
1777
+ }
1778
+ catch (error) {
1779
+ const failure = browserExecutionFailure(new Error("Could not persist browser-state begin; the browser command was not run.", { cause: error }), executionSignal);
1780
+ return { ...failure, details: { ...(isRecord(failure.details) ? failure.details : {}), browserStatePersistence: "begin-unconfirmed" } };
1781
+ }
1782
+ sessionPageState.applyBrowserRecord(begin);
1783
+ }
1694
1784
  const initialArtifactManifest = browserRunState.artifactManifest;
1695
1785
  const initialNetworkRoutesBySession = browserRunState.networkRoutesBySession;
1696
1786
  const attachedSessionRequested = isAttachedBrowserInvocation(toolArgs)
@@ -1702,28 +1792,37 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1702
1792
  : getSessionContextKey(selectedPlan.sessionName, selectedPlan.namespace)
1703
1793
  ?? getSessionContextKey(browserRunState.managedSessionName, browserRunState.managedSessionNamespace);
1704
1794
  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;
1795
+ let result;
1796
+ try {
1797
+ result = await runAgentBrowserTool({
1798
+ daemonInactive,
1799
+ ctx,
1800
+ cwd: browserCwd,
1801
+ operationCwd,
1802
+ electronPostCommandStatusSettleMs: ELECTRON_POST_COMMAND_STATUS_SETTLE_MS,
1803
+ electronProfileIsolationDetails: ELECTRON_PROFILE_ISOLATION_DETAILS,
1804
+ implicitSessionCloseTimeoutMs,
1805
+ implicitSessionIdleTimeoutMs,
1806
+ input: resolvedInput,
1807
+ modelVisible,
1808
+ onUpdate,
1809
+ params,
1810
+ establishAttachedBrowserSession: attachedSessionRequested && !attachedSessionKnown,
1811
+ preserveAttachedBrowserSession: attachedSessionRequested || attachedSessionKnown,
1812
+ promptPolicy,
1813
+ sessionPageStateUpdate,
1814
+ signal: executionSignal,
1815
+ state: browserRunState,
1816
+ });
1817
+ }
1818
+ catch (error) {
1819
+ const failure = browserExecutionFailure(error, executionSignal);
1820
+ result = { ...failure, details: { ...browserRunState.observedBrowserEffects, ...(isRecord(failure.details) ? failure.details : {}) } };
1821
+ }
1822
+ const branchRestoreStillCurrent = branch.isCurrent();
1726
1823
  const resultDetails = isRecord(result.details) ? result.details : undefined;
1824
+ if (resultDetails && begin.event.state.wrapperManaged === true)
1825
+ resultDetails.managedSessionSocketDir = begin.event.state.managedSessionSocketDir;
1727
1826
  if (typeof resultDetails?.sessionName === "string" && browserRunState.managedSessionActive
1728
1827
  && getAgentBrowserSessionIdentityKey(resultDetails.sessionName, typeof resultDetails.namespace === "string" ? resultDetails.namespace : undefined) === getAgentBrowserSessionIdentityKey(browserRunState.managedSessionName, browserRunState.managedSessionNamespace)) {
1729
1828
  resultDetails.managedSessionCwd = browserRunState.managedSessionCwd;
@@ -1780,18 +1879,10 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1780
1879
  if (reservation)
1781
1880
  result = appendActiveRecordingCleanupAction(result, reservation);
1782
1881
  }
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
- }
1882
+ if (artifactManifest && isRecord(result.details))
1883
+ result.details.artifactRetentionSummary = formatSessionArtifactRetentionSummary(artifactManifest);
1793
1884
  }
1794
- const branchStateStillCurrent = generationAtStart === branchStateGeneration;
1885
+ const branchStateStillCurrent = generationAtStart === branchStateGeneration && branch.isCurrent();
1795
1886
  if (serializeBrowserCommand || branchStateStillCurrent) {
1796
1887
  freshSessionOrdinal = Math.max(freshSessionOrdinal, browserRunState.freshSessionOrdinal);
1797
1888
  managedSessionActive = browserRunState.managedSessionActive;
@@ -1806,6 +1897,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1806
1897
  }
1807
1898
  syncOwnedManagedSessionsFromResult(ownedManagedSessions, result, browserRunState.managedSessionCwd);
1808
1899
  mergeActiveElectronLaunchRecords(ownedElectronLaunchRecords, electronLaunchRecords, {
1900
+ ownerSessionId: ctx.sessionManager.getSessionId(),
1809
1901
  branchOwnedLaunchIds: branchOwnedElectronLaunchIds,
1810
1902
  touchedLaunchIds: !result.isError
1811
1903
  ? getTouchedElectronLaunchIds(explicitSessionName ?? browserRunState.managedSessionName, electronLaunchRecords, resultNamespace)
@@ -1814,6 +1906,39 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1814
1906
  if (serializeBrowserCommand)
1815
1907
  branchStateGeneration += 1;
1816
1908
  }
1909
+ if (!selectedPlan.plainTextInspection && branchRestoreStillCurrent) {
1910
+ const changes = browserPageChanges(workingPageState, priorPages, [...browserRunState.closedManagedSessionNames, ...(browserAffecting ? [selectedKey] : [])]);
1911
+ const artifacts = artifactChanges(initialArtifactManifest, artifactManifest);
1912
+ if (browserAffecting || changes.pages.length > 0 || artifacts && hasPublishedBrowserJournal(ctx.sessionManager) || resultDetails?.readConfirmation !== undefined || browserRunState.observedBrowserEffects?.readConfirmation !== undefined) {
1913
+ const finish = { event: {
1914
+ version: 1, phase: browserAffecting ? "finish" : "state", operationId, toolCallId, commandIndex: commandIndex ?? 0, isError: result.isError === true,
1915
+ state: { ...browserStateEffects(isRecord(result.details) ? result.details : {}), ...browserRunState.observedBrowserEffects,
1916
+ ownerSessionId: ctx.sessionManager.getSessionId(),
1917
+ ...(begin.event.state.wrapperManaged === true ? { managedSessionDaemon: managedSessionRestoreState.getDaemonReceipt(resultSessionName, resultNamespace) } : {}) }, pages: changes.pages,
1918
+ artifacts,
1919
+ }, ...(changes.snapshot ? { snapshot: changes.snapshot } : {}) };
1920
+ try {
1921
+ if (!await appendBrowserRecord(ctx.sessionManager, () => appendBrowserTransition(pi, finish), finish, branch)
1922
+ || !branch.isCurrent())
1923
+ throw new Error("The selected Pi branch changed before browser-state confirmation.");
1924
+ }
1925
+ catch (error) {
1926
+ 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: [{
1927
+ 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"]]) },
1928
+ 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.",
1929
+ }] } };
1930
+ }
1931
+ sessionPageState.applyBrowserRecord(finish);
1932
+ artifactManifest = applyArtifactChanges(artifactManifest, finish.event.artifacts);
1933
+ }
1934
+ }
1935
+ if (isRecord(result.details)) {
1936
+ result.details.browserEventVersion = 1;
1937
+ if (!(compiledElectron?.action === "launch" && compiledElectron.handoff === "snapshot")
1938
+ && !getArtifactCommandSteps(toolArgs, resolvedInput.toolStdin).steps.some(step => step[0] === "snapshot"))
1939
+ delete result.details.refSnapshot;
1940
+ result.details.artifactManifest = invocationArtifactManifest(branch.isCurrent() ? artifactManifest : browserRunState.artifactManifest, initialArtifactManifest, result.details);
1941
+ }
1817
1942
  return applyAgentBrowserOutputPath({ cwd: operationCwd, outputPath, preserveTextContent: Array.isArray(params.args) && params.args.includes("--json"), result: warnRecordingPersistence(result) });
1818
1943
  };
1819
1944
  const closesAllSessions = commandClosesAllSessions(toolArgs, resolvedInput.toolStdin);
@@ -1825,35 +1950,39 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1825
1950
  sessionMode: compiledElectron?.action === "launch" ? "fresh" : params.sessionMode ?? "auto",
1826
1951
  stdin: resolvedInput.toolStdin,
1827
1952
  });
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)
1953
+ const owner = ownedManagedSessions.get(getAgentBrowserSessionIdentityKey(plan.sessionName ?? "default", plan.namespace));
1954
+ return withAgentBrowserProcessEnvironment(owner?.socketDir ? { PI_AGENT_BROWSER_SOCKET_DIR: owner.socketDir } : {}, async () => {
1955
+ const run = (executionSignal = signal) => {
1956
+ const execute = () => withLaunchDefaults
1957
+ ? withLaunchDefaults(inactive => runBrowserCommand(inactive, executionSignal), executionSignal)
1958
+ : runBrowserCommand(undefined, executionSignal);
1959
+ if (!commandTouchesArtifactLifecycle(toolArgs, resolvedInput.toolStdin, outputPath))
1837
1960
  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 }));
1961
+ return artifactExecutionQueue.run(async () => {
1962
+ const error = getArtifactPreflightValidationError({ activeRecordingReservations: activeRecordingReservations.values(), args: toolArgs, cwd: operationCwd, outputPath, stdin: resolvedInput.toolStdin });
1963
+ if (!error)
1964
+ return execute();
1965
+ flushRecordingReservations();
1966
+ return warnRecordingPersistence(buildValidationFailureResult({ ...resolvedInput, attemptedKind: resolvedInput.kind, kind: "invalid", status: "invalid", validationError: error }));
1967
+ });
1968
+ };
1969
+ if (plan.validationError || (!plan.sessionName && plan.commandInfo.command !== "read" && !closesAllSessions))
1970
+ return run();
1971
+ try {
1972
+ const owned = plan.managedSessionName !== undefined || ownedManagedSessions.has(getAgentBrowserSessionIdentityKey(plan.sessionName ?? "default", plan.namespace));
1973
+ const identities = [await resolveBrowserExecutionIdentity({ namespace: plan.namespace, ownedManagedSession: owned, sessionName: closesAllSessions ? undefined : plan.sessionName ?? "default" })];
1974
+ if (managedSessionActive && (params.sessionMode === "fresh" || compiledElectron?.action === "launch") && !explicitSessionName) {
1975
+ const previousOwner = ownedManagedSessions.get(getAgentBrowserSessionIdentityKey(managedSessionName, managedSessionNamespace));
1976
+ identities.push(await resolveBrowserExecutionIdentity({ namespace: managedSessionNamespace, ownedManagedSession: true, sessionName: managedSessionName, env: previousOwner?.socketDir ? { AGENT_BROWSER_SOCKET_DIR: previousOwner.socketDir } : undefined }));
1977
+ }
1978
+ const activePageUrl = sessionPageState.get(getAgentBrowserSessionIdentityKey(plan.sessionName ?? "default", plan.namespace)).tabTarget?.url;
1979
+ const deadline = Date.now() + (params.timeoutMs ?? getCommandAwareProcessTimeoutMs(extractUpstreamCommandTokens(toolArgs), resolvedInput.toolStdin, activePageUrl) ?? getAgentBrowserProcessTimeoutMs());
1980
+ return await withBrowserExecutionLocks({ identities, signal, deadline, waitOnly: true }, run);
1849
1981
  }
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
- }
1982
+ catch (error) {
1983
+ return browserExecutionFailure(error, signal);
1984
+ }
1985
+ });
1857
1986
  };
1858
1987
  const runWithinSessionQueue = () => {
1859
1988
  if (closesAllSessions)
@@ -1887,9 +2016,15 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1887
2016
  return managedSessionExecutionQueue.run(executeAdmitted, signal);
1888
2017
  },
1889
2018
  };
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");
2019
+ const executeCode = async (toolCallId, params, signal, _onUpdate, ctx, branch) => {
2020
+ if (browserReplayError)
2021
+ return browserExecutionFailure(new Error(browserReplayError), signal, "validation-error");
2022
+ try {
2023
+ requirePublishedBrowserJournal(ctx.sessionManager);
2024
+ }
2025
+ catch (error) {
2026
+ return browserExecutionFailure(error, signal, "validation-error");
2027
+ }
1893
2028
  let operationCwd;
1894
2029
  try {
1895
2030
  operationCwd = resolveExecutionCwd(pi, ctx);
@@ -1903,12 +2038,8 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1903
2038
  if (signal?.aborted)
1904
2039
  abort();
1905
2040
  activeScriptControllers.add(controller);
1906
- let finish;
1907
- const execution = new Promise(resolve => { finish = resolve; });
1908
- activeScriptExecutions.add(execution);
1909
2041
  const deadline = Date.now() + (params.timeoutMs ?? AGENT_BROWSER_SCRIPT_DEFAULT_TIMEOUT_MS);
1910
2042
  const deadlineTimer = setTimeout(() => controller.abort(new DOMException("Browser code deadline exceeded.", "TimeoutError")), Math.max(0, deadline - Date.now()));
1911
- const generation = branchRestoreGeneration;
1912
2043
  const output = createBrowserCodeOutput();
1913
2044
  const nativeEnvironment = getAgentBrowserProcessEnvironment();
1914
2045
  try {
@@ -1937,9 +2068,10 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1937
2068
  const namespace = plan.namespace || undefined;
1938
2069
  const key = getAgentBrowserSessionIdentityKey(sessionName, namespace);
1939
2070
  const owned = plan.managedSessionName !== undefined || ownedManagedSessions.has(key);
1940
- const run = async () => {
2071
+ const run = async () => withAgentBrowserProcessEnvironment(ownedManagedSessions.get(key)?.socketDir ? { PI_AGENT_BROWSER_SOCKET_DIR: ownedManagedSessions.get(key).socketDir } : {}, async () => {
1941
2072
  const identity = await resolveBrowserExecutionIdentity({ sessionName, namespace, ownedManagedSession: owned });
1942
2073
  return withBrowserExecutionLock({ identity, deadline, signal: controller.signal }, async (executionSignal) => {
2074
+ let commandIndex = 0;
1943
2075
  let journalFailed = false;
1944
2076
  const run = await runAgentBrowserScript({
1945
2077
  code: params.code, signal: executionSignal, timeoutMs: Math.max(1, deadline - Date.now()),
@@ -1950,33 +2082,24 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1950
2082
  const { args } = bindBrowserCodeCall(inner, { sessionName, namespace });
1951
2083
  if (plan.usedImplicitSession && !managedSessionActive && managedSessionName === sessionName && extractExplicitSessionName(inner.args) === undefined)
1952
2084
  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 {
2085
+ // The ordinary executor journals direct, code and hidden-helper effects alike.
2086
+ 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));
2087
+ if (isRecord(result.details) && result.details.browserStatePersistence !== undefined) {
1958
2088
  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
- }
2089
+ 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
2090
  }
1974
2091
  return output.observe(result);
1975
2092
  },
1976
2093
  });
1977
2094
  const result = await output.finish(run, sessionName, namespace);
1978
- if (artifactManifest && isRecord(result.details))
1979
- result.details.artifactManifest = redactSensitiveValue(artifactManifest);
2095
+ if (journalFailed && isRecord(result.details)) {
2096
+ result.isError = true;
2097
+ result.details.resultCategory = "failure";
2098
+ result.details.failureCategory = "upstream-error";
2099
+ result.details.error = "Browser-state persistence failed. Dependent calls were stopped; inspect before continuing because already-dispatched effects may have happened.";
2100
+ }
2101
+ if (isRecord(result.details))
2102
+ result.details.browserEventVersion = 1;
1980
2103
  if (!params.outputPath)
1981
2104
  return result;
1982
2105
  return artifactExecutionQueue.run(async () => {
@@ -1988,7 +2111,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1988
2111
  return applyAgentBrowserOutputPath({ cwd: operationCwd, outputPath: params.outputPath, preserveTextContent: true, result });
1989
2112
  });
1990
2113
  });
1991
- };
2114
+ });
1992
2115
  return shouldSerializeBrowserCommand({ namespace, explicitSessionName: sessionName, managedSessionName, ownedElectronLaunchRecords, ownedManagedSessions })
1993
2116
  ? managedSessionExecutionQueue.run(run, controller.signal)
1994
2117
  : callerOwnedSessionExecutionQueues.run(key, namespace, run, controller.signal);
@@ -2004,11 +2127,9 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
2004
2127
  clearTimeout(deadlineTimer);
2005
2128
  activeScriptControllers.delete(controller);
2006
2129
  signal?.removeEventListener("abort", abort);
2007
- activeScriptExecutions.delete(execution);
2008
- finish();
2009
2130
  }
2010
2131
  };
2011
- const finalizeObservation = async (result, params, ctx) => {
2132
+ const finalizeObservation = async (result, params, ctx, branch) => {
2012
2133
  if ("args" in params && params.args && isPlainTextInspectionArgs(params.args))
2013
2134
  return result;
2014
2135
  const { renderAgentBrowserObservation } = await import("./lib/results/presentation/large-output.js");
@@ -2016,27 +2137,59 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
2016
2137
  if (result.isError && details.error === undefined)
2017
2138
  details.error = details.validationError ?? details.summary ?? result.content.filter(item => item.type === "text").map(item => item.text).join("\n");
2018
2139
  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) } : {}) } };
2140
+ const renderOptions = { content: result.content, details, json: "code" in params || params.args?.includes("--json") === true, succeeded: result.isError !== true,
2141
+ persistentArtifactStore: branch.isCurrent() ? getPersistentSessionArtifactStore(ctx) : undefined,
2142
+ withArtifactWrite: (write) => artifactExecutionQueue.run(() => {
2143
+ if (!branch.isCurrent())
2144
+ renderOptions.persistentArtifactStore = undefined;
2145
+ return write();
2146
+ }) };
2147
+ const rendered = await renderAgentBrowserObservation(renderOptions);
2148
+ if (!priorManifest && !rendered.artifactManifest)
2149
+ return { ...result, content: rendered.content, structuredContent: rendered.structuredContent, details: { ...details, browserEventVersion: 1 } };
2150
+ return artifactExecutionQueue.run(async () => {
2151
+ if (!branch.isCurrent())
2152
+ return { ...result, content: rendered.content, structuredContent: rendered.structuredContent, details: { ...details, browserEventVersion: 1, artifactManifest: rendered.artifactManifest } };
2153
+ const before = artifactManifest;
2154
+ const merged = mergeBrowserRunArtifactManifest(artifactManifest, priorManifest, rendered.artifactManifest);
2155
+ const changes = artifactChanges(before, merged);
2156
+ if (changes && hasPublishedBrowserJournal(ctx.sessionManager)) {
2157
+ const record = { event: { version: 1, phase: "state", operationId: randomUUID(), toolCallId: "observation", commandIndex: 0, isError: result.isError === true, state: {}, artifacts: changes } };
2158
+ if (!await appendBrowserRecord(ctx.sessionManager, () => appendBrowserTransition(pi, record), record, branch)
2159
+ || !branch.isCurrent())
2160
+ return { ...result, content: rendered.content, structuredContent: rendered.structuredContent, details: { ...details, browserEventVersion: 1, artifactManifest: rendered.artifactManifest } };
2161
+ }
2162
+ artifactManifest = mergeBrowserRunArtifactManifest(artifactManifest, priorManifest, rendered.artifactManifest);
2163
+ return { ...result, content: rendered.content, structuredContent: rendered.structuredContent, details: { ...details, browserEventVersion: 1, artifactManifest: invocationArtifactManifest(artifactManifest, before, details, priorManifest) } };
2164
+ });
2022
2165
  };
2023
2166
  registerAgentBrowserToolSurface(pi, {
2024
2167
  async execute(id, params, signal, onUpdate, ctx) {
2168
+ const branch = captureCurrentBrowserBranch(ctx);
2025
2169
  let result;
2026
2170
  try {
2027
- result = await agentBrowserTool.execute(id, params, signal, onUpdate, ctx);
2171
+ result = await agentBrowserTool.execute(id, params, signal, onUpdate, ctx, id, undefined, true, undefined, branch);
2028
2172
  }
2029
2173
  catch (error) {
2030
2174
  result = browserExecutionFailure(error, signal);
2031
2175
  }
2032
- return finalizeObservation(result, params, ctx);
2176
+ return finalizeObservation(result, params, ctx, branch);
2033
2177
  },
2034
2178
  async executeCode(id, params, signal, onUpdate, ctx) {
2035
- const result = await executeCode(id, params, signal, onUpdate, ctx);
2036
- return finalizeObservation(result, params, ctx);
2179
+ const branch = captureCurrentBrowserBranch(ctx);
2180
+ let finish;
2181
+ const execution = new Promise(resolve => { finish = resolve; });
2182
+ activeScriptExecutions.add(execution);
2183
+ try {
2184
+ const result = await executeCode(id, params, signal, onUpdate, ctx, branch);
2185
+ return await finalizeObservation(result, params, ctx, branch);
2186
+ }
2187
+ finally {
2188
+ activeScriptExecutions.delete(execution);
2189
+ finish();
2190
+ }
2037
2191
  },
2038
2192
  executionMode: beforeExecute ? "sequential" : undefined,
2039
- promptGuidelines: toolPromptGuidelines,
2040
2193
  renderCall: agentBrowserTool.renderCall, renderResult: agentBrowserTool.renderResult,
2041
2194
  });
2042
2195
  registerWebSearchToolIfAvailable(agentBrowserConfig);