pi-agent-browser-native 0.8.3 → 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.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,18 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 0.9.0 - 2026-10-01
4
+
5
+ ### Breaking changes
6
+
7
+ - Require Pi 1.0.0 or newer; Node 24.21.0 and separately installed upstream `agent-browser` requirements are unchanged. Pin the official development graph to 1.0.0 while retaining host-owned wildcard optional peers.
8
+ - Remove Browser integration with dropped checkpoint and fork metadata/revision APIs. Keep stopped-copy legacy journal conversion, native save-on-close, browser/session replay and ownership-aware cleanup.
9
+
10
+ ### Changed
11
+
12
+ - Use native `defaultActive: false` for the five advanced direct tools, retaining additive loader activation, explicit/defaultTools selections and the narrow official-1.0 initial-resume fallback.
13
+ - Return canonical native `isError` and prose failure notices directly, without a result-repair hook; preserve explicit parseable JSON. Add native namespace/output-schema metadata and bounded redacted structured observations, loader inventory and normalized search results without exposing internal replay state or replacing the Browser code VM.
14
+ - Skip irrelevant tool-event policy work; keep raw latest-user policy fresh through message completion and native content replacement. Reuse validated capture-local admission prefixes and one queued replay/launch-configuration fold, preserving publication checks and captured-leaf correctness. Skip unchanged per-row TUI formatting and text updates.
15
+ - Replace the fork-only checkpoint CI gate with portable Native lifecycle Linux SDK qualification. Official/fork qualification remains independent; while the fork's `main` is below the supported Pi floor its lane emits a truthful unattempted-below-floor receipt and skips the fork build instead of resurrecting dropped APIs. Windows remains owner-waived, not passed.
4
16
 
5
17
  ## 0.8.3 - 2026-10-01
6
18
 
package/README.md CHANGED
@@ -6,9 +6,9 @@ It is for Pi users who want agents to browse sites, inspect pages, click through
6
6
 
7
7
  ## Pi release qualification
8
8
 
9
- Development host dependencies are pinned to official Pi **0.99.2**, with wildcard runtime peers. `npm run check:compat` checks the installed candidate SDK/CLI identity, typechecks, runs the existing offline tests (including the native Pi pipeline with a controlled provider and fake browser), and tests a packed runtime-only installation through both the SDK and the actual bundled RPC CLI. The fork lane additionally requires the native idle checkpoint hook. It does not substitute a fork SDK while leaving official types or child CLI installed.
9
+ Development host dependencies are pinned to official Pi **1.0.0**, with wildcard runtime peers. `npm run check:compat` checks the installed candidate SDK/CLI identity, typechecks, runs the existing offline tests (including the native Pi pipeline with a controlled provider and fake browser), and tests a packed runtime-only installation through both the SDK and the actual bundled RPC CLI. It does not substitute a fork SDK while leaving official types or child CLI installed.
10
10
 
11
- Run in an empty HOME with a **short** TMPDIR outside your real home. Long temporary paths can exceed macOS's 103-byte Unix socket limit; keep the fixture's socket safety checks intact. Real-browser/checkpoint controls, live upstream help sampling, lifecycle dogfood, and platform qualification remain the separate gates documented below. A Pi-only compatibility run uses no live browser profiles, credentials, or agent-browser fork, and does not certify those external integrations.
11
+ Run in an empty HOME with a **short** TMPDIR outside your real home. Long temporary paths can exceed macOS's 103-byte Unix socket limit; keep the fixture's socket safety checks intact. Native lifecycle controls, live upstream help sampling, lifecycle dogfood, and platform qualification remain the separate gates documented below. A Pi-only compatibility run uses no live browser profiles, credentials, or agent-browser fork, and does not certify those external integrations.
12
12
 
