pi-agent-browser-native 0.8.3 → 0.9.1

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 (48) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +12 -11
  3. package/dist/extensions/agent-browser/index.js +96 -320
  4. package/dist/extensions/agent-browser/lib/argv-descriptor.js +1 -1
  5. package/dist/extensions/agent-browser/lib/argv-grammar.js +3 -3
  6. package/dist/extensions/agent-browser/lib/browser-journal.js +33 -12
  7. package/dist/extensions/agent-browser/lib/browser-transcript.js +1 -1
  8. package/dist/extensions/agent-browser/lib/command-taxonomy.js +0 -3
  9. package/dist/extensions/agent-browser/lib/config-policy.js +0 -13
  10. package/dist/extensions/agent-browser/lib/config.js +3 -48
  11. package/dist/extensions/agent-browser/lib/electron/launch.js +4 -4
  12. package/dist/extensions/agent-browser/lib/execution-cwd.js +0 -16
  13. package/dist/extensions/agent-browser/lib/input-modes/params.js +2 -3
  14. package/dist/extensions/agent-browser/lib/input-modes/script.js +28 -6
  15. package/dist/extensions/agent-browser/lib/input-modes/types.js +2 -2
  16. package/dist/extensions/agent-browser/lib/managed-session-restore.js +1 -4
  17. package/dist/extensions/agent-browser/lib/native-output.js +46 -0
  18. package/dist/extensions/agent-browser/lib/orchestration/batch-stdin.js +0 -9
  19. package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-merge.js +22 -0
  20. package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +1 -1
  21. package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +2 -2
  22. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +1 -1
  23. package/dist/extensions/agent-browser/lib/orchestration/execution-queue.js +86 -0
  24. package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +31 -17
  25. package/dist/extensions/agent-browser/lib/process.js +0 -3
  26. package/dist/extensions/agent-browser/lib/prompt-policy.js +16 -12
  27. package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +2 -2
  28. package/dist/extensions/agent-browser/lib/results/network-routes.js +1 -1
  29. package/dist/extensions/agent-browser/lib/results/network.js +1 -1
  30. package/dist/extensions/agent-browser/lib/results/presentation/batch.js +1 -1
  31. package/dist/extensions/agent-browser/lib/results/presentation/content.js +4 -0
  32. package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +1 -1
  33. package/dist/extensions/agent-browser/lib/results/presentation/large-output.js +15 -8
  34. package/dist/extensions/agent-browser/lib/results/presentation/semantic-action.js +2 -2
  35. package/dist/extensions/agent-browser/lib/results/selector-recovery.js +1 -1
  36. package/dist/extensions/agent-browser/lib/runtime.js +4 -2
  37. package/dist/extensions/agent-browser/lib/session-page-state.js +1 -1
  38. package/dist/extensions/agent-browser/lib/tool-surface.js +39 -11
  39. package/dist/extensions/agent-browser/lib/web-search.js +100 -79
  40. package/docs/ARCHITECTURE.md +17 -19
  41. package/docs/COMMAND_REFERENCE.md +6 -2
  42. package/docs/RELEASE.md +26 -20
  43. package/docs/REQUIREMENTS.md +5 -5
  44. package/docs/SESSION_STORAGE.md +4 -4
  45. package/docs/SUPPORT_MATRIX.md +15 -13
  46. package/docs/TOOL_CONTRACT.md +12 -8
  47. package/package.json +6 -6
  48. package/scripts/doctor.mjs +12 -4
@@ -1,7 +1,6 @@
1
- import { AsyncLocalStorage } from "node:async_hooks";
2
1
  import { randomUUID } from "node:crypto";
3
2
  import { existsSync, readFileSync } from "node:fs";
4
- import { dirname, join } from "node:path";
3
+ import { dirname, isAbsolute, join } from "node:path";
5
4
  import { fileURLToPath } from "node:url";
6
5
  import { Text } from "@earendil-works/pi-tui";
7
6
  import { batchHasSuccessfulCloseAll, getSuccessfulBatchCloseLifecycle } from "./lib/batch-lifecycle.js";
@@ -13,17 +12,19 @@ import { buildExecutionPlan, canUseHeadlessCompatibilityUserAgent, createEphemer
13
12
  import { deleteIdentityKeysInNamespace, extractExplicitNamespace, extractExplicitSessionName, getAgentBrowserSessionIdentityKey, isAgentBrowserSessionIdentityKeyInNamespace, isUpstreamEnvFlagEnabled, resolveAgentBrowserNamespace, scanUpstreamGlobalFlagOccurrences } from "./lib/argv-grammar.js";
14
13
  import { parseArgvDescriptor } from "./lib/argv-descriptor.js";
15
14
  import { needsManagedSession } from "./lib/command-policy.js";
16
- import { ManagedSessionRestoreState, resolveOwnedManagedSessionContext, withOwnedManagedSessionContext } from "./lib/managed-session-restore.js";
15
+ import { ManagedSessionRestoreState } from "./lib/managed-session-restore.js";
17
16
  import { isRecord } from "./lib/parsing.js";
18
17
  import { getAgentBrowserProcessTimeoutMs, resolveAgentBrowserSocketDir, runAgentBrowserProcess } from "./lib/process.js";
19
18
  import { getCommandAwareProcessTimeoutMs } from "./lib/orchestration/browser-run/prepare/wait-timeouts.js";
20
19
  import { getAgentBrowserProcessEnvironment, withAgentBrowserProcessEnvironment, withIsolatedAgentBrowserEnvironment } from "./lib/process-environment.js";
21
20
  import { withNativeSessionDefaults } from "./lib/orchestration/native-session-defaults.js";
22
- import { getBrowserCwdError, resolveExecutionCwd, restoreManagedSessionCwd } from "./lib/execution-cwd.js";
21
+ import { getBrowserCwdError, resolveExecutionCwd } from "./lib/execution-cwd.js";
23
22
  import { resolveOperationPaths } from "./lib/orchestration/operation-paths.js";
23
+ import { AsyncExecutionQueue, KeyedAsyncExecutionQueue } from "./lib/orchestration/execution-queue.js";
24
+ import { mergeBrowserRunArtifactManifest } from "./lib/orchestration/browser-run/artifact-merge.js";
24
25
  import { closeManagedSession, inspectManagedSessionDaemon } from "./lib/orchestration/browser-run/managed-session-daemon-policy.js";
25
26
  import { MINIMUM_AGENT_BROWSER_VERSION, SUPPORTED_AGENT_BROWSER_VERSION_LABEL, TARGET_AGENT_BROWSER_VERSION, getAgentBrowserVersionValidationError, parseAgentBrowserVersionOutput, } from "./lib/upstream-version.js";
26
- import { buildPromptPolicy, getLatestUserPrompt } from "./lib/prompt-policy.js";
27
+ import { buildPromptPolicy, getLatestUserMessage, getMessageText } from "./lib/prompt-policy.js";
27
28
  import { isCloseAllCommand, isCloseCommand } from "./lib/command-taxonomy.js";
28
29
  import { hasLaunchScopedFlagToken } from "./lib/launch-scoped-flags.js";
29
30
  import { cleanupSecureTempArtifacts } from "./lib/temp.js";
@@ -37,14 +38,14 @@ import { applyAgentBrowserOutputPath, canWriteAgentBrowserOutput, normalizeReque
37
38
  import { appendScriptSessionLease, createBrowserCodeOutput, getScriptSessionLeasesFromBranch } from "./lib/orchestration/script-mode.js";
38
39
  import { resolveBrowserExecutionIdentity, withBrowserExecutionLock, withBrowserExecutionLocks } from "./lib/managed-session-policy-lock.js";
39
40
  import { AGENT_BROWSER_INSTRUCTION_GROUP, AGENT_BROWSER_TOOL_INVENTORY, registerAgentBrowserToolSurface } from "./lib/tool-surface.js";
40
- import { formatSessionArtifactRetentionSummary, getSessionArtifactManifestEntryKey, isPendingRecordingCommand, isSessionArtifactManifest, mergeSessionArtifactManifest, retirePendingRecordingManifestEntries } from "./lib/results/artifact-manifest.js";
41
+ import { formatSessionArtifactRetentionSummary, getSessionArtifactManifestEntryKey, isSessionArtifactManifest, retirePendingRecordingManifestEntries } from "./lib/results/artifact-manifest.js";
41
42
  import { appendUniqueAgentBrowserNextActions, applyNamespaceToNextActions, applySessionToNextActions, buildNextToolAction } from "./lib/results/next-actions.js";
42
43
  import { canRegisterWebSearchTool, loadAgentBrowserConfigSync } from "./lib/config.js";
43
44
  import { appendRecordingReservationTransition, applyRecordingArtifactsToReservations, restoreRecordingReservationStateFromBranch, retireRecordingReservation, } from "./lib/recording-reservations.js";
44
45
  import { createAgentBrowserWebSearchTool } from "./lib/web-search.js";
45
46
  import { scopeReadConfirmationArgs } from "./lib/read-confirmation.js";
46
47
  import { isDirectAgentBrowserBashAllowed, isHarmlessAgentBrowserInspectionCommand, looksLikeDirectAgentBrowserBash, } from "./lib/bash-guard.js";
47
- import { AgentBrowserResultComponent, buildAgentBrowserToolResultPatch, formatAgentBrowserRenderCall, formatAgentBrowserRenderResult, } from "./lib/pi-tool-rendering.js";
48
+ import { AgentBrowserResultComponent, formatAgentBrowserRenderCall, } from "./lib/pi-tool-rendering.js";
48
49
  function browserExecutionFailure(error, signal, fallbackCategory = "upstream-error") {
49
50
  const timeout = error instanceof Error && error.name === "TimeoutError";
50
51
  const summary = redactSensitiveText(error instanceof Error ? error.message : String(error));
@@ -230,84 +231,6 @@ function getRecognizedCompatibilityWorkaround(value) {
230
231
  ? { id: workaround.id, reason: workaround.reason }
231
232
  : undefined;
232
233
  }
233
- function restoreManagedSessionCompatibilityWorkaroundFromBranch(branch, sessionName, namespace) {
234
- let restored;
235
- const targetKey = getSessionContextKey(sessionName, namespace);
236
- for (const entry of branch) {
237
- const message = getBrowserResultMessage(entry);
238
- if (!message)
239
- continue;
240
- const details = isRecord(message.details) ? message.details : undefined;
241
- if (!details)
242
- continue;
243
- if (getSessionContextKey(typeof details.sessionName === "string" ? details.sessionName : undefined, typeof details.namespace === "string" ? details.namespace : undefined) !== targetKey)
244
- continue;
245
- const recognizedWorkaround = getRecognizedCompatibilityWorkaround(details.compatibilityWorkaround);
246
- const succeeded = getSuccessfulToolResult(details, message);
247
- const outcome = getManagedSessionOutcome(details);
248
- const activeAfterFailure = recognizedWorkaround
249
- && outcome?.activeAfter === true
250
- && typeof outcome.currentSessionName === "string"
251
- && getSessionContextKey(outcome.currentSessionName, typeof outcome.currentSessionNamespace === "string" ? outcome.currentSessionNamespace : undefined) === targetKey
252
- && (outcome.status === "created" || outcome.status === "replaced" || outcome.status === "unchanged");
253
- if (!succeeded && !activeAfterFailure)
254
- continue;
255
- if (recognizedWorkaround) {
256
- restored = recognizedWorkaround;
257
- }
258
- else if (!canUseHeadlessCompatibilityUserAgent(getToolResultArgs(details))) {
259
- restored = undefined;
260
- }
261
- }
262
- return restored;
263
- }
264
- function restoreManagedSessionHeadedAutosaveDisabledFromBranch(branch, sessionName, namespace) {
265
- let restored = false;
266
- const targetKey = getSessionContextKey(sessionName, namespace);
267
- for (const entry of branch) {
268
- const message = getBrowserResultMessage(entry);
269
- if (!message)
270
- continue;
271
- const details = isRecord(message.details) ? message.details : undefined;
272
- if (!details)
273
- continue;
274
- if (getSessionContextKey(typeof details.sessionName === "string" ? details.sessionName : undefined, typeof details.namespace === "string" ? details.namespace : undefined) !== targetKey)
275
- continue;
276
- const outcome = getManagedSessionOutcome(details);
277
- const activeAfterFailure = outcome?.activeAfter === true
278
- && typeof outcome.currentSessionName === "string"
279
- && getSessionContextKey(outcome.currentSessionName, typeof outcome.currentSessionNamespace === "string" ? outcome.currentSessionNamespace : undefined) === targetKey;
280
- if ((getSuccessfulToolResult(details, message) || activeAfterFailure) && typeof details.managedSessionHeadedAutosaveDisabled === "boolean") {
281
- restored = details.managedSessionHeadedAutosaveDisabled;
282
- }
283
- }
284
- return restored;
285
- }
286
- function restoreManagedSessionHeadedAutosaveIntervalFromBranch(branch, sessionName, namespace) {
287
- let restored;
288
- const targetKey = getSessionContextKey(sessionName, namespace);
289
- for (const entry of branch) {
290
- const message = getBrowserResultMessage(entry);
291
- if (!message)
292
- continue;
293
- const details = isRecord(message.details) ? message.details : undefined;
294
- if (!details)
295
- continue;
296
- if (getSessionContextKey(typeof details.sessionName === "string" ? details.sessionName : undefined, typeof details.namespace === "string" ? details.namespace : undefined) !== targetKey)
297
- continue;
298
- const outcome = getManagedSessionOutcome(details);
299
- const activeAfterFailure = outcome?.activeAfter === true
300
- && typeof outcome.currentSessionName === "string"
301
- && getSessionContextKey(outcome.currentSessionName, typeof outcome.currentSessionNamespace === "string" ? outcome.currentSessionNamespace : undefined) === targetKey;
302
- if (!getSuccessfulToolResult(details, message) && !activeAfterFailure)
303
- continue;
304
- if (typeof details.managedSessionHeadedAutosaveInterval === "string")
305
- restored = details.managedSessionHeadedAutosaveInterval;
306
- else if (details.managedSessionHeadedAutosaveDisabled === true)
307
- restored = "0";
308
- }
309
- return restored;
310
- }
311
234
  function getToolResultArgs(details) {
312
235
  if (Array.isArray(details.args) && details.args.every((arg) => typeof arg === "string"))
313
236
  return details.args;
@@ -385,18 +308,6 @@ function trackOwnedManagedSession(sessions, sessionName, cwd, options = {}) {
385
308
  const headedManagedAutosaveInterval = options.headedManagedAutosaveInterval ?? existing?.headedManagedAutosaveInterval;
386
309
  sessions.set(key, { branchOwned, compatibilityWorkaround, cwd, headedManagedAutosaveDisabled, headedManagedAutosaveInterval, namespace: options.namespace, sessionName, socketDir: options.socketDir ?? existing?.socketDir });
387
310
  }
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;
399
- }
400
311
  function untrackOwnedManagedSession(sessions, sessionName, namespace) {
401
312
  if (!sessionName)
402
313
  return;
@@ -590,6 +501,7 @@ function collectBranchManagedResourceEvents(branch) {
590
501
  managedSessionActiveIdentities: new Map(),
591
502
  managedSessionActiveRanks: new Map(),
592
503
  managedSessionCloseRanks: new Map(),
504
+ managedSessionLaunchState: new Map(),
593
505
  };
594
506
  let eventRank = 0;
595
507
  for (const entry of branch) {
@@ -611,6 +523,32 @@ function collectBranchManagedResourceEvents(branch) {
611
523
  const batchCloseLifecycle = getSuccessfulBatchCloseLifecycle(details.batchSteps);
612
524
  const closeAllApplied = detailsReportCloseAllApplied(details, succeeded);
613
525
  const outcome = getManagedSessionOutcome(details);
526
+ if (sessionName) {
527
+ const key = getSessionContextKey(sessionName, namespace) ?? sessionName;
528
+ const launch = events.managedSessionLaunchState.get(key) ?? { compatibilityWorkaround: undefined, headedManagedAutosaveDisabled: false };
529
+ if (typeof details.managedSessionCwd === "string" && isAbsolute(details.managedSessionCwd))
530
+ launch.cwd = details.managedSessionCwd;
531
+ if (typeof details.managedSessionSocketDir === "string")
532
+ launch.socketDir = details.managedSessionSocketDir;
533
+ const retainedAfterFailure = outcome?.activeAfter === true && typeof outcome.currentSessionName === "string"
534
+ && getSessionContextKey(outcome.currentSessionName, typeof outcome.currentSessionNamespace === "string" ? outcome.currentSessionNamespace : undefined) === key;
535
+ const workaround = getRecognizedCompatibilityWorkaround(details.compatibilityWorkaround);
536
+ if (succeeded || workaround && retainedAfterFailure && ["created", "replaced", "unchanged"].includes(String(outcome?.status))) {
537
+ if (workaround)
538
+ launch.compatibilityWorkaround = workaround;
539
+ else if (!canUseHeadlessCompatibilityUserAgent(getToolResultArgs(details)))
540
+ launch.compatibilityWorkaround = undefined;
541
+ }
542
+ if (succeeded || retainedAfterFailure) {
543
+ if (typeof details.managedSessionHeadedAutosaveDisabled === "boolean")
544
+ launch.headedManagedAutosaveDisabled = details.managedSessionHeadedAutosaveDisabled;
545
+ if (typeof details.managedSessionHeadedAutosaveInterval === "string")
546
+ launch.headedManagedAutosaveInterval = details.managedSessionHeadedAutosaveInterval;
547
+ else if (details.managedSessionHeadedAutosaveDisabled === true)
548
+ launch.headedManagedAutosaveInterval = "0";
549
+ }
550
+ events.managedSessionLaunchState.set(key, launch);
551
+ }
614
552
  const outcomeSucceeded = outcome?.succeeded === true;
615
553
  const outcomeStatus = typeof outcome?.status === "string" ? outcome.status : undefined;
616
554
  const outcomeCurrentSessionName = typeof outcome?.currentSessionName === "string" ? outcome.currentSessionName : undefined;
@@ -710,90 +648,6 @@ function shouldSerializeBrowserCommand(options) {
710
648
  return true;
711
649
  return getActiveElectronRecords(options.ownedElectronLaunchRecords).some((record) => record.sessionName === options.explicitSessionName);
712
650
  }
713
- // Serializes managed-session read/modify/write work so overlapping tool calls cannot promote stale state or close an in-use session.
714
- class AsyncExecutionQueue {
715
- tail = Promise.resolve();
716
- active = new AsyncLocalStorage();
717
- isCurrent() { return this.active.getStore()?.active === true; }
718
- run(work, signal, barrier = Promise.resolve()) {
719
- if (this.isCurrent()) {
720
- signal?.throwIfAborted();
721
- return work();
722
- }
723
- const previous = this.tail;
724
- let release;
725
- this.tail = new Promise((resolve) => {
726
- release = resolve;
727
- });
728
- let rejectWaiting;
729
- const cancelled = new Promise((_resolve, reject) => { rejectWaiting = reject; });
730
- const abortWaiting = () => rejectWaiting(signal?.reason);
731
- signal?.addEventListener("abort", abortWaiting, { once: true });
732
- if (signal?.aborted)
733
- abortWaiting();
734
- const execution = (async () => {
735
- await Promise.all([previous, barrier]);
736
- signal?.removeEventListener("abort", abortWaiting);
737
- try {
738
- signal?.throwIfAborted();
739
- const scope = { active: true };
740
- try {
741
- return await this.active.run(scope, work);
742
- }
743
- finally {
744
- scope.active = false;
745
- }
746
- }
747
- finally {
748
- release();
749
- }
750
- })();
751
- return signal ? Promise.race([execution, cancelled]) : execution;
752
- }
753
- }
754
- export class KeyedAsyncExecutionQueue {
755
- barriers = new Map();
756
- entries = new Map();
757
- async run(key, namespace, work, signal) {
758
- const entry = this.entries.get(key) ?? { queue: new AsyncExecutionQueue(), users: 0 };
759
- if (entry.queue.isCurrent())
760
- return work();
761
- const barrier = this.barriers.get(getAgentBrowserSessionIdentityKey("", namespace)) ?? Promise.resolve();
762
- entry.users += 1;
763
- this.entries.set(key, entry);
764
- try {
765
- return await entry.queue.run(work, signal, barrier);
766
- }
767
- finally {
768
- entry.users -= 1;
769
- if (entry.users === 0 && this.entries.get(key) === entry)
770
- this.entries.delete(key);
771
- }
772
- }
773
- async runExclusive(namespace, work) {
774
- const namespaceKey = getAgentBrowserSessionIdentityKey("", namespace);
775
- const previous = this.barriers.get(namespaceKey) ?? Promise.resolve();
776
- let release;
777
- const blocked = new Promise((resolve) => {
778
- release = resolve;
779
- });
780
- const barrier = previous.then(() => blocked);
781
- this.barriers.set(namespaceKey, barrier);
782
- const drains = [...this.entries]
783
- .filter(([key]) => isAgentBrowserSessionIdentityKeyInNamespace(key, namespace))
784
- .map(([, { queue }]) => queue.run(async () => undefined));
785
- await previous;
786
- await Promise.all(drains);
787
- try {
788
- return await work();
789
- }
790
- finally {
791
- release();
792
- if (this.barriers.get(namespaceKey) === barrier)
793
- this.barriers.delete(namespaceKey);
794
- }
795
- }
796
- }
797
651
  function mergeBrowserRunMap(current, initial, updated) {
798
652
  if (updated === initial)
799
653
  return current;
@@ -808,27 +662,6 @@ function mergeBrowserRunMap(current, initial, updated) {
808
662
  }
809
663
  return merged;
810
664
  }
811
- export function mergeBrowserRunArtifactManifest(current, initial, updated) {
812
- if (!updated || updated === initial)
813
- return current;
814
- if (current === initial)
815
- return updated;
816
- const initialEntries = new Map((initial?.entries ?? []).map((entry) => [getSessionArtifactManifestEntryKey(entry), entry]));
817
- const changedEntries = updated.entries
818
- .map((entry, index) => ({ entry, index }))
819
- .filter(({ entry }) => initialEntries.get(getSessionArtifactManifestEntryKey(entry)) !== entry)
820
- .sort((left, right) => left.entry.createdAtMs - right.entry.createdAtMs
821
- || Number(isPendingRecordingCommand(left.entry.command, left.entry.subcommand, left.entry.kind)) - Number(isPendingRecordingCommand(right.entry.command, right.entry.subcommand, right.entry.kind))
822
- || left.index - right.index)
823
- .map(({ entry }) => entry);
824
- return changedEntries.length === 0
825
- ? current
826
- : mergeSessionArtifactManifest({
827
- base: current,
828
- entries: changedEntries,
829
- nowMs: Math.max(Date.now(), (current?.updatedAtMs ?? 0) + 1, updated.updatedAtMs),
830
- });
831
- }
832
665
  function findPackageRoot(startDir) {
833
666
  let currentDir = startDir;
834
667
  while (true) {
@@ -858,7 +691,7 @@ function hasArgvFlag(argv, longFlag, shortFlag) {
858
691
  function shouldIncludeProjectConfig(ctx, argv = process.argv) {
859
692
  if (hasArgvFlag(argv, "--no-approve", "-na"))
860
693
  return false;
861
- return ctx?.isProjectTrusted?.() ?? true;
694
+ return ctx.isProjectTrusted();
862
695
  }
863
696
  export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
864
697
  const ephemeralSessionSeed = createEphemeralSessionSeed();
@@ -868,6 +701,21 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
868
701
  });
869
702
  const implicitSessionIdleTimeoutMs = String(getImplicitSessionIdleTimeoutMs());
870
703
  const implicitSessionCloseTimeoutMs = getImplicitSessionCloseTimeoutMs();
704
+ let promptState;
705
+ const restorePromptState = (ctx) => {
706
+ return promptState = { sessionId: ctx.sessionManager.getSessionId(), message: getLatestUserMessage(ctx.sessionManager) };
707
+ };
708
+ const getPromptPolicy = (ctx) => {
709
+ const state = promptState?.sessionId === ctx.sessionManager.getSessionId() ? promptState : restorePromptState(ctx);
710
+ // Native message_end precedes later transforms/persistence; retain the message
711
+ // reference so replacement user text cannot leave policy on the earlier text.
712
+ const text = getMessageText(state.message?.content);
713
+ if (!state.policy || state.text !== text) {
714
+ state.text = text;
715
+ state.policy = buildPromptPolicy(text);
716
+ }
717
+ return state.policy;
718
+ };
871
719
  let webSearchToolRegistered = false;
872
720
  let managedSessionActive = false;
873
721
  let managedSessionBaseName = createImplicitSessionName(undefined, process.cwd(), ephemeralSessionSeed);
@@ -1121,11 +969,11 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1121
969
  clearSessionScopedBrowserState(sessionName, AGENT_BROWSER_SCRIPT_NAMESPACE);
1122
970
  return undefined;
1123
971
  };
1124
- const recoverScriptSessionLeasesWithinQueue = async (ctx) => {
972
+ const recoverScriptSessionLeasesWithinQueue = async (ctx, branch) => {
1125
973
  const pendingSessionNames = new Set([...ownedManagedSessions.values()]
1126
974
  .map((session) => session.sessionName)
1127
975
  .filter(isAgentBrowserScriptSessionName));
1128
- for (const lease of getScriptSessionLeasesFromBranch(await readBrowserEntries(ctx.sessionManager), ctx.sessionManager.getSessionId()).values()) {
976
+ for (const lease of getScriptSessionLeasesFromBranch(branch, ctx.sessionManager.getSessionId()).values()) {
1129
977
  if (lease.cleanup !== "closed")
1130
978
  pendingSessionNames.add(lease.sessionName);
1131
979
  }
@@ -1138,6 +986,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1138
986
  const restoreBranchBackedState = async (ctx, options) => {
1139
987
  branchRestoreGeneration += 1;
1140
988
  branchStateGeneration += 1;
989
+ restorePromptState(ctx);
1141
990
  const previousManagedSessionActive = managedSessionActive;
1142
991
  const previousManagedSessionName = managedSessionName;
1143
992
  const previousFreshSessionOrdinal = freshSessionOrdinal;
@@ -1179,15 +1028,11 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1179
1028
  : createFreshSessionName(managedSessionBaseName, ephemeralSessionSeed, nextFreshSessionOrdinal)
1180
1029
  : restoredState.sessionName;
1181
1030
  managedSessionNamespace = shouldReservePostCloseSession ? undefined : restoredState.namespace;
1182
- managedSessionCompatibilityWorkaround = managedSessionActive
1183
- ? restoreManagedSessionCompatibilityWorkaroundFromBranch(branch, managedSessionName, managedSessionNamespace)
1184
- : undefined;
1185
- managedSessionHeadedAutosaveDisabled = managedSessionActive
1186
- && restoreManagedSessionHeadedAutosaveDisabledFromBranch(branch, managedSessionName, managedSessionNamespace);
1187
- managedSessionHeadedAutosaveInterval = managedSessionActive
1188
- ? restoreManagedSessionHeadedAutosaveIntervalFromBranch(branch, managedSessionName, managedSessionNamespace)
1189
- : undefined;
1190
- managedSessionCwd = restoreManagedSessionCwd(branch, managedSessionName, managedSessionNamespace, ctx.cwd);
1031
+ const currentLaunch = branchResourceEvents.managedSessionLaunchState.get(getSessionContextKey(managedSessionName, managedSessionNamespace) ?? managedSessionName);
1032
+ managedSessionCompatibilityWorkaround = managedSessionActive ? currentLaunch?.compatibilityWorkaround : undefined;
1033
+ managedSessionHeadedAutosaveDisabled = managedSessionActive && currentLaunch?.headedManagedAutosaveDisabled === true;
1034
+ managedSessionHeadedAutosaveInterval = managedSessionActive ? currentLaunch?.headedManagedAutosaveInterval : undefined;
1035
+ managedSessionCwd = currentLaunch?.cwd ?? ctx.cwd;
1191
1036
  freshSessionOrdinal = nextFreshSessionOrdinal;
1192
1037
  sessionPageState = SessionPageState.fromBranch(branch);
1193
1038
  traceOwners = new Map();
@@ -1238,13 +1083,14 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1238
1083
  continue;
1239
1084
  if (!isRestorableManagedSessionName(identity.sessionName, managedSessionBaseName))
1240
1085
  continue;
1241
- trackOwnedManagedSession(ownedManagedSessions, identity.sessionName, restoreManagedSessionCwd(branch, identity.sessionName, identity.namespace, ctx.cwd), {
1086
+ const launch = branchResourceEvents.managedSessionLaunchState.get(sessionKey);
1087
+ trackOwnedManagedSession(ownedManagedSessions, identity.sessionName, launch?.cwd ?? ctx.cwd, {
1242
1088
  branchOwned: true,
1243
- compatibilityWorkaround: restoreManagedSessionCompatibilityWorkaroundFromBranch(branch, identity.sessionName, identity.namespace),
1244
- headedManagedAutosaveDisabled: restoreManagedSessionHeadedAutosaveDisabledFromBranch(branch, identity.sessionName, identity.namespace),
1245
- headedManagedAutosaveInterval: restoreManagedSessionHeadedAutosaveIntervalFromBranch(branch, identity.sessionName, identity.namespace),
1089
+ compatibilityWorkaround: launch?.compatibilityWorkaround,
1090
+ headedManagedAutosaveDisabled: launch?.headedManagedAutosaveDisabled ?? false,
1091
+ headedManagedAutosaveInterval: launch?.headedManagedAutosaveInterval,
1246
1092
  namespace: identity.namespace,
1247
- socketDir: restoreManagedSessionSocketDir(branch, identity.sessionName, identity.namespace),
1093
+ socketDir: launch?.socketDir,
1248
1094
  });
1249
1095
  }
1250
1096
  if (restoredState.active) {
@@ -1254,7 +1100,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1254
1100
  headedManagedAutosaveDisabled: managedSessionHeadedAutosaveDisabled,
1255
1101
  headedManagedAutosaveInterval: managedSessionHeadedAutosaveInterval,
1256
1102
  namespace: restoredState.namespace,
1257
- socketDir: restoreManagedSessionSocketDir(branch, restoredState.sessionName, restoredState.namespace),
1103
+ socketDir: currentLaunch?.socketDir,
1258
1104
  });
1259
1105
  }
1260
1106
  const pendingOwnedBegins = new Map();
@@ -1290,11 +1136,12 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1290
1136
  const closeRank = branchResourceEvents.managedSessionCloseRanks.get(sessionKey);
1291
1137
  if (activeRank === undefined || (closeRank !== undefined && closeRank >= activeRank))
1292
1138
  continue;
1293
- trackOwnedManagedSession(ownedManagedSessions, record.sessionName, restoreManagedSessionCwd(branch, record.sessionName, undefined, ctx.cwd), {
1139
+ const launch = branchResourceEvents.managedSessionLaunchState.get(sessionKey);
1140
+ trackOwnedManagedSession(ownedManagedSessions, record.sessionName, launch?.cwd ?? ctx.cwd, {
1294
1141
  branchOwned: true,
1295
- headedManagedAutosaveDisabled: restoreManagedSessionHeadedAutosaveDisabledFromBranch(branch, record.sessionName),
1296
- headedManagedAutosaveInterval: restoreManagedSessionHeadedAutosaveIntervalFromBranch(branch, record.sessionName),
1297
- socketDir: restoreManagedSessionSocketDir(branch, record.sessionName),
1142
+ headedManagedAutosaveDisabled: launch?.headedManagedAutosaveDisabled ?? false,
1143
+ headedManagedAutosaveInterval: launch?.headedManagedAutosaveInterval,
1144
+ socketDir: launch?.socketDir,
1298
1145
  });
1299
1146
  }
1300
1147
  mergeActiveElectronLaunchRecords(ownedElectronLaunchRecords, new Map([...electronLaunchRecords].filter(([, record]) => record.ownerSessionId === browserOwnerSessionId)), {
@@ -1323,6 +1170,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1323
1170
  attachedSessionKeys.add(sessionKey);
1324
1171
  }
1325
1172
  }
1173
+ return branch;
1326
1174
  };
1327
1175
  const registerWebSearchToolIfAvailable = (configState) => {
1328
1176
  if (webSearchToolRegistered || !canRegisterWebSearchTool(configState))
@@ -1339,15 +1187,19 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1339
1187
  });
1340
1188
  webSearchToolRegistered = true;
1341
1189
  };
1190
+ pi.on("message_end", (event, ctx) => {
1191
+ if (event.message.role === "user")
1192
+ promptState = { sessionId: ctx.sessionManager.getSessionId(), message: event.message };
1193
+ });
1342
1194
  pi.on("session_start", async (_event, ctx) => {
1343
- await restoreBranchBackedState(ctx, { resetRuntimeOwnership: true });
1344
- electronChildProcesses = new Map();
1345
- registerWebSearchToolIfAvailable(loadAgentBrowserConfigSync({
1346
- cwd: ctx.cwd,
1347
- includeProjectConfig: shouldIncludeProjectConfig(ctx),
1348
- }));
1349
1195
  await managedSessionExecutionQueue.run(() => artifactExecutionQueue.run(async () => {
1350
- await recoverScriptSessionLeasesWithinQueue(ctx);
1196
+ const branch = await restoreBranchBackedState(ctx, { resetRuntimeOwnership: true });
1197
+ electronChildProcesses = new Map();
1198
+ registerWebSearchToolIfAvailable(loadAgentBrowserConfigSync({
1199
+ cwd: ctx.cwd,
1200
+ includeProjectConfig: shouldIncludeProjectConfig(ctx),
1201
+ }));
1202
+ await recoverScriptSessionLeasesWithinQueue(ctx, branch);
1351
1203
  flushRecordingReservations();
1352
1204
  notifyRecordingPersistence(ctx);
1353
1205
  }));
@@ -1358,87 +1210,12 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1358
1210
  controller.abort();
1359
1211
  await Promise.allSettled([...activeScriptExecutions]);
1360
1212
  await managedSessionExecutionQueue.run(() => artifactExecutionQueue.run(async () => {
1361
- await restoreBranchBackedState(ctx, { resetRuntimeOwnership: false });
1362
- await recoverScriptSessionLeasesWithinQueue(ctx);
1213
+ const branch = await restoreBranchBackedState(ctx, { resetRuntimeOwnership: false });
1214
+ await recoverScriptSessionLeasesWithinQueue(ctx, branch);
1363
1215
  flushRecordingReservations();
1364
1216
  notifyRecordingPersistence(ctx);
1365
1217
  }));
1366
1218
  });
1367
- // Fork-only checkpoint event; official Pi validation types do not declare it.
1368
- // Pi owns awaited tools/events (including their execution queues). Detached code
1369
- // children and retained browser/recording resources still need extension-side checks.
1370
- const checkpointPi = pi;
1371
- checkpointPi.on("session_checkpoint", async (event, ctx) => {
1372
- const blocked = (reason) => ({ sleepReady: false, reason });
1373
- if (browserReplayError)
1374
- return blocked(browserReplayError);
1375
- if (activeScriptControllers.size || activeScriptExecutions.size)
1376
- return blocked("Browser code execution is pending");
1377
- flushRecordingReservations();
1378
- if (recordingReservationsDirty)
1379
- return blocked("Browser recording journal persistence is dirty");
1380
- if (activeRecordingReservations.size)
1381
- return blocked("Browser recording is pending; finish it explicitly before sleep");
1382
- const entries = await readBrowserEntries(ctx.sessionManager, true);
1383
- if ([...getScriptSessionLeasesFromBranch(entries).values()].some(lease => lease.cleanup !== "closed")
1384
- || [...ownedManagedSessions.values()].some(owner => isAgentBrowserScriptSessionName(owner.sessionName))) {
1385
- return blocked("Browser script cleanup lease is unresolved");
1386
- }
1387
- if (getActiveElectronRecords(ownedElectronLaunchRecords).length || getActiveElectronRecords(electronLaunchRecords).length
1388
- || [...electronChildProcesses.values()].some(child => child.exitCode === null && child.signalCode === null)) {
1389
- return blocked("Electron launch is still active");
1390
- }
1391
- if (attachedSessionKeys.size || restoreAttachedSessionKeysFromBranch(entries).size)
1392
- return blocked("Attached browser state is caller-owned and not checkpointed");
1393
- if (traceOwners.size || [...networkRoutesBySession.values()].some(routes => routes.length))
1394
- return blocked("Browser trace or network routes are still active");
1395
- // Include off-branch and caller-owned/root identities, without acquiring
1396
- // cleanup ownership. Transcript page/ref details do not serialize a browser.
1397
- const historicalResources = collectBranchManagedResourceEvents(entries);
1398
- const sessions = new Map(ownedManagedSessions);
1399
- if (managedSessionActive)
1400
- trackOwnedManagedSession(sessions, managedSessionName, managedSessionCwd, { namespace: managedSessionNamespace });
1401
- for (const entry of entries) {
1402
- const message = getBrowserResultMessage(entry);
1403
- if (!message)
1404
- continue;
1405
- const details = isRecord(message.details) ? message.details : undefined;
1406
- // Helpers can launch before a main-process failure. Conversely, native
1407
- // launch flags on sessionless reads can open the unnamed default browser.
1408
- const sessionName = typeof details?.sessionName === "string" ? details.sessionName
1409
- : details?.agentBrowserStarted === true ? "default" : undefined;
1410
- if (!sessionName)
1411
- continue;
1412
- const namespace = typeof details?.namespace === "string" ? details.namespace : undefined;
1413
- const key = getSessionContextKey(sessionName, namespace) ?? sessionName;
1414
- if (!sessions.has(key))
1415
- trackOwnedManagedSession(sessions, sessionName, ctx.cwd, { namespace });
1416
- }
1417
- for (const [key, owner] of sessions) {
1418
- event.signal.throwIfAborted();
1419
- // An abnormal restart can leave a wrapper-created browser only off-branch.
1420
- // Reuse native ownership events for inspection, never cleanup ownership.
1421
- // A later close can refer to the same identity in a different socket root.
1422
- const historicalIdentity = historicalResources.managedSessionActiveIdentities.get(key);
1423
- const historicallyOwned = historicalIdentity && isRestorableManagedSessionName(historicalIdentity.sessionName, managedSessionBaseName);
1424
- const context = resolveOwnedManagedSessionContext({
1425
- ...owner,
1426
- currentManagedSessionName: managedSessionActive ? managedSessionName : undefined,
1427
- currentManagedSessionNamespace: managedSessionNamespace,
1428
- recordedOwnedSession: ownedManagedSessions.get(key) ?? (historicallyOwned ? { ...historicalIdentity, cwd: ctx.cwd } : undefined),
1429
- restoreState: managedSessionRestoreState,
1430
- });
1431
- // Owned routing must not mask an explicit caller's same-name ambient daemon.
1432
- for (const inspectionContext of context ? [{ ...context, reuseOnly: true }, undefined] : [undefined]) {
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 })));
1434
- if (daemon.status !== "inactive")
1435
- return blocked("Browser daemon is live or unverified; finish browser work explicitly before sleep");
1436
- }
1437
- }
1438
- // No detached writer remains to pause/resume. Native ingress stays held;
1439
- // checkpoint never closes a browser or changes ordinary shutdown ownership.
1440
- return { sleepReady: true };
1441
- });
1442
1219
  pi.on("session_shutdown", async (event, ctx) => {
1443
1220
  for (const controller of activeScriptControllers)
1444
1221
  controller.abort();
@@ -1548,11 +1325,11 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1548
1325
  event.systemPromptOptions.sections.agent_browser = fullInstructions(ctx);
1549
1326
  });
1550
1327
  pi.on("tool_call", async (event, ctx) => {
1551
- const promptPolicy = buildPromptPolicy(getLatestUserPrompt(ctx.sessionManager.getBranch()));
1552
- if (isBashToolCallEvent(event) &&
1553
- !promptPolicy.allowLegacyAgentBrowserBash &&
1554
- looksLikeDirectAgentBrowserBash(event.input.command) &&
1555
- !isHarmlessAgentBrowserInspectionCommand(event.input.command) &&
1328
+ if (!isBashToolCallEvent(event) || !looksLikeDirectAgentBrowserBash(event.input.command)
1329
+ || isHarmlessAgentBrowserInspectionCommand(event.input.command))
1330
+ return;
1331
+ const promptPolicy = getPromptPolicy(ctx);
1332
+ if (!promptPolicy.allowLegacyAgentBrowserBash &&
1556
1333
  !(await isDirectAgentBrowserBashAllowed(ctx.cwd))) {
1557
1334
  return {
1558
1335
  block: true,
@@ -1560,7 +1337,6 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1560
1337
  };
1561
1338
  }
1562
1339
  });
1563
- pi.on("tool_result", async (event) => buildAgentBrowserToolResultPatch(event));
1564
1340
  let emptyBrowserBranch;
1565
1341
  const captureCurrentBrowserBranch = (ctx) => {
1566
1342
  const generation = branchRestoreGeneration;
@@ -1583,7 +1359,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1583
1359
  const component = context.lastComponent instanceof AgentBrowserResultComponent
1584
1360
  ? context.lastComponent
1585
1361
  : new AgentBrowserResultComponent();
1586
- component.setState(formatAgentBrowserRenderResult(result, options, theme, context.isError), options.expanded, theme);
1362
+ component.setResult(result, options, theme, context.isError);
1587
1363
  return component;
1588
1364
  },
1589
1365
  async execute(toolCallId, params, signal, onUpdate, ctx, nativeToolCallId = toolCallId, capturedCwd, modelVisible = true, commandIndex, selectedBranch) {
@@ -1595,7 +1371,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1595
1371
  catch (error) {
1596
1372
  return buildValidationFailureResult({ kind: "invalid", status: "invalid", redactedArgs: [], toolArgs: [], validationError: error instanceof Error ? error.message : String(error) });
1597
1373
  }
1598
- const promptPolicy = buildPromptPolicy(getLatestUserPrompt(ctx.sessionManager.getBranch()));
1374
+ const promptPolicy = getPromptPolicy(ctx);
1599
1375
  const outputPath = isRecord(params) && typeof params.outputPath === "string" ? params.outputPath : undefined;
1600
1376
  const admittedInput = resolveAgentBrowserInput({
1601
1377
  getBatchPreflightValidationError: (args, stdin) => getArtifactPreflightValidationError({ args, cwd: operationCwd, outputPath, stdin }),
@@ -2266,10 +2042,10 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
2266
2042
  }) };
2267
2043
  const rendered = await renderAgentBrowserObservation(renderOptions);
2268
2044
  if (!priorManifest && !rendered.artifactManifest)
2269
- return { ...result, content: rendered.content, details: { ...details, browserEventVersion: 1 } };
2045
+ return { ...result, content: rendered.content, structuredContent: rendered.structuredContent, details: { ...details, browserEventVersion: 1 } };
2270
2046
  return artifactExecutionQueue.run(async () => {
2271
2047
  if (!branch.isCurrent())
2272
- return { ...result, content: rendered.content, details: { ...details, browserEventVersion: 1, artifactManifest: rendered.artifactManifest } };
2048
+ return { ...result, content: rendered.content, structuredContent: rendered.structuredContent, details: { ...details, browserEventVersion: 1, artifactManifest: rendered.artifactManifest } };
2273
2049
  const before = artifactManifest;
2274
2050
  const merged = mergeBrowserRunArtifactManifest(artifactManifest, priorManifest, rendered.artifactManifest);
2275
2051
  const changes = artifactChanges(before, merged);
@@ -2277,10 +2053,10 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
2277
2053
  const record = { event: { version: 1, phase: "state", operationId: randomUUID(), toolCallId: "observation", commandIndex: 0, isError: result.isError === true, state: {}, artifacts: changes } };
2278
2054
  if (!await appendBrowserRecord(ctx.sessionManager, () => appendBrowserTransition(pi, record), record, branch)
2279
2055
  || !branch.isCurrent())
2280
- return { ...result, content: rendered.content, details: { ...details, browserEventVersion: 1, artifactManifest: rendered.artifactManifest } };
2056
+ return { ...result, content: rendered.content, structuredContent: rendered.structuredContent, details: { ...details, browserEventVersion: 1, artifactManifest: rendered.artifactManifest } };
2281
2057
  }
2282
2058
  artifactManifest = mergeBrowserRunArtifactManifest(artifactManifest, priorManifest, rendered.artifactManifest);
2283
- return { ...result, content: rendered.content, details: { ...details, browserEventVersion: 1, artifactManifest: invocationArtifactManifest(artifactManifest, before, details, priorManifest) } };
2059
+ return { ...result, content: rendered.content, structuredContent: rendered.structuredContent, details: { ...details, browserEventVersion: 1, artifactManifest: invocationArtifactManifest(artifactManifest, before, details, priorManifest) } };
2284
2060
  });
2285
2061
  };
2286
2062
  registerAgentBrowserToolSurface(pi, {
@@ -76,7 +76,7 @@ function getOpenCommandTarget(commandTokens) {
76
76
  }
77
77
  return undefined;
78
78
  }
79
- export function parseCommandInfoFromTokens(commandTokens) {
79
+ function parseCommandInfoFromTokens(commandTokens) {
80
80
  const upstreamCommandTokens = stripUpstreamGlobalFlags(commandTokens);
81
81
  const command = upstreamCommandTokens[0];
82
82
  return {