13
13
  For the 0.6.16 compatibility rollout, the owner explicitly waived Windows qualification on 2026-09-21. Windows diagnostics and failures remain visible but nonblocking; this is **not** a Windows full-suite pass. Linux/macOS, declared Node floors, official/fork host and consumer-artifact checks remain required. Known Windows failures and follow-up are tracked in [#191](https://github.com/fitchmultz/pi-agent-browser-native/issues/191); see the [current support evidence](docs/SUPPORT_MATRIX.md#current-0616-rollout).
14
14
 
@@ -89,7 +89,7 @@ The result is optimized for agent work:
89
89
  | Auth/profile workflows can leak secrets | Supports `auth save --password-stdin`, redacts sensitive args, SAML/OAuth-bearing URLs, stdout/stderr, details, and snapshot spills, and discards malformed oversized stdout instead of persisting a parse-failure spill | `test/agent-browser.extension-security-redaction.test.ts` |
90
90
  | Stateful cookies/storage/auth output bloats or leaks context | Presentation layer redacts `details.data` for cookies and credential-like storage values while keeping low-risk local QA values such as `theme: dark` readable; recursively scrubs other structured upstream JSON (network, diff, trace/profiler, stream, dashboard, chat, auth, dialog, frame, state, and similar) using sensitive key names plus string heuristics; masks sensitive argv flags and positionals, including raw batch rows and ordered code journals; scrubs secrets from failed batch step errors; and exposes a compact redacted `batch` matrix on top-level `details.data` | `extensions/agent-browser/lib/results/presentation.ts`, `extensions/agent-browser/lib/results/presentation/diagnostics.ts`, `extensions/agent-browser/lib/runtime.ts`, `test/agent-browser.presentation-diagnostics.test.ts` |
91
91
  | Stale `@eN` refs fail mysteriously | Defines full per-session refs once in the native journal, exposes them on dedicated captures, rejects mismatched URLs / unknown refs / unsafe `batch` stdin ordering before spawn, adds recovery guidance to rerun `snapshot -i` or use stable `find` locators | `extensions/agent-browser/index.ts`, `extensions/agent-browser/lib/session-page-state.ts`, `test/agent-browser.session-page-state.test.ts`, `test/agent-browser.results.test.ts`, `test/agent-browser.extension-ref-guards.test.ts`, `test/agent-browser.extension-semantic-recovery.test.ts` |
92
- | Agents need stable success/failure buckets | Exposes bounded `resultCategory`, `successCategory`, and `failureCategory` on tool `details` for branching without parsing prose; a `tool_result` hook also aligns real Pi `isError` semantics, naming `Pi tool isError: true` in prose output while preserving parseable caller-requested `--json` output | [`docs/TOOL_CONTRACT.md`](docs/TOOL_CONTRACT.md#details), `extensions/agent-browser/lib/results/categories.ts`, `extensions/agent-browser/index.ts`, `extensions/agent-browser/lib/pi-tool-rendering.ts`, `test/agent-browser.results.test.ts`, `test/agent-browser.extension-validation.test.ts`, `test/agent-browser.pi-pipeline.test.ts` |
92
+ | Agents need stable success/failure buckets | Exposes bounded `resultCategory`, `successCategory`, and `failureCategory` on tool `details` for branching without parsing prose; final results return native `isError` and a visible prose failure notice while preserving parseable caller-requested `--json` output. Native structured results expose the same bounded, redacted observations, not internal replay metadata | [`docs/TOOL_CONTRACT.md`](docs/TOOL_CONTRACT.md#details), `extensions/agent-browser/lib/results/categories.ts`, `extensions/agent-browser/index.ts`, `extensions/agent-browser/lib/pi-tool-rendering.ts`, `test/agent-browser.results.test.ts`, `test/agent-browser.extension-validation.test.ts`, `test/agent-browser.pi-pipeline.test.ts` |
93
93
  | Clicks can report success without the page receiving the event | Top-level non-Electron direct `click` calls on `xpath=` targets or role-gated current `@e…` refs (`button`, `checkbox`, `menuitem`, `radio`, `switch`, `tab`) install a bounded target-specific DOM-event probe; eligible `@e…` refs require a unique role/name in both the latest snapshot and the live candidates. A per-probe temporary DOM marker must be read back by native `get attr <original-selector> <marker>` before the candidate can verify dispatch, including XPath scope. Missing or mismatched identity cleans up the probe and leaves the native click unverified, not failed. Duplicate-name refs pass through without a probe: their old ordinal cannot prove which element received the click. If upstream reports success but no trusted event reaches the resolved target, the wrapper fails the tool, exposes `details.clickDispatch`, and suggests explicit retry/inspect next actions (no in-page replay), including a nested-scroll `scrollintoview` action when the probe sees the target outside a scroll container or viewport. Unresolved locator clicks such as raw `find … click` are left upstream-owned to avoid false failures for frame-scoped targets. Other click results still expose `details.pageChangeSummary`; `observed: false` explicitly marks dispatch-only mutation summaries and adds a visible `Action dispatched; application change unverified` warning. Unchanged-URL clicks can surface evidence-backed `details.overlayBlockers` candidates. | [`docs/TOOL_CONTRACT.md`](docs/TOOL_CONTRACT.md#details), `extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.ts`, `extensions/agent-browser/lib/results/presentation/navigation.ts`, `test/agent-browser.presentation.test.ts`, `test/agent-browser.extension-click-dispatch.test.ts` |
94
94
  | Dashboard scroll commands can look successful while nothing moves | Handles standard `scroll <dir> [px]` against the document first (including pages whose smooth-scroll CSS defeats upstream wheel timing), falls back upstream when the document cannot move, and samples viewport/containers around the fallback; explicit CSS-container scrolling uses instant movement so smooth-scroll CSS cannot cause a premature no-movement failure; unchanged positions fail as `upstream-error` with `details.scrollNoop`, visible recovery guidance, and exact snapshot/screenshot checks. Unsupported `scrollintoview text=...` fails before dispatch, including inside effective batch rows, and shows exact native `find text ... hover` and snapshot/ref recovery payloads; help remains native pass-through. | [`docs/TOOL_CONTRACT.md`](docs/TOOL_CONTRACT.md#details), [`docs/COMMAND_REFERENCE.md`](docs/COMMAND_REFERENCE.md#core-page-and-element-commands), `test/agent-browser.extension-validation.test.ts` |
95
95
  | Dropdown/combobox clicks can focus or hit native option box-model errors | Adds first-class `select <selector> <value...>` paths through native `args` and `agent_browser_action`; semantic role/name or label select resolves exactly one current visible combobox/listbox ref before action. Custom combobox clicks still detect focused controls with explicit `aria-expanded` state but no visible options and return `details.comboboxFocus` plus exact recovery `nextActions` | [`docs/TOOL_CONTRACT.md`](docs/TOOL_CONTRACT.md#details), [`docs/COMMAND_REFERENCE.md`](docs/COMMAND_REFERENCE.md#core-page-and-element-commands), `extensions/agent-browser/lib/input-modes/semantic-action.ts`, `test/agent-browser.extension-input-modes.test.ts`, `test/agent-browser.extension-validation.test.ts` |
@@ -103,7 +103,7 @@ Artifact results show known requested paths separately from reported/resolved lo
103
103
 
104
104
  ## Fastest way to try it
105
105
 
106
- Use Node 24.21.0 or newer and Pi 0.87.0 or newer. This package keeps optional Pi core imports as wildcard `peerDependencies` because Pi package docs require the host Pi install to provide those packages, pins its direct Pi validation dependencies to 0.99.2, and makes Pi hosts below 0.87.0 a setup failure through `pi-agent-browser-doctor`. There are no compatibility shims for older Pi releases.
106
+ Use Node 24.21.0 or newer and Pi 1.0.0 or newer. This package keeps optional Pi core imports as wildcard `peerDependencies` because Pi package docs require the host Pi install to provide those packages, pins its direct Pi validation dependencies to 1.0.0, and makes Pi hosts below 1.0.0 a setup failure through `pi-agent-browser-doctor`. There are no compatibility shims for older Pi releases.
107
107
 
108
108
  Install upstream `agent-browser` first and make sure it is on `PATH`:
109
109
 
@@ -172,7 +172,7 @@ pi --no-extensions -e npm:pi-agent-browser-native
172
172
 
173
173
  `--no-extensions` disables automatic extension loading, not Pi settings, configured package resolution, skills, prompts, themes, or context files.
174
174
 
175
- Pi 0.84.0+ may ask whether to trust projects with trust-gated settings or resources. This extension follows Pi's trust decision when loading its project-local config. `--no-approve` skips that config and Pi's trust-gated project resources; context files such as `AGENTS.md` still load unless context loading is separately disabled.
175
+ Pi may ask whether to trust projects with trust-gated settings or resources. This extension follows Pi's trust decision when loading its project-local config. `--no-approve` skips that config and Pi's trust-gated project resources; context files such as `AGENTS.md` still load unless context loading is separately disabled.
176
176
 
177
177
  For a specific published version:
178
178
 
@@ -286,7 +286,7 @@ printf '%s' "$EXA_API_KEY" | npm exec --yes --package pi-agent-browser-native@la
286
286
  npm exec --yes --package pi-agent-browser-native@latest -- pi-agent-browser-config web-search set-command "op read 'op://Private/Brave Search/API Key'" --provider brave --global
287
287
  ```
288
288
 
289
- Config merges in this order: global → project → `PI_AGENT_BROWSER_CONFIG` override. Under Pi 0.84.0+, the globally installed or CLI-loaded extension still loads project-local `.pi/config/pi-agent-browser-native/config.json` when Pi trust allows that project layer; it skips that project layer when Pi reports the project is untrusted or when Pi is launched with `--no-approve`. `webSearch.enabled` is evaluated after the loaded layers merge. Use `web-search disable --global` for a user default, `web-search disable --project` for one repo, and a `PI_AGENT_BROWSER_CONFIG` override with `{ "webSearch": { "enabled": false } }` when web search must stay off even if project config exists. Loaded config may use plaintext, custom environment aliases, interpolation literals, malformed-or-late-bound `$` values, and `!command` credential sources; the resolved secret is passed to the provider request while tool content, details, status output, and docs examples stay redacted. `web-search set-key`, `set-command`, and `clear` require `--provider`; `set-env` infers Exa/Brave from `EXA_API_KEY` or `BRAVE_API_KEY` unless you pass `--provider`.
289
+ Config merges in this order: global → project → `PI_AGENT_BROWSER_CONFIG` override. The globally installed or CLI-loaded extension still loads project-local `.pi/config/pi-agent-browser-native/config.json` when Pi trust allows that project layer; it skips that project layer when Pi reports the project is untrusted or when Pi is launched with `--no-approve`. `webSearch.enabled` is evaluated after the loaded layers merge. Use `web-search disable --global` for a user default, `web-search disable --project` for one repo, and a `PI_AGENT_BROWSER_CONFIG` override with `{ "webSearch": { "enabled": false } }` when web search must stay off even if project config exists. Loaded config may use plaintext, custom environment aliases, interpolation literals, malformed-or-late-bound `$` values, and `!command` credential sources; the resolved secret is passed to the provider request while tool content, details, status output, and docs examples stay redacted. `web-search set-key`, `set-command`, and `clear` require `--provider`; `set-env` infers Exa/Brave from `EXA_API_KEY` or `BRAVE_API_KEY` unless you pass `--provider`.
290
290
 
291
291
  For Exa, the effective mode is the per-call `searchType`, then `webSearch.defaultSearchType`, then `auto`. A research-heavy coding workflow should set the config default to `deep-lite`; callers can still override it per search. Users who do not opt in keep the existing `auto` latency.
292
292
 
@@ -550,9 +550,9 @@ Policy-required URL reads keep the two-call read → confirm/deny flow. Returned
550
550
 
551
551
  Full live identity, browser-independent native reads/confirmations and detailed recording receipts require the companion upstream fixes; the current recommended release does not yet supply all of them. Older supported versions remain accepted with unavailable evidence marked unknown. The extension does not upgrade or restart your installed browser to obtain these fields.
552
552
 
553
- ### Native working-session checkpoints
553
+ ### Lifecycle and cleanup
554
554
 
555
- On Pi hosts with `session_checkpoint`, an unused or cleanly closed browser integration can qualify for sleep. Live browsers, attachments, active code calls, unfinished recordings/legacy cleanup leases, and dirty recording persistence keep compute alive. Pi core owns native journal flush/repair and strict restore; an ongoing journal I/O failure rejects checkpoint acquisition. Checkpointing never closes browsers or changes cleanup ownership. Native save-on-close and per-root restore remain the persistence path; page JavaScript, unsaved forms, and live recordings are not serialized. See [checkpoint boundaries](docs/ARCHITECTURE.md#native-checkpoint-boundary).
555
+ Shared root and caller-owned browsers survive Pi quit; managed/fresh browsers and wrapper-launched Electron resources retain ownership-aware cleanup. Close or clean up owned resources explicitly before disabling or removing the extension. Reload after extension removal has no browser handover guarantee. Native save-on-close and per-root restore remain the persistence path; page JavaScript, unsaved forms, and live recordings are not serialized. See [session ownership](docs/ARCHITECTURE.md#session-model).
556
556
 
557
557
  ### Profile selection
558
558
 
@@ -702,7 +702,7 @@ npm run verify -- release
702
702
  `pi-agent-browser-native` is intentionally thin:
703
703
 
704
704
  1. Pi loads the compiled `dist/extensions/agent-browser/index.js` entrypoint from the package manifest; TypeScript under `extensions/` remains the source of truth and `npm run build` regenerates `dist/` before packing.
705
- 2. The extension registers compact direct/code tools, an advanced-tool loader with five initially inactive specialized tools, and the optional credential-backed `agent_browser_web_search` companion.
705
+ 2. The extension registers compact direct/code tools, an advanced-tool loader with five native default-inactive specialized tools (unless selected by the host), and the optional credential-backed `agent_browser_web_search` companion.
706
706
  3. Tool calls are translated into upstream `agent-browser` CLI invocations with controlled args, stdin, environment, timeout, and session planning.
707
707
  4. Upstream JSON/plain-text output is parsed into model-friendly content and structured details.
708
708
  5. Screenshots, downloads, recordings, traces, profiles, and spill files are normalized as Pi-visible artifacts where possible.
@@ -729,7 +729,7 @@ Install upstream `agent-browser`, then install dependencies:
729
729
  npm install
730
730
  ```
731
731
 
732
- Use the npm version declared in `package.json` `packageManager` when refreshing `package-lock.json` (for example `npx -y npm@12.1.0 install`) so optional-platform lockfile metadata does not drift. Use Node 24.21.0 and Pi 0.87.0 or newer for lifecycle and interactive browser smokes; the pinned Pi 0.99.2 devDependencies are validation fixtures, not an exact-version requirement for the host CLI. See [Environment and automation pitfalls](docs/RELEASE.md#environment-and-automation-pitfalls) in `docs/RELEASE.md`.
732
+ Use the npm version declared in `package.json` `packageManager` when refreshing `package-lock.json` (for example `npx -y npm@12.1.0 install`) so optional-platform lockfile metadata does not drift. Use Node 24.21.0 and Pi 1.0.0 or newer for lifecycle and interactive browser smokes; the pinned Pi 1.0.0 devDependencies are validation fixtures, not an exact-version requirement for the host CLI. See [Environment and automation pitfalls](docs/RELEASE.md#environment-and-automation-pitfalls) in `docs/RELEASE.md`.
733
733
 
734
734
  Checkout-only extension smoke test:
735
735
 
@@ -747,7 +747,7 @@ Configured-source lifecycle validation:
747
747
  npm run verify -- lifecycle
748
748
  ```
749
749
 
750
- The harness defaults to Pi model `zai/glm-5.2` and **180000 ms** per-step tmux waits; pass `--model <id>` and/or `--timeout-ms <ms>` after `lifecycle` when you need different settings (see [Configured-source lifecycle validation](docs/RELEASE.md#configured-source-lifecycle-validation) in `docs/RELEASE.md`). It launches the supported Pi runtime with `--approve` and a deterministic `--session-id`, drives `/reload`, closes Pi, relaunches the exact same session, asserts the JSONL header id, and checks managed-session continuity, compiled-entrypoint pickup after process restart, persisted spill reachability, and real Pi `tool_result` failure-patch behavior.
750
+ The harness defaults to Pi model `zai/glm-5.2` and **180000 ms** per-step tmux waits; pass `--model <id>` and/or `--timeout-ms <ms>` after `lifecycle` when you need different settings (see [Configured-source lifecycle validation](docs/RELEASE.md#configured-source-lifecycle-validation) in `docs/RELEASE.md`). It launches the supported Pi runtime with `--approve` and a deterministic `--session-id`, drives `/reload`, closes Pi, relaunches the exact same session, asserts the JSONL header id, and checks managed-session continuity, compiled-entrypoint pickup after process restart, persisted spill reachability, and native returned failure status and JSON preservation.
751
751
 
752
752
  Use lifecycle validation when testing `/reload`, exact-session relaunch, `/resume`, managed-session continuity, or persisted artifact behavior. Branch-backed state and `session_tree` cleanup ownership are covered by focused extension harness tests. Maintainers must run the lifecycle harness before every publish; see [Pre-release checks](docs/RELEASE.md#pre-release-checks).
753
753
 
@@ -1,7 +1,7 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { existsSync, readFileSync } from "node:fs";
4
- import { dirname, join } from "node:path";
4
+ import { dirname, isAbsolute, join } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { Text } from "@earendil-works/pi-tui";
7
7
  import { batchHasSuccessfulCloseAll, getSuccessfulBatchCloseLifecycle } from "./lib/batch-lifecycle.js";
@@ -13,17 +13,17 @@ import { buildExecutionPlan, canUseHeadlessCompatibilityUserAgent, createEphemer
13
13
  import { deleteIdentityKeysInNamespace, extractExplicitNamespace, extractExplicitSessionName, getAgentBrowserSessionIdentityKey, isAgentBrowserSessionIdentityKeyInNamespace, isUpstreamEnvFlagEnabled, resolveAgentBrowserNamespace, scanUpstreamGlobalFlagOccurrences } from "./lib/argv-grammar.js";
14
14
  import { parseArgvDescriptor } from "./lib/argv-descriptor.js";
15
15
  import { needsManagedSession } from "./lib/command-policy.js";
16
- import { ManagedSessionRestoreState, resolveOwnedManagedSessionContext, withOwnedManagedSessionContext } from "./lib/managed-session-restore.js";
16
+ import { ManagedSessionRestoreState } from "./lib/managed-session-restore.js";
17
17
  import { isRecord } from "./lib/parsing.js";
18
18
  import { getAgentBrowserProcessTimeoutMs, resolveAgentBrowserSocketDir, runAgentBrowserProcess } from "./lib/process.js";
19
19
  import { getCommandAwareProcessTimeoutMs } from "./lib/orchestration/browser-run/prepare/wait-timeouts.js";
20
20
  import { getAgentBrowserProcessEnvironment, withAgentBrowserProcessEnvironment, withIsolatedAgentBrowserEnvironment } from "./lib/process-environment.js";
21
21
  import { withNativeSessionDefaults } from "./lib/orchestration/native-session-defaults.js";
22
- import { getBrowserCwdError, resolveExecutionCwd, restoreManagedSessionCwd } from "./lib/execution-cwd.js";
22
+ import { getBrowserCwdError, resolveExecutionCwd } from "./lib/execution-cwd.js";
23
23
  import { resolveOperationPaths } from "./lib/orchestration/operation-paths.js";
24
24
  import { closeManagedSession, inspectManagedSessionDaemon } from "./lib/orchestration/browser-run/managed-session-daemon-policy.js";
25
25
  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";
26
+ import { buildPromptPolicy, getLatestUserMessage, getMessageText } from "./lib/prompt-policy.js";
27
27
  import { isCloseAllCommand, isCloseCommand } from "./lib/command-taxonomy.js";
28
28
  import { hasLaunchScopedFlagToken } from "./lib/launch-scoped-flags.js";
29
29
  import { cleanupSecureTempArtifacts } from "./lib/temp.js";
@@ -44,7 +44,7 @@ import { appendRecordingReservationTransition, applyRecordingArtifactsToReservat
44
44
  import { createAgentBrowserWebSearchTool } from "./lib/web-search.js";
45
45
  import { scopeReadConfirmationArgs } from "./lib/read-confirmation.js";
46
46
  import { isDirectAgentBrowserBashAllowed, isHarmlessAgentBrowserInspectionCommand, looksLikeDirectAgentBrowserBash, } from "./lib/bash-guard.js";
47
- import { AgentBrowserResultComponent, buildAgentBrowserToolResultPatch, formatAgentBrowserRenderCall, formatAgentBrowserRenderResult, } from "./lib/pi-tool-rendering.js";
47
+ import { AgentBrowserResultComponent, formatAgentBrowserRenderCall, } from "./lib/pi-tool-rendering.js";
48
48
  function browserExecutionFailure(error, signal, fallbackCategory = "upstream-error") {
49
49
  const timeout = error instanceof Error && error.name === "TimeoutError";
50
50
  const summary = redactSensitiveText(error instanceof Error ? error.message : String(error));
@@ -230,84 +230,6 @@ function getRecognizedCompatibilityWorkaround(value) {
230
230
  ? { id: workaround.id, reason: workaround.reason }
231
231
  : undefined;
232
232
  }
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
233
  function getToolResultArgs(details) {
312
234
  if (Array.isArray(details.args) && details.args.every((arg) => typeof arg === "string"))
313
235
  return details.args;
@@ -385,18 +307,6 @@ function trackOwnedManagedSession(sessions, sessionName, cwd, options = {}) {
385
307
  const headedManagedAutosaveInterval = options.headedManagedAutosaveInterval ?? existing?.headedManagedAutosaveInterval;
386
308
  sessions.set(key, { branchOwned, compatibilityWorkaround, cwd, headedManagedAutosaveDisabled, headedManagedAutosaveInterval, namespace: options.namespace, sessionName, socketDir: options.socketDir ?? existing?.socketDir });
387
309
  }
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
310
  function untrackOwnedManagedSession(sessions, sessionName, namespace) {
401
311
  if (!sessionName)
402
312
  return;
@@ -590,6 +500,7 @@ function collectBranchManagedResourceEvents(branch) {
590
500
  managedSessionActiveIdentities: new Map(),
591
501
  managedSessionActiveRanks: new Map(),
592
502
  managedSessionCloseRanks: new Map(),
503
+ managedSessionLaunchState: new Map(),
593
504
  };
594
505
  let eventRank = 0;
595
506
  for (const entry of branch) {
@@ -611,6 +522,32 @@ function collectBranchManagedResourceEvents(branch) {
611
522
  const batchCloseLifecycle = getSuccessfulBatchCloseLifecycle(details.batchSteps);
612
523
  const closeAllApplied = detailsReportCloseAllApplied(details, succeeded);
613
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
+ }
614
551
  const outcomeSucceeded = outcome?.succeeded === true;
615
552
  const outcomeStatus = typeof outcome?.status === "string" ? outcome.status : undefined;
616
553
  const outcomeCurrentSessionName = typeof outcome?.currentSessionName === "string" ? outcome.currentSessionName : undefined;
@@ -858,7 +795,7 @@ function hasArgvFlag(argv, longFlag, shortFlag) {
858
795
  function shouldIncludeProjectConfig(ctx, argv = process.argv) {
859
796
  if (hasArgvFlag(argv, "--no-approve", "-na"))
860
797
  return false;
861
- return ctx?.isProjectTrusted?.() ?? true;
798
+ return ctx.isProjectTrusted();
862
799
  }
863
800
  export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
864
801
  const ephemeralSessionSeed = createEphemeralSessionSeed();
@@ -868,6 +805,21 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
868
805
  });
869
806
  const implicitSessionIdleTimeoutMs = String(getImplicitSessionIdleTimeoutMs());
870
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
+ };
871
823
  let webSearchToolRegistered = false;
872
824
  let managedSessionActive = false;
873
825
  let managedSessionBaseName = createImplicitSessionName(undefined, process.cwd(), ephemeralSessionSeed);
@@ -1121,11 +1073,11 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1121
1073
  clearSessionScopedBrowserState(sessionName, AGENT_BROWSER_SCRIPT_NAMESPACE);
1122
1074
  return undefined;
1123
1075
  };
1124
- const recoverScriptSessionLeasesWithinQueue = async (ctx) => {
1076
+ const recoverScriptSessionLeasesWithinQueue = async (ctx, branch) => {
1125
1077
  const pendingSessionNames = new Set([...ownedManagedSessions.values()]
1126
1078
  .map((session) => session.sessionName)
1127
1079
  .filter(isAgentBrowserScriptSessionName));
1128
- for (const lease of getScriptSessionLeasesFromBranch(await readBrowserEntries(ctx.sessionManager), ctx.sessionManager.getSessionId()).values()) {
1080
+ for (const lease of getScriptSessionLeasesFromBranch(branch, ctx.sessionManager.getSessionId()).values()) {
1129
1081
  if (lease.cleanup !== "closed")
1130
1082
  pendingSessionNames.add(lease.sessionName);
1131
1083
  }
@@ -1138,6 +1090,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1138
1090
  const restoreBranchBackedState = async (ctx, options) => {
1139
1091
  branchRestoreGeneration += 1;
1140
1092
  branchStateGeneration += 1;
1093
+ restorePromptState(ctx);
1141
1094
  const previousManagedSessionActive = managedSessionActive;
1142
1095
  const previousManagedSessionName = managedSessionName;
1143
1096
  const previousFreshSessionOrdinal = freshSessionOrdinal;
@@ -1179,15 +1132,11 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1179
1132
  : createFreshSessionName(managedSessionBaseName, ephemeralSessionSeed, nextFreshSessionOrdinal)
1180
1133
  : restoredState.sessionName;
1181
1134
  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);
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;
1191
1140
  freshSessionOrdinal = nextFreshSessionOrdinal;
1192
1141
  sessionPageState = SessionPageState.fromBranch(branch);
1193
1142
  traceOwners = new Map();
@@ -1238,13 +1187,14 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1238
1187
  continue;
1239
1188
  if (!isRestorableManagedSessionName(identity.sessionName, managedSessionBaseName))
1240
1189
  continue;
1241
- 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, {
1242
1192
  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),
1193
+ compatibilityWorkaround: launch?.compatibilityWorkaround,
1194
+ headedManagedAutosaveDisabled: launch?.headedManagedAutosaveDisabled ?? false,
1195
+ headedManagedAutosaveInterval: launch?.headedManagedAutosaveInterval,
1246
1196
  namespace: identity.namespace,
1247
- socketDir: restoreManagedSessionSocketDir(branch, identity.sessionName, identity.namespace),
1197
+ socketDir: launch?.socketDir,
1248
1198
  });
1249
1199
  }
1250
1200
  if (restoredState.active) {
@@ -1254,7 +1204,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1254
1204
  headedManagedAutosaveDisabled: managedSessionHeadedAutosaveDisabled,
1255
1205
  headedManagedAutosaveInterval: managedSessionHeadedAutosaveInterval,
1256
1206
  namespace: restoredState.namespace,
1257
- socketDir: restoreManagedSessionSocketDir(branch, restoredState.sessionName, restoredState.namespace),
1207
+ socketDir: currentLaunch?.socketDir,
1258
1208
  });
1259
1209
  }
1260
1210
  const pendingOwnedBegins = new Map();
@@ -1290,11 +1240,12 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1290
1240
  const closeRank = branchResourceEvents.managedSessionCloseRanks.get(sessionKey);
1291
1241
  if (activeRank === undefined || (closeRank !== undefined && closeRank >= activeRank))
1292
1242
  continue;
1293
- 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, {
1294
1245
  branchOwned: true,
1295
- headedManagedAutosaveDisabled: restoreManagedSessionHeadedAutosaveDisabledFromBranch(branch, record.sessionName),
1296
- headedManagedAutosaveInterval: restoreManagedSessionHeadedAutosaveIntervalFromBranch(branch, record.sessionName),
1297
- socketDir: restoreManagedSessionSocketDir(branch, record.sessionName),
1246
+ headedManagedAutosaveDisabled: launch?.headedManagedAutosaveDisabled ?? false,
1247
+ headedManagedAutosaveInterval: launch?.headedManagedAutosaveInterval,
1248
+ socketDir: launch?.socketDir,
1298
1249
  });
1299
1250
  }
1300
1251
  mergeActiveElectronLaunchRecords(ownedElectronLaunchRecords, new Map([...electronLaunchRecords].filter(([, record]) => record.ownerSessionId === browserOwnerSessionId)), {
@@ -1323,6 +1274,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1323
1274
  attachedSessionKeys.add(sessionKey);
1324
1275
  }
1325
1276
  }
1277
+ return branch;
1326
1278
  };
1327
1279
  const registerWebSearchToolIfAvailable = (configState) => {
1328
1280
  if (webSearchToolRegistered || !canRegisterWebSearchTool(configState))
@@ -1339,15 +1291,19 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1339
1291
  });
1340
1292
  webSearchToolRegistered = true;
1341
1293
  };
1294
+ pi.on("message_end", (event, ctx) => {
1295
+ if (event.message.role === "user")
1296
+ promptState = { sessionId: ctx.sessionManager.getSessionId(), message: event.message };
1297
+ });
1342
1298
  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
1299
  await managedSessionExecutionQueue.run(() => artifactExecutionQueue.run(async () => {
1350
- 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);
1351
1307
  flushRecordingReservations();
1352
1308
  notifyRecordingPersistence(ctx);
1353
1309
  }));
@@ -1358,87 +1314,12 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1358
1314
  controller.abort();
1359
1315
  await Promise.allSettled([...activeScriptExecutions]);
1360
1316
  await managedSessionExecutionQueue.run(() => artifactExecutionQueue.run(async () => {
1361
- await restoreBranchBackedState(ctx, { resetRuntimeOwnership: false });
1362
- await recoverScriptSessionLeasesWithinQueue(ctx);
1317
+ const branch = await restoreBranchBackedState(ctx, { resetRuntimeOwnership: false });
1318
+ await recoverScriptSessionLeasesWithinQueue(ctx, branch);
1363
1319
  flushRecordingReservations();
1364
1320
  notifyRecordingPersistence(ctx);
1365
1321
  }));
1366
1322
  });
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
1323
  pi.on("session_shutdown", async (event, ctx) => {
1443
1324
  for (const controller of activeScriptControllers)
1444
1325
  controller.abort();
@@ -1548,11 +1429,11 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1548
1429
  event.systemPromptOptions.sections.agent_browser = fullInstructions(ctx);
1549
1430
  });
1550
1431
  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) &&
1432
+ if (!isBashToolCallEvent(event) || !looksLikeDirectAgentBrowserBash(event.input.command)
1433
+ || isHarmlessAgentBrowserInspectionCommand(event.input.command))
1434
+ return;
1435
+ const promptPolicy = getPromptPolicy(ctx);
1436
+ if (!promptPolicy.allowLegacyAgentBrowserBash &&
1556
1437
  !(await isDirectAgentBrowserBashAllowed(ctx.cwd))) {
1557
1438
  return {
1558
1439
  block: true,
@@ -1560,7 +1441,6 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1560
1441
  };
1561
1442
  }
1562
1443
  });
1563
- pi.on("tool_result", async (event) => buildAgentBrowserToolResultPatch(event));
1564
1444
  let emptyBrowserBranch;
1565
1445
  const captureCurrentBrowserBranch = (ctx) => {
1566
1446
  const generation = branchRestoreGeneration;
@@ -1583,7 +1463,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1583
1463
  const component = context.lastComponent instanceof AgentBrowserResultComponent
1584
1464
  ? context.lastComponent
1585
1465
  : new AgentBrowserResultComponent();
1586
- component.setState(formatAgentBrowserRenderResult(result, options, theme, context.isError), options.expanded, theme);
1466
+ component.setResult(result, options, theme, context.isError);
1587
1467
  return component;
1588
1468
  },
1589
1469
  async execute(toolCallId, params, signal, onUpdate, ctx, nativeToolCallId = toolCallId, capturedCwd, modelVisible = true, commandIndex, selectedBranch) {
@@ -1595,7 +1475,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1595
1475
  catch (error) {
1596
1476
  return buildValidationFailureResult({ kind: "invalid", status: "invalid", redactedArgs: [], toolArgs: [], validationError: error instanceof Error ? error.message : String(error) });
1597
1477
  }
1598
- const promptPolicy = buildPromptPolicy(getLatestUserPrompt(ctx.sessionManager.getBranch()));
1478
+ const promptPolicy = getPromptPolicy(ctx);
1599
1479
  const outputPath = isRecord(params) && typeof params.outputPath === "string" ? params.outputPath : undefined;
1600
1480
  const admittedInput = resolveAgentBrowserInput({
1601
1481
  getBatchPreflightValidationError: (args, stdin) => getArtifactPreflightValidationError({ args, cwd: operationCwd, outputPath, stdin }),
@@ -2266,10 +2146,10 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
2266
2146
  }) };
2267
2147
  const rendered = await renderAgentBrowserObservation(renderOptions);
2268
2148
  if (!priorManifest && !rendered.artifactManifest)
2269
- return { ...result, content: rendered.content, details: { ...details, browserEventVersion: 1 } };
2149
+ return { ...result, content: rendered.content, structuredContent: rendered.structuredContent, details: { ...details, browserEventVersion: 1 } };
2270
2150
  return artifactExecutionQueue.run(async () => {
2271
2151
  if (!branch.isCurrent())
2272
- return { ...result, content: rendered.content, details: { ...details, browserEventVersion: 1, artifactManifest: rendered.artifactManifest } };
2152
+ return { ...result, content: rendered.content, structuredContent: rendered.structuredContent, details: { ...details, browserEventVersion: 1, artifactManifest: rendered.artifactManifest } };
2273
2153
  const before = artifactManifest;
2274
2154
  const merged = mergeBrowserRunArtifactManifest(artifactManifest, priorManifest, rendered.artifactManifest);
2275
2155
  const changes = artifactChanges(before, merged);
@@ -2277,10 +2157,10 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
2277
2157
  const record = { event: { version: 1, phase: "state", operationId: randomUUID(), toolCallId: "observation", commandIndex: 0, isError: result.isError === true, state: {}, artifacts: changes } };
2278
2158
  if (!await appendBrowserRecord(ctx.sessionManager, () => appendBrowserTransition(pi, record), record, branch)
2279
2159
  || !branch.isCurrent())
2280
- return { ...result, content: rendered.content, details: { ...details, browserEventVersion: 1, artifactManifest: rendered.artifactManifest } };
2160
+ return { ...result, content: rendered.content, structuredContent: rendered.structuredContent, details: { ...details, browserEventVersion: 1, artifactManifest: rendered.artifactManifest } };
2281
2161
  }
2282
2162
  artifactManifest = mergeBrowserRunArtifactManifest(artifactManifest, priorManifest, rendered.artifactManifest);
2283
- return { ...result, content: rendered.content, details: { ...details, browserEventVersion: 1, artifactManifest: invocationArtifactManifest(artifactManifest, before, details, priorManifest) } };
2163
+ return { ...result, content: rendered.content, structuredContent: rendered.structuredContent, details: { ...details, browserEventVersion: 1, artifactManifest: invocationArtifactManifest(artifactManifest, before, details, priorManifest) } };
2284
2164
  });
2285
2165
  };
2286
2166
  registerAgentBrowserToolSurface(pi, {