pi-agent-browser-native 0.7.1 → 0.8.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,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.0 - 2026-09-26
4
+
5
+ ### Breaking Changes
6
+
7
+ - Require Node.js 24.21.0 or newer.
8
+
9
+ ### Changed
10
+
11
+ - Update the official Pi validation graph to 0.87.1, adopt TypeScript 7, npm 12, and the latest compatible development tooling, and remove stale transitive dependency overrides.
12
+ - Qualify official Pi, the forked Pi host, package smoke, native checkpoints, and platform builds on Node 24.
13
+
14
+ ### Fixed
15
+
16
+ - Keep lifecycle sentinel replacement byte-stable outside its generated block without retaining the removed TypeScript compiler API.
17
+
3
18
  ## 0.7.1 - 2026-09-22
4
19
 
5
20
  ### Fixed
package/README.md CHANGED
@@ -6,7 +6,7 @@ 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.87.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. 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 **0.87.1**, 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.
10
10
 
11
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.
12
12
 
@@ -84,7 +84,7 @@ The result is optimized for agent work:
84
84
  | Agents build fragile shell commands or repeat browser calls for loops and branches | Exposes compact `agent_browser` for native commands and batch, `agent_browser_code` for fresh JavaScript against the same persistent browser, and discoverable action, QA, Electron, and source tools | `extensions/agent-browser/index.ts`, `extensions/agent-browser/lib/input-modes/`, [`docs/TOOL_CONTRACT.md`](docs/TOOL_CONTRACT.md) |
85
85
  | Page snapshots are too large or viewport-blind | Shows compact, main-content-first summaries, surfaces an `Omitted high-value controls` section (plus `details.data.highValueControlRefIds`) when dense pages or desktop host screens hide editables, named surfaces/tabs, primary action buttons, and high-signal named links such as repository results from the trimmed ref lists, supports wrapper-side `snapshot -i --search <text>` / `--filter role=<role>` to trim dense pages while preserving full `details.refSnapshot`, supports `snapshot --viewport` for scroll/viewport metadata, supports `snapshot --diff` for quick ref-map deltas versus the prior tracked snapshot, and stores full redacted output in spill files when needed | `extensions/agent-browser/lib/results/snapshot.ts`, `extensions/agent-browser/lib/orchestration/browser-run/prepare.ts`, `test/agent-browser.presentation.test.ts`, `test/agent-browser.extension-validation.test.ts` |
86
86
  | Screenshots/downloads get lost in text | Normalizes artifact paths, creates missing parent directories, leaves click/download execution to native download, and reports existence, size, cwd, session, and repair status | [`docs/COMMAND_REFERENCE.md`](docs/COMMAND_REFERENCE.md#download-screenshot-and-pdf-files) |
87
- | Profile restores and tab drift confuse agents | Tracks managed sessions, keeps every upstream helper probe on the same idle-timeout launch configuration so the background browser is not restarted between a snapshot and action, re-selects target tabs after observed drift, live-verifies the active target and refreshes its title after successful tab selection or `tab close` (while retaining deliberate `about:blank` selections and post-close blank targets), rehydrates branch-backed session state on Pi session-tree changes, and pins later commands only for sessions with drift/restored-session risk | generated tab-recovery notes below; `test/agent-browser.extension-tab-recovery.test.ts` (drift and about:blank recovery), `test/agent-browser.extension-tabs.test.ts` (post-close target), `test/agent-browser.extension-ref-guards.test.ts` (snapshot/action environment and session-tree rehydration), `test/agent-browser.resume-state.test.ts` (persisted session / resume planning) |
87
+ | Profile restores and tab drift confuse agents | Tracks managed sessions, keeps every upstream helper probe on the same idle-timeout launch configuration so the background browser is not restarted between a snapshot and action, retains native CDP target IDs so same-address tabs remain distinct after resume, re-selects target tabs after observed drift, live-verifies the active target and refreshes its title after successful tab selection or `tab close` (while retaining new blank tabs, deliberate `about:blank` selections, and post-close blank targets, including batch transitions), rehydrates branch-backed session state on Pi session-tree changes, and pins later commands only for sessions with drift/restored-session risk | generated tab-recovery notes below; `test/agent-browser.extension-tab-recovery.test.ts` (drift and about:blank recovery), `test/agent-browser.extension-tabs.test.ts` (post-close target), `test/agent-browser.extension-ref-guards.test.ts` (snapshot/action environment and session-tree rehydration), `test/agent-browser.resume-state.test.ts` (persisted session / resume planning) |
88
88
  | 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` |
89
89
  | 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` |
90
90
  | Stale `@eN` refs fail mysteriously | Records per-session `details.refSnapshot`, 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` |
@@ -102,7 +102,7 @@ Artifact results show known requested paths separately from reported/resolved lo
102
102
 
103
103
  ## Fastest way to try it
104
104
 
105
- Use 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.87.0, and makes hosts below 0.87.0 a setup failure through `pi-agent-browser-doctor`. There are no compatibility shims for older Pi releases.
105
+ 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.87.1, 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
106
 
107
107
  Install upstream `agent-browser` first and make sure it is on `PATH`:
108
108
 
@@ -492,7 +492,7 @@ Use native `batch --bail` for known sequences. Add an explicit destination/text
492
492
  }
493
493
  ```
494
494
 
495
- Use the exact returned recovery payload after errors. Timeout partial progress distinguishes observed results from planned or unknown steps; it never proves an unobserved mutation did not run. For paced typing, native batch can interleave `keyboard type` characters and positional `wait <ms>` rows; see [the migration map](docs/TOOL_CONTRACT.md#07-migration). Do not assume a native `--delay` option reproduces the removed job behavior.
495
+ Use the exact returned recovery payload after errors. Timeout partial progress reports current page and file observations separately from planned steps, whose outcomes remain unknown without upstream results. An existing artifact or matching URL does not prove any step ran. Multi-step timeouts offer inspection rather than guessing a retry position; a single retryable step may offer a retry candidate after inspection. For paced typing, native batch can interleave `keyboard type` characters and positional `wait <ms>` rows; see [the migration map](docs/TOOL_CONTRACT.md#07-migration). Do not assume a native `--delay` option reproduces the removed job behavior.
496
496
 
497
497
  Both JSON and prose results expose a bounded canonical observation with recovery actions and artifact receipts. Oversized observations name an `observationPath` for the complete redacted result; audit-only `details` is not the model's sole recovery channel. Inner code calls avoid full model rendering, spills, and image encoding until output is selected.
498
498
 
@@ -720,7 +720,7 @@ Install upstream `agent-browser`, then install dependencies:
720
720
  npm install
721
721
  ```
722
722
 
723
- Use the npm version declared in `package.json` `packageManager` when refreshing `package-lock.json` (for example `npx -y npm@11.14.0 install`) so optional-platform lockfile metadata does not drift. Use Pi 0.87.0 or newer for lifecycle and interactive browser smokes; the pinned Pi 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`.
723
+ 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.87.1 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`.
724
724
 
725
725
  Checkout-only extension smoke test:
726
726
 
@@ -768,7 +768,7 @@ These calls return plain text and stay stateless: the extension does not inject
768
768
  - After open/goto/navigate calls with --profile, --restore, --session-name, or --state, agent_browser best-effort re-selects the tab whose URL matches the returned page when restored tabs steal focus during launch or reconnect.
769
769
  - After confirmed shutdown of an automatically restored managed session, the wrapper retains its complete recorded URL, including the fragment, until the first current-page operation (including get url and reload). Non-page calls such as tab list may start a daemon without fulfilling that reopen; explicit URL reads leave the managed browser and pending reopen untouched. The wrapper uses native open once, verifies the observed tab, and discards old refs/frame scope; it does not restore unsaved forms, JavaScript memory, or history. Explicit navigation, caller-owned/attached sessions, and restore-disabled sessions are not auto-reopened.
770
770
  - For a still-live browser after tab drift or resume, the wrapper verifies/selects the intended tab before ref/semantic helpers and page commands; failed selection stops the call without navigating. Local commands, read <url>, URL a11y/vitals, diff url, window new, and explicit tab/navigation/connection/state recovery do not require the prior tab. Batch checks follow effective rows past non-page prefixes and stop at explicit context changes, preserving caller argv/stdin and continue-on-error behavior. Same-tab reselection is avoided because it clears refs. Use exact batch --bail for fail-fast, not --bail=<value>. Routine same-session calls skip tab-list preflights.
771
- - For sessions with observed tab-drift risk, after a successful command on a known target tab, agent_browser also best-effort restores that intended tab if a restored/background tab steals focus after the command completes. Routine same-session commands skip this post-command tab-list probe.
771
+ - For sessions with observed tab-drift risk, after a successful command on a known target tab, agent_browser also best-effort restores that intended tab if a restored/background tab steals focus after the command completes. Routine same-session commands skip focus correction; observed pages without a native targetId use tab list to establish their identity.
772
772
  - If a known session target unexpectedly reports about:blank, agent_browser best-effort re-selects the prior intended target when it still exists; if recovery fails, it records the observed about:blank target and reports exact recovery guidance instead of treating the prior page as active.
773
773
  - If upstream reports tab_gone, the pinned bound tab is gone; use details.nextActions (tab list / tab new) instead of assuming another tab is yours.
774
774
  <!-- agent-browser-playbook:end wrapper-tab-recovery -->
@@ -784,81 +784,6 @@ function getTimeoutStepRetry(step) {
784
784
  const command = step.args[0];
785
785
  return command && TIMEOUT_RETRYABLE_COMMANDS.has(command) ? { args: ["batch"], stdin: JSON.stringify([step.args]) } : undefined;
786
786
  }
787
- function normalizeUrlForTimeoutComparison(url) {
788
- if (!url)
789
- return undefined;
790
- try {
791
- return new URL(url);
792
- }
793
- catch {
794
- return undefined;
795
- }
796
- }
797
- function currentUrlMatchesNavigationStep(currentUrl, plannedUrl) {
798
- if (!currentUrl || !plannedUrl)
799
- return false;
800
- if (currentUrl === plannedUrl)
801
- return true;
802
- const current = normalizeUrlForTimeoutComparison(currentUrl);
803
- const planned = normalizeUrlForTimeoutComparison(plannedUrl);
804
- if (!current || !planned || current.origin !== planned.origin)
805
- return false;
806
- const plannedPath = planned.pathname.endsWith("/") ? planned.pathname : `${planned.pathname}/`;
807
- const currentPath = current.pathname.endsWith("/") ? current.pathname : `${current.pathname}/`;
808
- return planned.pathname === "/" || currentPath.startsWith(plannedPath);
809
- }
810
- function buildTimeoutProgressSteps(options) {
811
- let retryStep;
812
- let lastCompletedNavigationIndex;
813
- const progressSteps = options.steps.map((step) => {
814
- const stepArtifacts = options.artifacts.filter((artifact) => artifact.stepIndex === step.index);
815
- const command = step.args[0];
816
- const navigationUrl = isOpenNavigationCommand(command) || command === "pushstate" ? getLastPositionalToken(step.args) : undefined;
817
- if (stepArtifacts.some((artifact) => artifact.exists)) {
818
- return { ...step, reason: "Declared artifact exists on disk after timeout.", status: "completed" };
819
- }
820
- if (options.currentPageSource === "live" && currentUrlMatchesNavigationStep(options.currentPageUrl, navigationUrl)) {
821
- lastCompletedNavigationIndex = step.index;
822
- return { ...step, reason: "Live page URL was recovered after timeout.", status: "completed" };
823
- }
824
- return { ...step, reason: stepArtifacts.length > 0 ? "Declared artifact was not present when the watchdog fired." : undefined, status: "unknown" };
825
- });
826
- const highestCompletedIndex = Math.max(0, ...progressSteps.filter((step) => step.status === "completed").map((step) => step.index));
827
- for (const step of progressSteps) {
828
- if (step.status === "unknown" && step.index < highestCompletedIndex) {
829
- step.status = "completed";
830
- step.reason = "Later step completion evidence indicates the batch advanced past this step before timeout.";
831
- }
832
- }
833
- for (const step of progressSteps) {
834
- const command = step.args[0];
835
- if (step.status === "completed" && (isOpenNavigationCommand(command) || command === "pushstate")) {
836
- lastCompletedNavigationIndex = Math.max(lastCompletedNavigationIndex ?? 0, step.index);
837
- }
838
- }
839
- for (const step of progressSteps) {
840
- if (step.status === "completed")
841
- continue;
842
- if (!retryStep) {
843
- const retry = getTimeoutStepRetry(step);
844
- retryStep = {
845
- ...step,
846
- reason: step.reason ?? (retry ? "Likely active when the wrapper watchdog fired." : "Likely active when the wrapper watchdog fired; executable retry omitted because this step may have already mutated page state."),
847
- retry,
848
- status: "failed",
849
- };
850
- Object.assign(step, retryStep);
851
- continue;
852
- }
853
- step.status = "pending";
854
- step.reason = step.reason ?? `Pending behind timed-out step ${retryStep.index}.`;
855
- }
856
- return {
857
- openedButPostOpenTimedOut: lastCompletedNavigationIndex !== undefined && retryStep !== undefined && retryStep.index > lastCompletedNavigationIndex,
858
- retryStep,
859
- steps: progressSteps,
860
- };
861
- }
862
787
  export async function collectTimeoutPartialProgress(options) {
863
788
  if ((options.commandTokens[0] === "session" && options.commandTokens[1] === "info") || isBrowserIndependentRead(options.commandTokens, options.stdin))
864
789
  return undefined;
@@ -873,14 +798,16 @@ export async function collectTimeoutPartialProgress(options) {
873
798
  const plannedUrl = recoveredUrl ? undefined : getPlannedCurrentPageUrl(rawSteps);
874
799
  const url = recoveredUrl ?? plannedUrl;
875
800
  const currentPageSource = recoveredUrl ? "live" : plannedUrl ? "planned" : title ? "live" : undefined;
876
- const stepProgress = buildTimeoutProgressSteps({ artifacts, currentPageSource: recoveredUrl ? "live" : undefined, currentPageUrl: recoveredUrl, steps: rawSteps });
801
+ // A matching URL or existing file is current state, not an execution receipt.
802
+ const steps = rawSteps.map(step => ({ ...step, status: "unknown", reason: "Upstream did not report this step's outcome before timeout." }));
803
+ const singleStep = steps.length === 1 ? steps[0] : undefined;
804
+ const retry = singleStep ? getTimeoutStepRetry(singleStep) : undefined;
805
+ const retryStep = singleStep && retry ? { ...singleStep, retry } : undefined;
877
806
  if (rawSteps.length === 0 && artifacts.length === 0 && !url && !title)
878
807
  return undefined;
879
808
  const foundArtifacts = artifacts.filter((artifact) => artifact.exists).length;
880
- const completedSteps = stepProgress.steps.filter((step) => step.status === "completed").length;
881
809
  const pageStateSummary = recoveredUrl || title ? " and current page state" : plannedUrl ? " and planned page URL" : "";
882
- const retrySummary = stepProgress.retryStep ? ` Retry step ${stepProgress.retryStep.index} is the first incomplete step.` : "";
883
- return { artifacts, currentPage: url || title ? { source: currentPageSource, title, url } : undefined, liveUrlRecovered: recoveredUrl !== undefined, openedButPostOpenTimedOut: stepProgress.openedButPostOpenTimedOut, retryStep: stepProgress.retryStep, steps: stepProgress.steps.length > 0 ? stepProgress.steps : undefined, summary: `Timed out before upstream returned final results; recovered ${completedSteps}/${rawSteps.length} planned step state${rawSteps.length === 1 ? "" : "s"} and ${foundArtifacts}/${artifacts.length} declared artifact path${artifacts.length === 1 ? "" : "s"}${pageStateSummary}.${retrySummary}` };
810
+ return { artifacts, currentPage: url || title ? { source: currentPageSource, title, url } : undefined, liveUrlRecovered: recoveredUrl !== undefined, retryStep, steps: steps.length > 0 ? steps : undefined, summary: `Timed out before upstream returned final results; ${steps.length} planned step outcome${steps.length === 1 ? " is" : "s are"} unknown. Found ${foundArtifacts}/${artifacts.length} declared artifact path${artifacts.length === 1 ? "" : "s"}${pageStateSummary}; these observations do not prove step execution.` };
884
811
  }
885
812
  function redactSensitivePathSegmentsForDiagnostic(path) {
886
813
  return path.split(/([/\\]+)/).map((segment) => segment === "/" || segment === "\\" || /^[/\\]+$/.test(segment) ? segment : redactSensitiveText(segment) !== segment || /(?:secret|token|password|passwd|credential|auth|api[-_]?key|bearer)/i.test(segment) ? "[REDACTED]" : segment).join("");
@@ -934,7 +861,7 @@ export function formatTimeoutPartialProgressText(progress, pageTargetUnknown = f
934
861
  const payload = JSON.stringify({ ...progress.retryStep.retry, stdin: JSON.stringify([redactInvocationArgs(progress.retryStep.args)]) });
935
862
  lines.push(pageTargetUnknown
936
863
  ? `Retry candidate for step ${progress.retryStep.index}: ${payload}. Verify the current URL before running it.`
937
- : `Retry failed step: ${payload}`);
864
+ : `Retry candidate for step ${progress.retryStep.index} (outcome unknown): ${payload}`);
938
865
  }
939
866
  for (const artifact of progress.artifacts)
940
867
  lines.push(`Artifact from step ${artifact.stepIndex}: ${redactSensitivePathSegmentsForDiagnostic(artifact.path)} (${artifact.exists ? `exists${typeof artifact.sizeBytes === "number" ? `, ${artifact.sizeBytes} bytes` : ""}` : "missing"})`);
@@ -217,7 +217,7 @@ function buildTimeoutPartialProgressNextActions(options) {
217
217
  args: withOptionalSessionArgs(options.executionPlan.sessionName, ["batch", "--bail"]),
218
218
  stdin: JSON.stringify([["get", "url"], ["snapshot", "-i"]]),
219
219
  },
220
- reason: `Verify the current URL, then inspect the page after timeout${stepIndex === undefined ? "" : ` before resuming from incomplete step ${stepIndex}`}.`,
220
+ reason: `Verify the current URL, then inspect the page after timeout${stepIndex === undefined ? "" : ` before considering a retry of step ${stepIndex}`}.`,
221
221
  safety: "Fail-fast read-only recovery: snapshot runs only after get url succeeds, satisfying the wrapper page-target guard without trusting the planned URL.",
222
222
  tool: "agent_browser",
223
223
  }];
@@ -229,8 +229,8 @@ function buildTimeoutPartialProgressNextActions(options) {
229
229
  ? { ...retry, sessionMode: "fresh" }
230
230
  : { ...retry, args: withOptionalSessionArgs(options.executionPlan.sessionName, retry.args) },
231
231
  reason: freshSessionAbandoned
232
- ? `Retry the first incomplete timed-out step${stepIndex === undefined ? "" : ` ${stepIndex}`} in a fresh browser session because the timed-out fresh session was not proven live.`
233
- : `Retry the first incomplete timed-out step${stepIndex === undefined ? "" : ` ${stepIndex}`} against the current browser session.`,
232
+ ? `Consider retrying the single timed-out step${stepIndex === undefined ? "" : ` ${stepIndex}`} in a fresh browser session because the timed-out fresh session was not proven live.`
233
+ : `Consider retrying the single timed-out step${stepIndex === undefined ? "" : ` ${stepIndex}`} against the current browser session.`,
234
234
  safety: "Only read-only or idempotent timeout steps get executable retry args; inspect current page/artifact state before using the action.",
235
235
  tool: "agent_browser",
236
236
  }];
@@ -240,7 +240,7 @@ function buildTimeoutPartialProgressNextActions(options) {
240
240
  return [{
241
241
  id: "inspect-current-page-after-timeout",
242
242
  params: { args: withOptionalSessionArgs(options.executionPlan.sessionName, ["snapshot", "-i"]) },
243
- reason: `Inspect the current page after timeout before deciding how to resume${stepIndex === undefined ? "" : ` from incomplete step ${stepIndex}`}.`,
243
+ reason: `Inspect the current page after timeout before deciding how to resume${stepIndex === undefined ? "" : ` with step ${stepIndex}`}.`,
244
244
  safety: "Read details.timeoutPartialProgress first. Do not blindly retry mutating steps such as clicks, fills, key presses, selects, or checks; split the remaining flow into shorter batches around the next navigation or DOM mutation boundary.",
245
245
  tool: "agent_browser",
246
246
  }];
@@ -8,6 +8,7 @@ import { prepareAgentBrowserSpawnArgs, withChromeStartupArgs } from "../../proce
8
8
  import { cleanupElectronLaunchResources } from "../../electron/cleanup.js";
9
9
  import { launchElectronApp } from "../../electron/launch.js";
10
10
  import { pathExists } from "../../fs-utils.js";
11
+ import { isRecord } from "../../parsing.js";
11
12
  import { getCompiledSemanticActionSessionPrefix } from "../../input-modes/semantic-action.js";
12
13
  import { tryNetworkRequestsPageFilter } from "./prepare/network-page-filter.js";
13
14
  import { tryContainerScroll, tryPageScrollTo } from "./prepare/scroll-shims.js";
@@ -599,14 +600,20 @@ export async function prepareBrowserRun(options) {
599
600
  });
600
601
  if (!executionPlan.validationError && !executionPlan.plainTextInspection && !knownStaleRef && !invalidStdin && priorSessionTabTarget && pinSessionTab) {
601
602
  signal?.throwIfAborted();
602
- const reopened = !coldManagedSession || await runSessionCommandData({
603
+ const reopenedData = coldManagedSession ? await runSessionCommandData({
603
604
  args: ["open", priorSessionTabTarget.url], cwd, namespace: executionPlan.namespace, sessionName: executionPlan.sessionName, signal, timeoutMs: params.timeoutMs,
604
605
  onProcessResult: ({ agentBrowserStarted }) => {
605
606
  // A started open may have navigated even if its CLI was aborted before replying.
606
607
  if (agentBrowserStarted && sessionStateKey)
607
608
  sessionPageState.setTabReopenPending({ pending: false, sessionName: sessionStateKey, update: options.sessionPageStateUpdate });
608
609
  },
609
- }) !== undefined;
610
+ }) : undefined;
611
+ const reopened = !coldManagedSession || reopenedData !== undefined;
612
+ if (coldManagedSession && reopened)
613
+ priorSessionTabTarget = {
614
+ ...priorSessionTabTarget,
615
+ targetId: isRecord(reopenedData) && typeof reopenedData.targetId === "string" ? reopenedData.targetId : undefined,
616
+ };
610
617
  const selection = signal?.aborted ? undefined : reopened
611
618
  ? await ensureSessionTabTarget({ cwd, namespace: executionPlan.namespace, sessionName: executionPlan.sessionName, signal, target: priorSessionTabTarget })
612
619
  : { error: "agent-browser could not reopen the remembered URL after the managed browser shut down. Navigate explicitly, then run snapshot -i before retrying." };
@@ -4,7 +4,7 @@ import { parseArgvDescriptor } from "../../argv-descriptor.js";
4
4
  import { isBrowserIndependentRead, needsManagedSession } from "../../command-policy.js";
5
5
  import { deleteIdentityKeysInNamespace, getAgentBrowserSessionIdentityKey, isAgentBrowserSessionIdentityKeyInNamespace } from "../../argv-grammar.js";
6
6
  import { batchHasSuccessfulCloseAll, getSuccessfulBatchCloseLifecycle } from "../../batch-lifecycle.js";
7
- import { isCloseAllCommand, isCloseCommand, isOpenNavigationCommand, isPageMutationCommand, isRecordPageTransitionCommand, isUnverifiedPageTransitionCommand, isWindowOrDiffPageTransitionCommand } from "../../command-taxonomy.js";
7
+ import { isCloseAllCommand, isCloseCommand, isNavigationObservableCommandName, isOpenNavigationCommand, isPageMutationCommand, isRecordPageTransitionCommand, isUnverifiedPageTransitionCommand, isWindowOrDiffPageTransitionCommand } from "../../command-taxonomy.js";
8
8
  import { OPEN_RESULT_TAB_CORRECTION_FLAGS } from "../../launch-scoped-flags.js";
9
9
  import { cleanupElectronLaunchResources, inspectElectronLaunchStatus } from "../../electron/cleanup.js";
10
10
  import { getResultingPageTargetState, commandRequiresLivePageVerification } from "../../page-target-validation.js";
@@ -26,7 +26,7 @@ import { isManagedSessionRestoreKey } from "../../managed-session-storage.js";
26
26
  import { createFreshSessionName, extractUpstreamCommandTokens, resolveManagedSessionState } from "../../runtime.js";
27
27
  import { getUpstreamEffectiveBatchSteps } from "../batch-stdin.js";
28
28
  import { closeManagedSession, inspectManagedSessionDaemon } from "./managed-session-daemon-policy.js";
29
- import { applyOpenResultTabCorrection, buildAboutBlankRecoveryHint, buildAboutBlankWarning, buildElectronPostCommandHealthDiagnostic, buildElectronRefFreshnessDiagnostic, buildElectronSessionMismatch, buildManagedSessionOutcome, collectOpenResultTabCorrection, collectSessionTabSelection, commandChoosesSessionTabTarget, extractNavigationSummaryFromData, extractStringResultField, findElectronLaunchRecordForSession, formatElectronPostCommandHealthText, formatElectronSessionMismatchText, getSessionContextKey, getStaleRefArgs, mergeNavigationSummaryIntoData, runSessionCommandData, shouldCaptureNavigationSummary, shouldCorrectSessionTabAfterCommand, shouldInspectElectronPostCommandHealth, updateTraceOwnerState, } from "./session-state.js";
29
+ import { applyOpenResultTabCorrection, buildAboutBlankRecoveryHint, buildAboutBlankWarning, buildElectronPostCommandHealthDiagnostic, buildElectronRefFreshnessDiagnostic, buildElectronSessionMismatch, buildManagedSessionOutcome, collectOpenResultTabCorrection, collectSessionTabSelection, collectSessionTabTarget, commandChoosesSessionTabTarget, extractNavigationSummaryFromData, extractStringResultField, findElectronLaunchRecordForSession, formatElectronPostCommandHealthText, formatElectronSessionMismatchText, getSessionContextKey, getStaleRefArgs, mergeNavigationSummaryIntoData, runSessionCommandData, shouldCaptureNavigationSummary, shouldCorrectSessionTabAfterCommand, shouldInspectElectronPostCommandHealth, updateTraceOwnerState, } from "./session-state.js";
30
30
  import { collectClickDispatchDiagnostic } from "./click-dispatch.js";
31
31
  import { buildScrollNoopDiagnostic, collectComboboxFocusDiagnostic, collectElectronBroadGetTextScopeDiagnostics, collectElectronHandoff, collectFillVerificationDiagnostic, collectNavigationSummary, collectOverlayBlockerDiagnostic, collectQaAttachedTarget, collectSnapshotOverlayBlockerDiagnostic, collectRecordingDependencyWarning, collectScrollPositionSnapshot, collectSelectorTextVisibilityDiagnostics, collectTimeoutPartialProgress, sleepMs, formatQaAttachedTargetText, getArtifactCleanupGuidance, getEvalResultWarning, getEvalStdinHint, getSourceLookupElectronContext, } from "./diagnostics.js";
32
32
  import { repairScreenshotData } from "./prepare.js";
@@ -296,7 +296,10 @@ export async function processBrowserOutput(input) {
296
296
  presentationEnvelope = { ...(presentationEnvelope ?? {}), error: clickDispatchDiagnostic.summary, success: false };
297
297
  }
298
298
  }
299
- const tabTransition = prepared.executionPlan.commandInfo.command === "tab" && prepared.executionPlan.commandInfo.subcommand !== undefined && !["list", "new"].includes(prepared.executionPlan.commandInfo.subcommand);
299
+ const tabTransition = dispatchedCommands.some((step) => {
300
+ const [command, subcommand] = extractUpstreamCommandTokens(step);
301
+ return command === "tab" && subcommand !== undefined && subcommand !== "list";
302
+ });
300
303
  // Non-page rows (including a failed prefix) cannot retire a cold target for a navigation that never ran.
301
304
  const resultingPageState = sessionPageState.get(sessionStateKey).tabReopenPending === true
302
305
  ? { currentPageUrl: prepared.priorSessionTabTarget?.url, pageTargetMayHaveChanged: false, pageUrlUnknown: prepared.priorSessionTabTargetUnknown === true }
@@ -306,11 +309,12 @@ export async function processBrowserOutput(input) {
306
309
  currentPageUrl: prepared.priorSessionTabTarget?.url,
307
310
  pageUrlUnknown: prepared.priorSessionTabTargetUnknown === true,
308
311
  });
309
- if (succeeded &&
312
+ if (succeeded && !nestedBatchClosed &&
310
313
  (shouldCaptureNavigationSummary(prepared.executionPlan.commandInfo.command, presentationEnvelope?.data, prepared.executionPlan.commandInfo.subcommand) ||
314
+ (prepared.executionPlan.commandInfo.command === "batch" && dispatchedCommands.some(([command, subcommand]) => isNavigationObservableCommandName(command, subcommand))) ||
311
315
  shouldCaptureSemanticActionNavigationSummary(prepared.compiledSemanticAction, presentationEnvelope?.data) ||
312
316
  commandRequiresLivePageVerification(prepared.executionPlan.effectiveArgs, prepared.runtimeToolStdin) ||
313
- (destinationTransition && !nestedBatchClosed) || tabTransition)) {
317
+ destinationTransition || tabTransition)) {
314
318
  navigationSummary = await collectNavigationSummary({ cwd, namespace: prepared.executionPlan.namespace, priorTarget: prepared.priorSessionTabTarget, reusePriorTitle: !tabTransition, sessionName: prepared.executionPlan.sessionName, signal });
315
319
  }
316
320
  // Failed transitions may already have changed the page; keep only a live observed URL.
@@ -319,8 +323,11 @@ export async function processBrowserOutput(input) {
319
323
  !processResult.aborted &&
320
324
  !processResult.timedOut &&
321
325
  !nestedBatchClosed &&
322
- (destinationTransition || (prepared.executionPlan.commandInfo.command !== "batch" &&
323
- isUnverifiedPageTransitionCommand(prepared.executionPlan.commandInfo.command, prepared.executionPlan.commandInfo.subcommand)))) {
326
+ dispatchedCommands.some((step) => {
327
+ const [command, subcommand] = extractUpstreamCommandTokens(step);
328
+ return (prepared.executionPlan.commandInfo.command === "batch" && isOpenNavigationCommand(command))
329
+ || isUnverifiedPageTransitionCommand(command, subcommand);
330
+ })) {
324
331
  navigationSummary = await collectNavigationSummary({ cwd, namespace: prepared.executionPlan.namespace, priorTarget: prepared.priorSessionTabTarget, sessionName: prepared.executionPlan.sessionName, signal });
325
332
  // Re-verifying a failed transition's URL does not make its prior refs valid.
326
333
  failedTransitionReverification = navigationSummary !== undefined;
@@ -342,11 +349,21 @@ export async function processBrowserOutput(input) {
342
349
  const failedWebMcpSettlement = prepared.priorSessionTabTargetUnknown === true && ((!succeeded && isWebMcpSettlementCommand(prepared.executionPlan.commandInfo.command, prepared.executionPlan.commandInfo.subcommand))
343
350
  || (prepared.executionPlan.commandInfo.command === "batch" && batchHasFailedWebMcpSettlement(presentationEnvelope?.data)));
344
351
  const unsettledWebMcpMutation = pendingWebMcpMutation || failedWebMcpSettlement;
345
- const observedSessionTabTarget = unsettledWebMcpMutation || (unobservedMutation && !failedTransitionReverification)
352
+ let observedSessionTabTarget = unsettledWebMcpMutation || (unobservedMutation && !failedTransitionReverification)
346
353
  ? undefined
347
354
  : normalizeSessionTabTarget(navigationSummary)
348
355
  ?? (trustsReportedPageTarget ? extractSessionTabTargetFromBatchResults(presentationEnvelope?.data) : undefined)
349
356
  ?? (succeeded && trustsReportedPageTarget ? extractSessionTabTargetFromCommandData(prepared.commandTokens, presentationEnvelope?.data) : undefined);
357
+ if (observedSessionTabTarget && !browserIndependentRead && !nestedBatchClosed && !isCloseCommand(prepared.executionPlan.commandInfo.command)) {
358
+ const reportedTarget = prepared.executionPlan.commandInfo.command === "batch"
359
+ ? extractSessionTabTargetFromBatchResults(presentationEnvelope?.data)
360
+ : prepared.commandTokens[0] === "tab" && prepared.commandTokens[1] === "close" ? undefined
361
+ : extractSessionTabTargetFromCommandData(prepared.commandTokens, presentationEnvelope?.data);
362
+ if (reportedTarget?.targetId)
363
+ observedSessionTabTarget.targetId = reportedTarget.targetId;
364
+ else
365
+ observedSessionTabTarget = await collectSessionTabTarget({ cwd, namespace: prepared.executionPlan.namespace, sessionName: prepared.executionPlan.sessionName, signal, target: observedSessionTabTarget });
366
+ }
350
367
  const safeObservedSessionTabTarget = observedSessionTabTarget;
351
368
  let currentSessionTabTarget = safeObservedSessionTabTarget;
352
369
  if (!currentSessionTabTarget && nestedBatchClose === undefined && !unobservedMutation) {
@@ -459,7 +476,7 @@ export async function processBrowserOutput(input) {
459
476
  : processResult.agentBrowserStarted && (directTransitionInvalidation || plannedBatchTransitionInvalidation)
460
477
  ? directTransitionInvalidation ?? plannedBatchTransitionInvalidation
461
478
  : failedTransitionReverification
462
- ? buildPageTransitionRefSnapshotInvalidation("A failed eval/back/forward/reload/connect/state-load/tab command may still have changed the page, so the prior snapshot refs were invalidated. Run snapshot -i before using page-scoped refs.")
479
+ ? buildPageTransitionRefSnapshotInvalidation("A failed page transition may still have changed the page, so the prior snapshot refs were invalidated. Run snapshot -i before using page-scoped refs.")
463
480
  : batchRefSnapshotState?.invalidation?.reason === "page-transition"
464
481
  ? batchRefSnapshotState.invalidation
465
482
  : undefined;
@@ -460,6 +460,8 @@ export function shouldCorrectSessionTabAfterCommand(options) {
460
460
  !isSessionTabPostCommandCorrectionExcludedCommand(options.command));
461
461
  }
462
462
  function getTabSelection(tab) {
463
+ if (tab.targetId)
464
+ return { selectedTab: tab.targetId, selectionKind: "targetId" };
463
465
  if (typeof tab.tabId === "string" && tab.tabId.trim().length > 0)
464
466
  return { selectedTab: tab.tabId.trim(), selectionKind: "tabId" };
465
467
  if (typeof tab.label === "string" && tab.label.trim().length > 0)
@@ -467,6 +469,10 @@ function getTabSelection(tab) {
467
469
  return typeof tab.index === "number" ? { selectedTab: String(tab.index), selectionKind: "index" } : undefined;
468
470
  }
469
471
  function selectSessionTargetTab(options) {
472
+ if (options.target.targetId) {
473
+ const target = selectAnySessionTargetTab(options);
474
+ return target && options.tabs.find((tab) => tab.targetId === options.target.targetId)?.active !== true ? target : undefined;
475
+ }
470
476
  return chooseOpenResultTabCorrection({
471
477
  tabs: options.tabs,
472
478
  targetTitle: options.target.title,
@@ -477,7 +483,9 @@ function selectAnySessionTargetTab(options) {
477
483
  const targetUrl = typeof options.target.url === "string" ? normalizeComparableUrl(options.target.url) : undefined;
478
484
  if (!targetUrl)
479
485
  return undefined;
480
- const matchingTabs = options.tabs.filter((tab) => normalizeComparableUrl(tab.url ?? "") === targetUrl);
486
+ const matchingTabs = options.tabs.filter((tab) => options.target.targetId
487
+ ? tab.targetId === options.target.targetId
488
+ : normalizeComparableUrl(tab.url ?? "") === targetUrl);
481
489
  const targetTitle = options.target.title?.trim() ?? "";
482
490
  const titledTabs = targetTitle ? matchingTabs.filter((tab) => tab.title?.trim() === targetTitle) : [];
483
491
  const selectedTab = titledTabs.find((tab) => tab.active) ?? titledTabs[0] ?? matchingTabs.find((tab) => tab.active) ?? matchingTabs[0];
@@ -538,6 +546,7 @@ export async function collectOpenResultTabCorrection(options) {
538
546
  index: typeof tab.index === "number" ? tab.index : index,
539
547
  label: typeof tab.label === "string" ? tab.label : undefined,
540
548
  tabId: typeof tab.tabId === "string" ? tab.tabId : undefined,
549
+ targetId: typeof tab.targetId === "string" ? tab.targetId : undefined,
541
550
  title: typeof tab.title === "string" ? tab.title : undefined,
542
551
  url: typeof tab.url === "string" ? tab.url : undefined,
543
552
  }));
@@ -551,10 +560,21 @@ function mapTabData(tabData) {
551
560
  index: typeof tab.index === "number" ? tab.index : index,
552
561
  label: typeof tab.label === "string" ? tab.label : undefined,
553
562
  tabId: typeof tab.tabId === "string" ? tab.tabId : undefined,
563
+ targetId: typeof tab.targetId === "string" ? tab.targetId : undefined,
554
564
  title: typeof tab.title === "string" ? tab.title : undefined,
555
565
  url: typeof tab.url === "string" ? tab.url : undefined,
556
566
  }));
557
567
  }
568
+ export async function collectSessionTabTarget(options) {
569
+ const tabs = mapTabData(await runSessionCommandData({ ...options, args: ["tab", "list"] }));
570
+ const active = tabs?.find((tab) => tab.active);
571
+ const observedUrl = normalizeComparableUrl(options.target.url);
572
+ if (normalizeComparableUrl(active?.url) !== observedUrl && tabs?.some((tab) => normalizeComparableUrl(tab.url) === observedUrl))
573
+ return options.target;
574
+ return active?.targetId
575
+ ? { ...options.target, targetId: active.targetId }
576
+ : options.target;
577
+ }
558
578
  export async function collectSessionTabSelection(options) {
559
579
  const { cwd, namespace, sessionName, signal, target } = options;
560
580
  const tabData = await runSessionCommandData({ args: ["tab", "list"], cwd, namespace, sessionName, signal });
@@ -569,13 +589,20 @@ export async function ensureSessionTabTarget(options) {
569
589
  const error = "agent-browser could not re-select and verify the intended tab before running the command. Run tab list and select the intended tab, then snapshot -i before retrying.";
570
590
  if (!correction)
571
591
  return { error };
592
+ const verifyUrl = async (tab) => {
593
+ if (normalizeComparableUrl(tab.url) === normalizeComparableUrl(options.target.url))
594
+ return true;
595
+ // Native tab metadata can retain the attempted URL while get url reports Chrome's error page.
596
+ const data = await runSessionCommandData({ ...options, args: ["get", "url"] });
597
+ return normalizeComparableUrl(extractStringResultField(data, "url")) === normalizeComparableUrl(options.target.url);
598
+ };
572
599
  // Native tab selection clears refs and frame scope even when selecting the current tab.
573
600
  if (active && getTabSelection(active)?.selectedTab === correction.selectedTab)
574
- return {};
601
+ return await verifyUrl(active) ? {} : { error };
575
602
  if (!await applyOpenResultTabCorrection({ ...options, correction }))
576
603
  return { correction, error };
577
604
  const selected = (await readTabs())?.find((tab) => tab.active);
578
- return selected && getTabSelection(selected)?.selectedTab === correction.selectedTab && normalizeComparableUrl(selected.url ?? "") === normalizeComparableUrl(options.target.url)
605
+ return selected && getTabSelection(selected)?.selectedTab === correction.selectedTab && await verifyUrl(selected)
579
606
  ? { correction }
580
607
  : { correction, error };
581
608
  }
@@ -92,7 +92,7 @@ export const WRAPPER_TAB_RECOVERY_BEHAVIOR = [
92
92
  "After open/goto/navigate calls with --profile, --restore, --session-name, or --state, agent_browser best-effort re-selects the tab whose URL matches the returned page when restored tabs steal focus during launch or reconnect.",
93
93
  "After confirmed shutdown of an automatically restored managed session, the wrapper retains its complete recorded URL, including the fragment, until the first current-page operation (including get url and reload). Non-page calls such as tab list may start a daemon without fulfilling that reopen; explicit URL reads leave the managed browser and pending reopen untouched. The wrapper uses native open once, verifies the observed tab, and discards old refs/frame scope; it does not restore unsaved forms, JavaScript memory, or history. Explicit navigation, caller-owned/attached sessions, and restore-disabled sessions are not auto-reopened.",
94
94
  "For a still-live browser after tab drift or resume, the wrapper verifies/selects the intended tab before ref/semantic helpers and page commands; failed selection stops the call without navigating. Local commands, read <url>, URL a11y/vitals, diff url, window new, and explicit tab/navigation/connection/state recovery do not require the prior tab. Batch checks follow effective rows past non-page prefixes and stop at explicit context changes, preserving caller argv/stdin and continue-on-error behavior. Same-tab reselection is avoided because it clears refs. Use exact batch --bail for fail-fast, not --bail=<value>. Routine same-session calls skip tab-list preflights.",
95
- "For sessions with observed tab-drift risk, after a successful command on a known target tab, agent_browser also best-effort restores that intended tab if a restored/background tab steals focus after the command completes. Routine same-session commands skip this post-command tab-list probe.",
95
+ "For sessions with observed tab-drift risk, after a successful command on a known target tab, agent_browser also best-effort restores that intended tab if a restored/background tab steals focus after the command completes. Routine same-session commands skip focus correction; observed pages without a native targetId use tab list to establish their identity.",
96
96
  "If a known session target unexpectedly reports about:blank, agent_browser best-effort re-selects the prior intended target when it still exists; if recovery fails, it records the observed about:blank target and reports exact recovery guidance instead of treating the prior page as active.",
97
97
  "If upstream reports tab_gone, the pinned bound tab is gone; use details.nextActions (tab list / tab new) instead of assuming another tab is yours.",
98
98
  ];
@@ -120,50 +120,41 @@ function redactLooseUrlUserinfo(text) {
120
120
  function redactLooseUrlMatches(text) {
121
121
  return text.replace(/\b[A-Za-z][A-Za-z0-9+.-]*:\/\/[^\s"'`<>\])]+/g, (match) => redactUrlToken(match));
122
122
  }
123
- function findBalancedJsonEnd(text, startIndex) {
124
- const opener = text[startIndex];
125
- const closer = opener === "{" ? "}" : opener === "[" ? "]" : undefined;
126
- if (!closer)
127
- return undefined;
128
- const stack = [closer];
129
- let inString = false;
130
- let escaped = false;
131
- for (let index = startIndex + 1; index < text.length; index += 1) {
132
- const char = text[index];
133
- if (inString) {
134
- if (escaped) {
135
- escaped = false;
136
- continue;
137
- }
138
- if (char === "\\") {
139
- escaped = true;
140
- continue;
141
- }
142
- if (char === '"') {
143
- inString = false;
123
+ function createBalancedJsonEndFinder(text) {
124
+ let boundaries;
125
+ return (startIndex) => {
126
+ const opener = text[startIndex];
127
+ if (opener !== "{" && opener !== "[")
128
+ return undefined;
129
+ if (!boundaries) {
130
+ // Resolve each suffix once: skip strings and balanced groups to its first unmatched closer.
131
+ boundaries = new Int32Array(text.length + 1).fill(-1);
132
+ let quoteAtNext = -1;
133
+ let quoteAfterNext = -1;
134
+ for (let index = text.length - 1; index >= 0; index -= 1) {
135
+ const char = text[index];
136
+ if (char === "}" || char === "]") {
137
+ boundaries[index] = index;
138
+ }
139
+ else if (char === '"') {
140
+ boundaries[index] = quoteAtNext < 0 ? -1 : boundaries[quoteAtNext + 1];
141
+ }
142
+ else if (char === "{" || char === "[") {
143
+ const end = boundaries[index + 1];
144
+ boundaries[index] = end >= 0 && text[end] === (char === "{" ? "}" : "]") ? boundaries[end + 1] : -1;
145
+ }
146
+ else {
147
+ boundaries[index] = boundaries[index + 1];
148
+ }
149
+ // Inside a string, backslash skips one character; outside, it is ordinary text.
150
+ const quote = char === '"' ? index : char === "\\" ? quoteAfterNext : quoteAtNext;
151
+ quoteAfterNext = quoteAtNext;
152
+ quoteAtNext = quote;
144
153
  }
145
- continue;
146
154
  }
147
- if (char === '"') {
148
- inString = true;
149
- continue;
150
- }
151
- if (char === "{") {
152
- stack.push("}");
153
- continue;
154
- }
155
- if (char === "[") {
156
- stack.push("]");
157
- continue;
158
- }
159
- if (char === "}" || char === "]") {
160
- if (stack.pop() !== char)
161
- return undefined;
162
- if (stack.length === 0)
163
- return index;
164
- }
165
- }
166
- return undefined;
155
+ const end = boundaries[startIndex + 1];
156
+ return end >= 0 && text[end] === (opener === "{" ? "}" : "]") ? end : undefined;
157
+ };
167
158
  }
168
159
  function redactSerializedJson(text) {
169
160
  // Validate grammar only; rebuilding parsed values loses duplicates and numeric spelling.
@@ -175,6 +166,7 @@ function redactSerializedJson(text) {
175
166
  }
176
167
  let output = "";
177
168
  let cursor = 0;
169
+ const findEnd = createBalancedJsonEndFinder(text);
178
170
  const strings = /"(?:\\.|[^"\\])*"/g;
179
171
  let match;
180
172
  while ((match = strings.exec(text)) !== null) {
@@ -189,7 +181,7 @@ function redactSerializedJson(text) {
189
181
  if (!separator || !isSensitiveFieldName(value))
190
182
  continue;
191
183
  const valueStart = end + separator[0].length;
192
- let valueEnd = findBalancedJsonEnd(text, valueStart);
184
+ let valueEnd = findEnd(valueStart);
193
185
  if (valueEnd !== undefined) {
194
186
  valueEnd += 1;
195
187
  }
@@ -214,6 +206,7 @@ function redactSerializedJson(text) {
214
206
  function redactEmbeddedStructuredText(text) {
215
207
  let output = "";
216
208
  let cursor = 0;
209
+ const findEnd = createBalancedJsonEndFinder(text);
217
210
  while (cursor < text.length) {
218
211
  const char = text[cursor];
219
212
  if (char !== "{" && char !== "[") {
@@ -221,7 +214,7 @@ function redactEmbeddedStructuredText(text) {
221
214
  cursor += 1;
222
215
  continue;
223
216
  }
224
- const endIndex = findBalancedJsonEnd(text, cursor);
217
+ const endIndex = findEnd(cursor);
225
218
  if (endIndex === undefined) {
226
219
  output += char;
227
220
  cursor += 1;
@@ -268,14 +261,25 @@ export function isSensitiveFieldName(key) {
268
261
  function isEnvSecretAssignmentKey(key) {
269
262
  if (!isSensitiveFieldName(key))
270
263
  return false;
271
- if (key.includes("_") || key.includes("-") || key === key.toUpperCase())
264
+ if (key === "password" || key.includes("_") || key.includes("-") || key === key.toUpperCase())
272
265
  return true;
273
266
  return /(?:apiKey|ApiKey|privateKey|PrivateKey|databaseUrl|DatabaseUrl|dbUrl|DbUrl|connectionString|ConnectionString|mongoUri|MongoUri|mongodbUri|MongodbUri|mongoDbUri|MongoDbUri|redisUrl|RedisUrl|Token|Secret|Password|Credential|Credentials)$/.test(key);
274
267
  }
275
268
  function redactEnvSecretAssignments(text) {
276
- return text.replace(ENV_SECRET_ASSIGNMENT_PATTERN, (match, prefix, key) => {
269
+ return text.replace(ENV_SECRET_ASSIGNMENT_PATTERN, (match, prefix, key, _separator, offset) => {
277
270
  if (!isEnvSecretAssignmentKey(key))
278
271
  return match;
272
+ if (key === "password") {
273
+ // The loose matcher cannot safely distinguish inline punctuation from password characters.
274
+ const end = offset + match.length;
275
+ if ((offset > 0 && text[offset - 1] !== "\n" && text[offset - 1] !== "\r")
276
+ || (end < text.length && text[end] !== "\n" && text[end] !== "\r")
277
+ || /^(?:\[REDACTED\]|%5Bredacted%5D)/i.test(match.slice(prefix.length)))
278
+ return match;
279
+ const value = match.slice(prefix.length);
280
+ const boundary = value[0] === '"' || value[0] === "'" ? -1 : value.search(/[&#](?=[A-Za-z_][A-Za-z0-9_-]*\s*=)/);
281
+ return `${prefix}[REDACTED]${boundary < 0 ? "" : value.slice(boundary)}`;
282
+ }
279
283
  return `${prefix}[REDACTED]`;
280
284
  });
281
285
  }
@@ -2,7 +2,7 @@ import { extractUpstreamCommandTokens } from "./argv-descriptor.js";
2
2
  import { getAgentBrowserSessionIdentityKey, isAgentBrowserSessionIdentityKeyInNamespace } from "./argv-grammar.js";
3
3
  import { batchHasSuccessfulCloseAll, getSuccessfulBatchCloseLifecycle } from "./batch-lifecycle.js";
4
4
  import { getBrowserResultMessage } from "./browser-transcript.js";
5
- import { isCloseAllCommand, isCloseCommand, isReadOnlyDiagnosticSessionTargetCommand, isRecordPageTransitionCommand, isUnverifiedPageTransitionCommand, isWebMcpPageMutationCommand, isWindowOrDiffPageTransitionCommand } from "./command-taxonomy.js";
5
+ import { isCloseAllCommand, isCloseCommand, isOpenNavigationCommand, isReadOnlyDiagnosticSessionTargetCommand, isRecordPageTransitionCommand, isUnverifiedPageTransitionCommand, isWebMcpPageMutationCommand, isWindowOrDiffPageTransitionCommand } from "./command-taxonomy.js";
6
6
  import { isRecord } from "./parsing.js";
7
7
  import { findReadConfirmation as findPendingReadConfirmation, parseReadConfirmation } from "./read-confirmation.js";
8
8
  import { getEditableRefEvidence } from "./results/editable-ref-evidence.js";
@@ -31,7 +31,7 @@ export function normalizeSessionTabTarget(target) {
31
31
  return undefined;
32
32
  }
33
33
  const title = target.title?.trim();
34
- return { title: title && title.length > 0 ? title : undefined, url };
34
+ return { ...(target.targetId?.trim() ? { targetId: target.targetId.trim() } : {}), title: title && title.length > 0 ? title : undefined, url };
35
35
  }
36
36
  export function isAboutBlankUrl(url) {
37
37
  return normalizeComparableUrl(url) === "about:blank";
@@ -64,6 +64,7 @@ function extractStringResultField(data, fieldName) {
64
64
  }
65
65
  export function extractSessionTabTargetFromData(data) {
66
66
  const directTarget = normalizeSessionTabTarget({
67
+ targetId: isRecord(data) && typeof data.targetId === "string" ? data.targetId : undefined,
67
68
  title: extractStringResultField(data, "title"),
68
69
  url: extractStringResultField(data, "url"),
69
70
  });
@@ -94,8 +95,13 @@ export function extractSessionTabTargetFromBatchResults(data) {
94
95
  for (const item of data) {
95
96
  if (!isRecord(item))
96
97
  continue;
97
- const [name, subcommand] = extractUpstreamCommandTokens(extractBatchResultCommand(item));
98
- if (isWindowOrDiffPageTransitionCommand(name, subcommand)) {
98
+ // Only this row's native identity can describe the active tab after it ran.
99
+ if (currentTarget?.targetId)
100
+ currentTarget = normalizeSessionTabTarget({ title: currentTarget.title, url: currentTarget.url });
101
+ const commandTokens = extractUpstreamCommandTokens(extractBatchResultCommand(item));
102
+ const [name, subcommand] = commandTokens;
103
+ if (isOpenNavigationCommand(name) || isUnverifiedPageTransitionCommand(name, subcommand)
104
+ || (name === "click" && commandTokens.includes("--new-tab"))) {
99
105
  currentTarget = undefined;
100
106
  pendingTitle = undefined;
101
107
  }
@@ -159,6 +165,7 @@ function getRestoredSessionTabTarget(details, command, subcommand) {
159
165
  }
160
166
  const storedTarget = isRecord(details.sessionTabTarget)
161
167
  ? normalizeSessionTabTarget({
168
+ targetId: typeof details.sessionTabTarget.targetId === "string" ? details.sessionTabTarget.targetId : undefined,
162
169
  title: typeof details.sessionTabTarget.title === "string" ? details.sessionTabTarget.title : undefined,
163
170
  url: typeof details.sessionTabTarget.url === "string" ? details.sessionTabTarget.url : undefined,
164
171
  })
@@ -166,13 +173,16 @@ function getRestoredSessionTabTarget(details, command, subcommand) {
166
173
  if (command !== "batch") {
167
174
  return storedTarget;
168
175
  }
176
+ if (isRecord(details.compiledNetworkSourceLookup) || batchContainsOnlyReadOnlyDiagnosticTargets(details.data)) {
177
+ return undefined;
178
+ }
179
+ // The stored target includes post-batch live verification and native tab identity.
180
+ if (storedTarget || details.sessionTabTargetUnknown === true)
181
+ return storedTarget;
169
182
  const batchTarget = extractSessionTabTargetFromBatchResults(details.data);
170
183
  if (batchTarget) {
171
184
  return batchTarget;
172
185
  }
173
- if (isRecord(details.compiledNetworkSourceLookup) || batchContainsOnlyReadOnlyDiagnosticTargets(details.data)) {
174
- return undefined;
175
- }
176
186
  return storedTarget;
177
187
  }
178
188
  function extractRefSnapshotRefs(data) {
@@ -30,7 +30,6 @@ export function registerAgentBrowserToolSurface(pi, options) {
30
30
  description: "Run fresh JavaScript against a persistent browser. await browser({args,stdin?,timeoutMs?}) returns success/data/error/nextActions and imageObservations; emit(selected JSON) and emitImage(image handle) explicitly choose output. No host APIs or imports. Use native batch for fixed sequences.",
31
31
  promptSnippet: "Branch, loop, and aggregate browser observations with explicit JSON/image output.",
32
32
  parameters: AGENT_BROWSER_CODE_PARAMS,
33
- constrainedSampling: { type: "json_schema", strict: "prefer" },
34
33
  execute: options.executeCode,
35
34
  executionMode: options.executionMode,
36
35
  renderCall(args, theme, context) {
@@ -487,8 +487,9 @@ const BRAVE_WEB_SEARCH_ADAPTER = {
487
487
  buildRequest(params) {
488
488
  return buildBraveSearchUrl({
489
489
  query: params.query,
490
- count: params.count,
491
- offset: params.offset,
490
+ // Brave offsets count pages; fetch the prefix and slice by result like Exa.
491
+ count: params.count + params.offset,
492
+ offset: 0,
492
493
  country: params.country,
493
494
  searchLang: params.searchLang,
494
495
  safesearch: params.safesearch,
@@ -502,7 +503,8 @@ const BRAVE_WEB_SEARCH_ADAPTER = {
502
503
  return {
503
504
  results: (response.web?.results ?? [])
504
505
  .map(normalizeBraveSearchResult)
505
- .filter((result) => Boolean(result)),
506
+ .filter((result) => Boolean(result))
507
+ .slice(params.offset, params.offset + params.count),
506
508
  returnedQuery: cleanSearchText(response.query?.altered, 300) ?? cleanSearchText(response.query?.original, 300) ?? params.query,
507
509
  };
508
510
  },
@@ -185,10 +185,10 @@ Practical policy:
185
185
  - expose `details.browserWindow` and one visible login handoff only when a successful first/fresh local wrapper-managed headed result, including `batch`, is not an attachment and has upstream `lifecycle.effectiveLaunch.browserLaunched: true` and a `created`/`replaced` managed-session outcome. Keep `visibility: "unverified"`: this is launch evidence, never a claim about the user's OS desktop
186
186
  - leave explicit caller-provided `--session` choices alone unless the caller closes them explicitly, but before any DOM/content-bearing read or interaction against a caller-owned explicit session, live-probe that session with `get url` instead of trusting missing or stale transcript page state; hold the effective canonical namespace/session queue from that probe through semantic snapshot resolution and the main command so another cooperating updated Pi call cannot change tabs in between. Non-bail batch analysis retains every possible page left by a failed transition up to a fixed bound and blocks later content only when the target is unverified; exceeding the bound also fails closed to exact `batch --bail` guidance. Nested `batch` steps remain unsupported, and raw batch command strings mirror upstream's ASCII-space tokenizer, including quote/backslash handling, rather than splitting on other Unicode whitespace.
187
187
  - after profiled `open` / `goto` / `navigate` calls, verify the active tab still matches the returned page URL and best-effort switch back when restored profile tabs steal focus
188
- - once the wrapper observes tab-drift risk for a session (profile restore correction, overlapping stale opens, or restored session state), later active-tab commands verify the intended tab under the existing session queue before semantic/ref helpers and user commands. Native selection runs only when the intended tab is not already active, because upstream selection clears refs and frame scope even on same-tab reselection. Missing targets, failed selection, and post-selection target mismatches fail before user commands. Caller argv/stdin and native `--pin-tab` / `--no-pin-tab` preferences remain unchanged. Local commands, live `get url`, explicit HTTP `read <url>` (including its flags), URL `a11y`/`vitals`/`web-vitals`, `diff url`, `window new`, URL-bearing recording commands, and explicit tab/navigation/`connect`/`state load` recovery do not require the old target; history back/forward/reload, `pushstate`, and page-content operations still do. The same classifier scans effective batch rows past non-page prefixes until a page dependency or explicit context change, without rewriting user rows or changing bail behavior. For `window new` and `diff url`, observe the resulting URL instead of retaining the old target or treating the requested second URL as redirect evidence. Fold only reached native batch rows when available, discard observations and refs from before those transitions, and let later successful snapshots rebuild refs even when another batch row fails. Retain an observed blank destination after either command instead of recovering the old page; if no final target is observed, use the existing unknown-target state. Caller batch arguments, stdin and bail behavior stay unchanged. Routine same-session commands avoid `tab list` preflights
189
- - for sessions with observed tab-drift risk, after a successful command on a known tab target, the wrapper may best-effort restore that same target again if restored/background tabs steal focus after the command returns; routine same-session commands skip this post-command `tab list` probe
190
- - after successful standalone tab selection or `tab close`, read the now-active URL and fresh non-blank title—even when two tabs share a URL—before updating per-session page state because upstream selection/close payloads are not sufficient page-target evidence; retain an explicitly selected existing `about:blank` tab or a blank tab revealed by close instead of treating either as accidental drift
191
- - keep a per-session `refSnapshot` aligned with the last successful `snapshot` (including refs merged from a successful `batch` by taking the last successful `snapshot` step in batch result order): restore it from persisted tool `details` when reloading, resuming, or moving to a different Pi session-tree branch, store bounded ref role/name metadata from the same snapshot for wrapper-side current-ref diagnostics, drop it on successful close commands (`close`, `quit`, or `exit`), replace it with a persisted `page-transition` invalidation after any upstream-executed `record start` attempt (direct or batch; older supported natives can swap the page before their already-active check, so failed starts count; the warning and persisted summary explicitly describe conservative ref invalidation, not an observed page change), a `record restart` with a URL operand, `window new`, `diff url`, or WebMCP `invoke` / `result` / `cancel` (these page-provided tools can mutate, rerender, or navigate; when a spawned `batch` yields no parseable result rows, for example after a wrapper timeout, planned transition steps still record the invalidation), or after a failed non-batch transition command (`eval`, `back`, `forward`, `reload`, `connect`, `state load`, `tab` selection) whose live URL re-verification probe observed the page (a failed transition can still have mutated or replaced the document before throwing, so keeping the verified URL must not keep the prior refs; transcript replay preserves the persisted invalidation summary), and refuse page-scoped `@e…` argv before spawn when the active tab URL no longer matches the snapshot URL, when a ref id was never in that snapshot, when the snapshot state is invalidated, or when a `batch` step would reuse `@e…` on a guarded getter or mutation step after an earlier invalidating step (including `record start`, URL-bearing `record restart`, and WebMCP `invoke` / `result` / `cancel`) without a later `snapshot` step in the same plan; batch steps come from the source upstream actually executes (raw batch argument strings exclusively when any exist — filtering only the exact `--bail` token like upstream — stdin only otherwise, via `getUpstreamEffectiveBatchSteps` in `extensions/agent-browser/lib/orchestration/batch-stdin.ts`); tab recovery (which leaves user argv/stdin and continue-on-error control flow unchanged), artifact/recording preflight, batch screenshot path preparation (parent directories are created for effective raw rows too, without rewriting raw strings), and stale-ref echo args use that same selection so pinning and preflights cannot act on upstream-ignored stdin, while the pre-spawn state-policy validator deliberately keeps scanning parseable stdin alongside argv as a fail-closed content superset and treats stdin parse failures as fatal only when upstream would actually read stdin (its raw-token filter also uses the exact `--bail` token only). Same-snapshot `fill @e…` rows are guarded but do not themselves set that invalidation latch, so ordinary form fills can precede a click/submit row in one batch—see [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#details) for the agent-visible contract and failure text; typed per-session tab/ref/pinning state lives in `extensions/agent-browser/lib/session-page-state.ts` and is updated from `extensions/agent-browser/index.ts` after each tool result
188
+ - once the wrapper observes tab-drift risk for a session (profile restore correction, overlapping stale opens, or restored session state), later active-tab commands verify the intended tab under the existing session queue before semantic/ref helpers and user commands. Targets retain native CDP `targetId` across transcript replay; exact identity distinguishes duplicate URL/title tabs, while older targets without an ID keep URL-based matching. A cold managed reopen replaces the old ID. Native selection runs only when the intended tab is not already active, because upstream selection clears refs and frame scope even on same-tab reselection. Missing targets, failed selection, and post-selection target mismatches fail before user commands. Caller argv/stdin and native `--pin-tab` / `--no-pin-tab` preferences remain unchanged. Local commands, live `get url`, explicit HTTP `read <url>` (including its flags), URL `a11y`/`vitals`/`web-vitals`, `diff url`, `window new`, URL-bearing recording commands, and explicit tab/navigation/`connect`/`state load` recovery do not require the old target; history back/forward/reload, `pushstate`, and page-content operations still do. The same classifier scans effective batch rows past non-page prefixes until a page dependency or explicit context change, without rewriting user rows or changing bail behavior. For `window new` and `diff url`, observe the resulting URL instead of retaining the old target or treating the requested second URL as redirect evidence. Fold only reached native batch rows when available, discard targets before failed navigation/context transitions and live-verify the final page, discard observations and refs from before window/diff transitions, and let later successful snapshots rebuild refs even when another batch row fails. Retain an observed blank destination after either command instead of recovering the old page; if no final target is observed, use the existing unknown-target state. Caller batch arguments, stdin and bail behavior stay unchanged. Successful batches containing navigation-observable actions use the same final live page/identity observation as direct actions, so same-address popup tabs replace the pre-batch target before later pinning. Terminal batch closes do not trigger a page probe, and pending WebMCP targets remain unknown. Routine same-session commands avoid `tab list` preflights
189
+ - for sessions with observed tab-drift risk, after a successful command on a known tab target, the wrapper may best-effort restore that same target again if restored/background tabs steal focus after the command returns; routine same-session commands skip focus correction, while observed pages without a returned native ID use `tab list` to establish current identity rather than inheriting an earlier tab's ID
190
+ - after successful tab creation, selection, or close, including dispatched native batch rows, read the now-active URL and fresh non-blank title—even when two tabs share a URL—before updating per-session page state; retain the deliberate blank target instead of treating it as accidental drift
191
+ - keep a per-session `refSnapshot` aligned with the last successful `snapshot` (including refs merged from a successful `batch` by taking the last successful `snapshot` step in batch result order): restore it from persisted tool `details` when reloading, resuming, or moving to a different Pi session-tree branch, store bounded ref role/name metadata from the same snapshot for wrapper-side current-ref diagnostics, drop it on successful close commands (`close`, `quit`, or `exit`), replace it with a persisted `page-transition` invalidation after any upstream-executed `record start` attempt (direct or batch; older supported natives can swap the page before their already-active check, so failed starts count; the warning and persisted summary explicitly describe conservative ref invalidation, not an observed page change), a `record restart` with a URL operand, `window new`, `diff url`, or WebMCP `invoke` / `result` / `cancel` (these page-provided tools can mutate, rerender, or navigate; when a spawned `batch` yields no parseable result rows, for example after a wrapper timeout, planned transition steps still record the invalidation), or after a failed transition command (`eval`, `back`, `forward`, `reload`, `connect`, `state load`, `tab` selection), including reached batch transitions and failed batch `open` rows whose live URL re-verification probe observed the page (a failed transition can still have mutated or replaced the document before throwing, so keeping the verified URL must not keep the prior refs; transcript replay preserves the persisted invalidation summary), and refuse page-scoped `@e…` argv before spawn when the active tab URL no longer matches the snapshot URL, when a ref id was never in that snapshot, when the snapshot state is invalidated, or when a `batch` step would reuse `@e…` on a guarded getter or mutation step after an earlier invalidating step (including `record start`, URL-bearing `record restart`, and WebMCP `invoke` / `result` / `cancel`) without a later `snapshot` step in the same plan; batch steps come from the source upstream actually executes (raw batch argument strings exclusively when any exist — filtering only the exact `--bail` token like upstream — stdin only otherwise, via `getUpstreamEffectiveBatchSteps` in `extensions/agent-browser/lib/orchestration/batch-stdin.ts`); tab recovery (which leaves user argv/stdin and continue-on-error control flow unchanged), artifact/recording preflight, batch screenshot path preparation (parent directories are created for effective raw rows too, without rewriting raw strings), and stale-ref echo args use that same selection so pinning and preflights cannot act on upstream-ignored stdin, while the pre-spawn state-policy validator deliberately keeps scanning parseable stdin alongside argv as a fail-closed content superset and treats stdin parse failures as fatal only when upstream would actually read stdin (its raw-token filter also uses the exact `--bail` token only). Same-snapshot `fill @e…` rows are guarded but do not themselves set that invalidation latch, so ordinary form fills can precede a click/submit row in one batch—see [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#details) for the agent-visible contract and failure text; typed per-session tab/ref/pinning state lives in `extensions/agent-browser/lib/session-page-state.ts` and is updated from `extensions/agent-browser/index.ts` after each tool result
192
192
  - native 0.37 owns first-document setup inheritance for new tabs; the wrapper does not copy headers, user agents or other browser setup itself. The page-summary formatter displays native positive WebMCP availability while retaining raw details; it adds no discovery probe
193
193
  - when a direct or batched WebMCP call returns `status: "pending"`, or `result` / `cancel` fails while that target is unknown, keep its tab target unknown and discard same-call snapshot evidence instead of treating an immediate post-dispatch URL probe as stable; `webmcp result` / `cancel`, `get url`, and explicit navigation remain available while unknown; replace the generic blocked snapshot action with `verify-page-target-after-pending-webmcp` (`get url`), and let a completed `batch --bail` use that verification before `snapshot -i` to re-establish both target and refs
194
194
  - for top-level non-Electron direct `click` commands with an eligible target, install a bounded in-page target-specific event probe before upstream runs; if upstream reports success but no trusted pointer/mouse/click event reached the resolved target, fail the tool and report `details.clickDispatch` with explicit retry/inspect next actions (the wrapper does not replay clicks in-page). The probe covers `xpath=` targets and current `@e…` / `ref=` refs whose latest stored `refSnapshot.refs` role is `button`, `checkbox`, `menuitem`, `radio`, `switch`, or `tab`; it requires a unique role/name in the saved snapshot and the live candidates instead of taking a fresh pre-click snapshot that could recycle upstream refs. Ref and XPath candidates must additionally prove identity through a per-probe temporary DOM attribute read back by native `get attr <original-selector> <marker>`. Missing or mismatched identity, including XPath/frame scope mismatches, cleans up the probe and lets native click run without dispatch verification; candidate naming heuristics alone are never proof. Duplicate-name refs pass through without a probe: their old ordinal is not target identity. The probe is intentionally skipped for CSS selector clicks, unresolved `find … click` locators, and `batch`/`qa` click steps
@@ -785,7 +785,9 @@ Stable tab ids look like `t1`, `t2`, and `t3`. Optional user labels such as `doc
785
785
  | `tab <t<N>|label>` | Switch to a tab by id or label. CDP target ids from `tab list --json` are also accepted and stay stable across daemon restarts. |
786
786
  | `tab close [t<N>|label|target]` | Close the current tab or a referenced tab. Generic references in workflows may say `tab close [target]`; use a stable `t<N>` id, label, or CDP target id when you have one. |
787
787
 
788
- After successful standalone tab selection or close, the wrapper live-probes `get url` and, for non-blank pages, `get title` before committing the active target. Tab transitions always refresh the title even when the URL matches the prior tab, and an explicit selection of an existing `about:blank` tab or a close that reveals one remains on that live target instead of triggering prior-tab correction. A successful probe clears the unverified-page gate for the next command; if the probe fails, `details.sessionTabTargetUnknown` stays true and recovery still starts with `get url`.
788
+ The wrapper persists native CDP `targetId` when available so resume/branch recovery distinguishes tabs with the same URL and title. A missing known target requires explicit tab recovery; older transcripts without IDs still use URL matching, and a cold managed reopen adopts its new ID. Failed reached batch navigations retire earlier targets and recheck the live page, including Chrome error pages, before subsequent inspection.
789
+
790
+ After successful tab creation, selection, or close, including dispatched native batch rows, the wrapper live-probes `get url` and, for non-blank pages, `get title` before committing the active target. Tab transitions always refresh the title even when the URL matches the prior tab. New blank tabs, explicitly selected `about:blank` tabs, and blank tabs revealed by close remain active instead of triggering prior-tab correction. A successful probe clears the unverified-page gate for the next command; if the probe fails, `details.sessionTabTargetUnknown` stays true and recovery still starts with `get url`.
789
791
 
790
792
  With `--pin-tab`, a closed bound tab fails as `tab_gone` (`data.targetId`, optional `data.lastUrl`) instead of falling back to another tab.
791
793
 
@@ -1077,11 +1079,11 @@ Other useful environment variables include `AGENT_BROWSER_DEFAULT_TIMEOUT`, `AGE
1077
1079
  - After open/goto/navigate calls with --profile, --restore, --session-name, or --state, agent_browser best-effort re-selects the tab whose URL matches the returned page when restored tabs steal focus during launch or reconnect.
1078
1080
  - After confirmed shutdown of an automatically restored managed session, the wrapper retains its complete recorded URL, including the fragment, until the first current-page operation (including get url and reload). Non-page calls such as tab list may start a daemon without fulfilling that reopen; explicit URL reads leave the managed browser and pending reopen untouched. The wrapper uses native open once, verifies the observed tab, and discards old refs/frame scope; it does not restore unsaved forms, JavaScript memory, or history. Explicit navigation, caller-owned/attached sessions, and restore-disabled sessions are not auto-reopened.
1079
1081
  - For a still-live browser after tab drift or resume, the wrapper verifies/selects the intended tab before ref/semantic helpers and page commands; failed selection stops the call without navigating. Local commands, read <url>, URL a11y/vitals, diff url, window new, and explicit tab/navigation/connection/state recovery do not require the prior tab. Batch checks follow effective rows past non-page prefixes and stop at explicit context changes, preserving caller argv/stdin and continue-on-error behavior. Same-tab reselection is avoided because it clears refs. Use exact batch --bail for fail-fast, not --bail=<value>. Routine same-session calls skip tab-list preflights.
1080
- - For sessions with observed tab-drift risk, after a successful command on a known target tab, agent_browser also best-effort restores that intended tab if a restored/background tab steals focus after the command completes. Routine same-session commands skip this post-command tab-list probe.
1082
+ - For sessions with observed tab-drift risk, after a successful command on a known target tab, agent_browser also best-effort restores that intended tab if a restored/background tab steals focus after the command completes. Routine same-session commands skip focus correction; observed pages without a native targetId use tab list to establish their identity.
1081
1083
  - If a known session target unexpectedly reports about:blank, agent_browser best-effort re-selects the prior intended target when it still exists; if recovery fails, it records the observed about:blank target and reports exact recovery guidance instead of treating the prior page as active.
1082
1084
  - If upstream reports tab_gone, the pinned bound tab is gone; use details.nextActions (tab list / tab new) instead of assuming another tab is yours.
1083
1085
  <!-- agent-browser-playbook:end wrapper-tab-recovery -->
1084
- - Wrapper-spawned commands clamp `AGENT_BROWSER_DEFAULT_TIMEOUT` to the upstream documented 25-second default and use a 35-second child-process watchdog (`PI_AGENT_BROWSER_PROCESS_TIMEOUT_MS` overrides the default 35s budget; top-level `timeoutMs` overrides it per browser CLI call). Explicit `wait <ms>`, `wait --timeout <ms>`, and WebMCP `invoke` / `result --timeout <ms>` calls can exceed that default; when top-level `timeoutMs` is omitted, the wrapper derives a subprocess watchdog from the requested command duration plus a small grace window. Batch budgeting and timeout recovery read the same effective source as upstream: raw command strings when present, otherwise stdin rows. Dialog commands are additionally bounded to 5 seconds (`PI_AGENT_BROWSER_DIALOG_PROCESS_TIMEOUT_MS`), and click/tap/find refs or tokens plus `eval --stdin` snippets that look like alert/confirm/prompt/dialog triggers are bounded to 8 seconds (`PI_AGENT_BROWSER_DIALOG_TRIGGER_PROCESS_TIMEOUT_MS`). A `session info` timeout returns only an exact-session `retry-session-info` status action, without page probes, liveness claims, or changes to existing page/ref state. When a browser-operation watchdog fires, `details.timeoutPartialProgress` may include a planned step list with per-step status (including `generatedFrom` labels for generated QA rows) and a `retry-timeout-step` next action with a one-row native batch (`args: ["batch"]` plus `stdin`) only when the first incomplete step is read-only or idempotent, or `inspect-current-page-after-timeout` when the target is already verified but the incomplete step may be mutating and should not be blindly retried. If the target is unknown, standalone snapshots are removed and visible failure text plus `details.nextActions` show `verify-page-target-after-timeout`, including its session-scoped `batch --bail` args and short stdin for fail-fast `get url` then `snapshot -i`; dialog status/accept/dismiss actions remain allowed for blocking-dialog recovery. It also includes current page URL from best-effort session `get url`, followed by `get title` only after a URL is recovered (or a planned URL inferred from the step list when the session cannot answer), an `openedButPostOpenTimedOut` classification only when a live page URL was recovered before a later step hung, and declared artifact paths such as `screenshot`, `pdf`, `download`, or `wait --download` outputs with existence/state checks; the same evidence is appended under `Timeout partial progress` in visible text with URL/path redaction.
1086
+ - Wrapper-spawned commands clamp `AGENT_BROWSER_DEFAULT_TIMEOUT` to the upstream documented 25-second default and use a 35-second child-process watchdog (`PI_AGENT_BROWSER_PROCESS_TIMEOUT_MS` overrides the default 35s budget; top-level `timeoutMs` overrides it per browser CLI call). Explicit `wait <ms>`, `wait --timeout <ms>`, and WebMCP `invoke` / `result --timeout <ms>` calls can exceed that default; when top-level `timeoutMs` is omitted, the wrapper derives a subprocess watchdog from the requested command duration plus a small grace window. Batch budgeting and timeout recovery read the same effective source as upstream: raw command strings when present, otherwise stdin rows. Dialog commands are additionally bounded to 5 seconds (`PI_AGENT_BROWSER_DIALOG_PROCESS_TIMEOUT_MS`), and click/tap/find refs or tokens plus `eval --stdin` snippets that look like alert/confirm/prompt/dialog triggers are bounded to 8 seconds (`PI_AGENT_BROWSER_DIALOG_TRIGGER_PROCESS_TIMEOUT_MS`). A `session info` timeout returns only an exact-session `retry-session-info` status action, without page probes, liveness claims, or changes to existing page/ref state. When a browser-operation watchdog fires, `details.timeoutPartialProgress` may include a planned step list with `status: "unknown"` (including `generatedFrom` labels for generated QA rows). Current URLs and existing files do not establish which steps ran, failed, or remained pending. Multi-step timeouts offer `inspect-current-page-after-timeout` when the target is already verified, rather than guessing a retry position. A single read-only or idempotent step may offer a `retry-timeout-step` candidate with a one-row native batch (`args: ["batch"]` plus `stdin`); inspect the current state before retrying because its outcome is still unknown. If the target is unknown, standalone snapshots are removed and visible failure text plus `details.nextActions` show `verify-page-target-after-timeout`, including its session-scoped `batch --bail` args and short stdin for fail-fast `get url` then `snapshot -i`; dialog status/accept/dismiss actions remain allowed for blocking-dialog recovery. It also includes current page URL from best-effort session `get url`, followed by `get title` only after a URL is recovered (or a planned URL inferred from the step list when the session cannot answer), and declared artifact paths such as `screenshot`, `pdf`, `download`, or `wait --download` outputs with existence/state checks; the same evidence is appended under `Timeout partial progress` in visible text with URL/path redaction.
1085
1087
  - Oversized snapshots and oversized generic outputs may be compacted in tool content, with the full redacted output written to a spill file path shown directly in the tool result. Recent artifact metadata is bounded by `PI_AGENT_BROWSER_SESSION_ARTIFACT_MANIFEST_MAX_ENTRIES` (default 100); persisted spill files have a separate `PI_AGENT_BROWSER_SESSION_ARTIFACT_MAX_BYTES` budget (default 32 MiB; `0` disables automatic eviction).
1086
1088
  - The wrapper keeps `--help` and `--version` stateless so they do not consume the implicit managed-session slot.
1087
1089
 
package/docs/RELEASE.md CHANGED
@@ -268,7 +268,7 @@ These show up often in cloud dev boxes and scripted smokes; they are maintainer
268
268
  | Topic | What to watch for | Mitigation |
269
269
  | --- | --- | --- |
270
270
  | **Pi CLI vs repo devDependencies** | Global `pi` older than the minimum Pi runtime floor for the release can change TUI behavior, `/reload`, package installs, and tool routing during lifecycle or checkout smokes. | Run `npm run doctor` and align `pi` with the current audited baseline before release gates (`pi update` or install the matching version). The published peer range stays wildcard per Pi package docs, and the doctor enforces the minimum Pi runtime floor before package validation. |
271
- | **npm lockfile (`packageManager`)** | `package.json` pins **npm@11**. npm 10 may only strip optional `libc` metadata on `@esbuild/*` platform entries in `package-lock.json` (no dependency version change). Private registry proxies can also leak organization URLs into `resolved` entries. | Prefer `npx -y npm@11.14.0 install` when refreshing the lockfile; do not commit npm-10-only churn or WorkOS/private-registry URLs. |
271
+ | **npm lockfile (`packageManager`)** | `package.json` pins **npm@12.1.0** for the Node 24.21.0 runtime baseline. Older npm versions can rewrite optional-platform metadata in `package-lock.json`; private registry proxies can also leak organization URLs into `resolved` entries. | Prefer `npx -y npm@12.1.0 install` when refreshing the lockfile; do not commit unrelated lockfile churn or WorkOS/private-registry URLs. |
272
272
  | **`pi -p` / print mode** | Non-interactive `pi -p` may hang or emit no stdout for long real-browser smokes without a TTY. | Use **tmux**-driven interactive `pi` for release evidence and checkout smokes; reserve `-p` for short, non-browser checks. |
273
273
  | **Real-browser cleanup** | `real-upstream`, Sauce Demo, and live-site runs can leave defunct Chrome/`agent-browser` children if a session aborts mid-flow. | Close via `agent_browser` / `agent-browser` `close`, kill stray tmux sessions, and remove temp screenshots/HARs under `/tmp` or your chosen artifact dirs. |
274
274
  | **Automated prompt driving** | Grepping tmux pane text for words that also appear in the **user** prompt (`PASS`, `FAIL`, `checkout overview`, `Smoke result:`) can false-complete before the agent finishes. | Wait for pane idle (no `Working…`), `agent_browser close` / `Artifact lifecycle`, or JSONL tool results—not instruction phrases copied from the prompt. |
@@ -324,9 +324,9 @@ node --test test/agent-browser.checkpoint-native.test.mjs
324
324
 
325
325
  #### Routine GitHub PR checks
326
326
 
327
- [`.github/workflows/pi-compatibility.yml`](https://github.com/fitchmultz/pi-agent-browser-native/blob/main/.github/workflows/pi-compatibility.yml) runs on PRs to `main` and manual dispatch. Independent Ubuntu jobs qualify the locked official Pi graph on Node 22.19.0 and the pinned fork's complete installed graph on Node 24.21.0. Each runs `check:compat` once: host identity, typecheck, the full offline suite, and packed SDK/CLI loading. A parallel macOS Node 24 job checks the packed Pi load without repeating the full suite. When the candidate official version differs from the supported Pi floor, that macOS job also selects the actual floor graph for a focused type/package smoke. The host selector uses the pinned `fitchmultz/.github` automation scripts without changing them. There are no cross-job host artifacts or persistent caches.
327
+ [`.github/workflows/pi-compatibility.yml`](https://github.com/fitchmultz/pi-agent-browser-native/blob/main/.github/workflows/pi-compatibility.yml) runs on PRs to `main` and manual dispatch. Independent Ubuntu jobs qualify the locked official Pi graph and the pinned fork's complete installed graph on Node 24.21.0. Each runs `check:compat` once: host identity, typecheck, the full offline suite, and packed SDK/CLI loading. A parallel macOS Node 24.21.0 job checks the packed Pi load without repeating the full suite. When the candidate official version differs from the supported Pi floor, that macOS job also selects the actual floor graph for a focused type/package smoke. The host selector uses the pinned `fitchmultz/.github` automation scripts without changing them. There are no cross-job host artifacts or persistent caches.
328
328
 
329
- [`.github/workflows/native-checkpoint.yml`](https://github.com/fitchmultz/pi-agent-browser-native/blob/main/.github/workflows/native-checkpoint.yml) independently checks out the PR head and runs on the same PR/manual triggers. Its CI-only [`ci-native-checkpoint.sh`](https://github.com/fitchmultz/pi-agent-browser-native/blob/main/scripts/ci-native-checkpoint.sh) builds the pinned fork and compiled extension on Node **26.9.0**, installs stock **agent-browser 0.38.1** and Chrome for Testing, checks generated playbook and live command-reference drift, then requires **13 passed, zero failed/cancelled/skipped/todo** real-browser checkpoints. It does not repeat the official full suite. Sources use a private `/tmp` root, sanitized environment, non-root execution, and a loopback-only network namespace. No personal profile, credentials, or model calls are involved.
329
+ [`.github/workflows/native-checkpoint.yml`](https://github.com/fitchmultz/pi-agent-browser-native/blob/main/.github/workflows/native-checkpoint.yml) independently checks out the PR head and runs on the same PR/manual triggers. Its CI-only [`ci-native-checkpoint.sh`](https://github.com/fitchmultz/pi-agent-browser-native/blob/main/scripts/ci-native-checkpoint.sh) builds the pinned fork and compiled extension on Node **24.21.0**, installs stock **agent-browser 0.38.1** and Chrome for Testing, checks generated playbook and live command-reference drift, then requires **13 passed, zero failed/cancelled/skipped/todo** real-browser checkpoints. It does not repeat the official full suite. Sources use a private `/tmp` root, sanitized environment, non-root execution, and a loopback-only network namespace. No personal profile, credentials, or model calls are involved.
330
330
 
331
331
  CI writes setup, provenance, command-reference, and native TAP results to the job log instead of uploading artifacts. The checks cover their exact source, host, and synthetic browser paths; they do not replace release/manual-publication gates, real-site authentication, or full personal-workspace sleep/resume qualification.
332
332
 
@@ -87,7 +87,7 @@ Current summary:
87
87
  | RQ-0149 | Upstream 0.35.1 fixes snapshot-diff ref lifecycle, active-main-frame stream URL updates, and Windows ARM64 launcher selection without adding commands or flags. The wrapper preserves the 0.35.0 stable floor and accepts newer stable versions without version-specific shims. | [`docs/COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#upstream-0351-rebaseline), [`scripts/agent-browser-target.mjs`](../scripts/agent-browser-target.mjs), `test/agent-browser.upstream-version.test.ts`, `test/agent-browser.real-upstream-contract.test.ts` |
88
88
  | RQ-0150 | Upstream 0.35.2 hardens standalone dashboard origin access and fixes root remote CDP WebSocket query URLs. The wrapper parses `--allowed-origins` as a value and keeps explicit or implicit dashboard-start lifecycle calls sessionless; upstream owns origin validation, tokens, and the CDP fix. | [`docs/COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#upstream-0352-rebaseline), [`docs/TOOL_CONTRACT.md`](TOOL_CONTRACT.md#dashboard-reverse-proxy-origins), `test/agent-browser.runtime.test.ts` |
89
89
  | RQ-0151 | Upstream 0.36.0 adds experimental WebMCP page tools, default local-Chrome enablement with launch-scoped `--no-webmcp`, an opt-in MCP profile, and the `webmcp-gen` skill. The wrapper passes list/invoke/result/cancel through, preserves params/frame/detach/timeout arguments, derives watchdogs from effective direct/raw/stdin commands, keeps pending detached targets unverified, and restores batch target/ref state only after URL verification and a fresh snapshot, while keeping 0.35.0 as the stable floor. | [`docs/COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#upstream-0360-rebaseline), [`docs/TOOL_CONTRACT.md`](TOOL_CONTRACT.md#experimental-webmcp), `test/agent-browser.runtime.test.ts`, `test/agent-browser.wait-timeouts.test.ts`, `test/agent-browser.process.test.ts`, `test/agent-browser.page-target-validation.test.ts`, `test/agent-browser.extension-ref-guards.test.ts`, `test/agent-browser.real-upstream-contract.test.ts` |
90
- | RQ-0152 | Agent feedback closes recovery and evidence gaps without a new runtime layer: unknown-target timeouts return a visibly executable `get url` → `snapshot -i` fail-fast action while dialog recovery remains allowed; successful tab selection/close live-verifies URL/fresh titles and retains deliberate and post-close blank tabs; compacted direct or batched `outputPath` writes use only full command-redacted live wrapper spills and fail rather than save metadata; explicit URL reads/all-read batches avoid browser helpers and managed replacement, while proven native read confirmations preserve exact session routing and capability-gated browser independence; single session-info preflights distinguish daemon/browser/native/Pi ownership; native recording receipts expose capture versus encoded measurements and survive failure/timeout exports, with bounded identity-matched recovery and no file-only success; direct or batched first/fresh local headed launches expose an unverified `browserWindow` handoff while attachments do not; snapshot spills and SAML/OAuth URL values are redacted at model/persistence boundaries; unsupported direct/batch `scrollintoview text=...` fails before upstream false success with visible native recovery payloads while help passes through; and web search sends Exa primary/version guidance, removes exact normalized-URL duplicates without overfetch, and labels provider page dates without claiming crawl/version proof. | [`docs/TOOL_CONTRACT.md`](TOOL_CONTRACT.md), [`docs/COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md), [`docs/ARCHITECTURE.md`](ARCHITECTURE.md), `test/agent-browser.extension-errors-artifacts.test.ts`, `test/agent-browser.extension-tabs.test.ts`, `test/agent-browser.extension-validation.test.ts`, `test/agent-browser.page-target-validation.test.ts`, `test/agent-browser.runtime.test.ts`, `test/agent-browser.snapshot-presentation.test.ts`, `test/agent-browser.web-search.test.ts`, `test/agent-browser.real-upstream-contract.test.ts` |
90
+ | RQ-0152 | Agent feedback closes recovery and evidence gaps without a new runtime layer: unknown-target timeouts return a visibly executable `get url` → `snapshot -i` fail-fast action while dialog recovery remains allowed; successful tab creation/selection/close, including dispatched batch rows, live-verifies URL/fresh titles and retains new, selected, and post-close blank tabs; compacted direct or batched `outputPath` writes use only full command-redacted live wrapper spills and fail rather than save metadata; explicit URL reads/all-read batches avoid browser helpers and managed replacement, while proven native read confirmations preserve exact session routing and capability-gated browser independence; single session-info preflights distinguish daemon/browser/native/Pi ownership; native recording receipts expose capture versus encoded measurements and survive failure/timeout exports, with bounded identity-matched recovery and no file-only success; direct or batched first/fresh local headed launches expose an unverified `browserWindow` handoff while attachments do not; snapshot spills and SAML/OAuth URL values are redacted at model/persistence boundaries; unsupported direct/batch `scrollintoview text=...` fails before upstream false success with visible native recovery payloads while help passes through; and web search sends Exa primary/version guidance, removes exact normalized-URL duplicates without overfetch, and labels provider page dates without claiming crawl/version proof. | [`docs/TOOL_CONTRACT.md`](TOOL_CONTRACT.md), [`docs/COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md), [`docs/ARCHITECTURE.md`](ARCHITECTURE.md), `test/agent-browser.extension-errors-artifacts.test.ts`, `test/agent-browser.extension-tabs.test.ts`, `test/agent-browser.extension-validation.test.ts`, `test/agent-browser.page-target-validation.test.ts`, `test/agent-browser.runtime.test.ts`, `test/agent-browser.snapshot-presentation.test.ts`, `test/agent-browser.web-search.test.ts`, `test/agent-browser.real-upstream-contract.test.ts` |
91
91
 
92
92
  QA truthfulness (#121, RQ-0145) is covered by the registered-tool controls in `test/agent-browser.extension-input-modes.test.ts` and the strict `contract suite matches QA non-pass after same-URL error-buffer rollover` case in `test/agent-browser.real-upstream-contract.test.ts`. The real fixture fills the native error FIFO with identical rows, then verifies that a new document actually throws again at the same URL and that QA is always non-pass. Matched rows fail as an unverified check, not an application-error assertion. Empty/cleared and explicit `checkErrors: false` controls preserve valid passes; the wrapper does not fix upstream `errors --clear` or accept a warning-only pass as resolution.
93
93
 
@@ -103,7 +103,7 @@ Upstream 0.38.1 observation and input compatibility is covered by `test/agent-br
103
103
 
104
104
  Upstream 0.37 recording compatibility uses one command-local path/URL reader around numeric `--fps` pairs; direct, raw-batch and stdin-batch preflight reject destination collisions before native dispatch. FPS-only calls keep the intended pinned tab, and FPS-only restarts keep refs. Every dispatched start still requires fresh refs to protect older supported natives. `test/agent-browser.artifact-diagnostics.test.ts`, `test/agent-browser.batch-fidelity.test.ts` and presentation tests cover these boundaries. The native contract also checks positive/absent WebMCP hints and first-load header inheritance/clearing without custom tab setup. Short/cold native Ubuntu captures can fail to encode; the FPS fixture explicitly logs 12-second captures and verifies output rather than treating an empty file as a frame.
105
105
 
106
- Artifact argv and timeout recovery (#119, RQ-0076) use native operand positions, retain literal global-looking tokens inside batch rows, and select raw batch commands instead of ignored stdin. Executable timeout retries preserve the original row in a one-row batch. Interleaved `wait --download` / `-d` timeout pairs keep the next retained path and its original index. Runtime, artifact-diagnostics, and timeout tests cover extraction, reservation, preparation, and presentation; `test/agent-browser.batch-fidelity.test.ts` adds real native saved-file, missing wait-download, reservation, and watchdog evidence under `npm run verify -- real-upstream`. Getter batching amortizes one extra ref-freshness snapshot per top-level call or batch when the existing checks apply. Direct `session info` timeouts stay status-only (`retry-session-info`), preserving page/ref/reopen state; cold-boundary and result tests cover this.
106
+ Artifact argv and timeout recovery (#119, RQ-0076) use native operand positions, retain literal global-looking tokens inside batch rows, and select raw batch commands instead of ignored stdin. Timeout observations retain current page and artifact existence evidence while all unacknowledged planned rows stay unknown; neither a matching URL nor an existing file proves execution. Multi-step timeouts offer inspection without inferring a failed/pending boundary. Executable retry candidates for a single retryable step preserve the original row in a one-row batch. Interleaved `wait --download` / `-d` timeout pairs keep the next retained path and its original index. Runtime, artifact-diagnostics, and timeout tests cover extraction, reservation, preparation, and presentation; `test/agent-browser.batch-fidelity.test.ts` adds real native saved-file, missing wait-download, reservation, and watchdog evidence under `npm run verify -- real-upstream`. Getter batching amortizes one extra ref-freshness snapshot per top-level call or batch when the existing checks apply. Direct `session info` timeouts stay status-only (`retry-session-info`), preserving page/ref/reopen state; cold-boundary and result tests cover this.
107
107
 
108
108
  Cold URL reopen is covered by `test/agent-browser.cold-resume.test.ts`, the daemon/page-boundary regressions in `test/agent-browser.cold-boundaries.test.ts`, and the opt-in `contract suite matches cold URL reopen after quit` case in `test/agent-browser.real-upstream-contract.test.ts`. Deterministic boundary tests cover non-page prefixes, fragment routes, history commands, explicit destinations, transcript replay, unreached navigation and unchanged native batch/error order. `test/agent-browser.destination-cancel.test.ts` covers direct/native-batch new-window and URL-diff follow-ups, redirected/unknown targets (including blank redirects with a remembered duplicate tab), ordered ref invalidation, reached versus unreached rows, and cancellation before or after a cold-reopen attempt with replay and process cleanup. Release acceptance requires the first page operation after confirmed shutdown to observe the remembered non-root URL with both empty and origin storage, including after non-page calls and on hash-routed pages, before any explicit open, with refreshed refs. Focused native Mac evidence does not replace actual Ubuntu, real Pi lifecycle, checkout, live-site, or final release gates.
109
109
 
@@ -147,7 +147,7 @@ Re-run the gates below before each release. Dated outcomes in this table are his
147
147
  | Configured-source lifecycle | `npm run verify -- lifecycle` (`scripts/verify-lifecycle.mjs`) drives `/reload`, closes and relaunches Pi with the same exact `--session-id`, checks the JSONL session header id, session continuity, slash-command sentinel tokens (`v1` before reload and `v2` after full relaunch because compiled JS package modules are process-cached), persisted spill reachability, and real Pi `tool_result` failure-patch semantics for a QA reclassification with a fake upstream on `PATH`. Default Pi model is `zai/glm-5.2`; default per-step wait is **180000 ms** (`DEFAULT_TIMEOUT_MS`); override model with `--model <id>` and waits with `--timeout-ms <ms>`. Passthrough flags in [`scripts/project.mjs`](https://github.com/fitchmultz/pi-agent-browser-native/blob/main/scripts/project.mjs): `--keep-artifacts`, `--model`, `--verbose`, and `--timeout-ms` plus a value (for example `npm run verify -- lifecycle --model openai-codex/gpt-5.5:minimal --keep-artifacts --verbose --timeout-ms 600000`). | **Pi 0.84.4 on 2026-09-01:** pass; `/reload`, exact-session relaunch, managed continuity, persisted spill reachability, and Pi failure-patch behavior all verified. |
148
148
  | Quick isolated Pi smoke | `pi --approve --no-extensions --no-skills -e .` from trusted repo root; checkout-native browser tools only. | **Earlier 0.36.0 / package 0.6.2 / Pi 0.84.4:** isolated checkout tmux smoke passed on 2026-09-01; native `agent_browser` completed version, localhost open, WebMCP list/invoke, visible-text verification, and close. |
149
149
 
150
- Runtime floor note: package metadata keeps optional Pi core package peer ranges wildcard per installed Pi package docs, pins direct Pi validation dependencies to 0.87.0, and makes `pi --version` below 0.87.0 a setup failure through `pi-agent-browser-doctor` / `npm run doctor`. The 0.7 floor follows the native tool-history/prompt APIs and includes no older-host compatibility shim.
150
+ Runtime floor note: package metadata requires Node 24.21.0 or newer, keeps optional Pi core package peer ranges wildcard per installed Pi package docs, pins direct Pi validation dependencies to 0.87.1, and makes `pi --version` below 0.87.0 a setup failure through `pi-agent-browser-doctor` / `npm run doctor`. The Pi floor follows the native tool-history/prompt APIs and includes no older-host compatibility shim.
151
151
 
152
152
  ## Baseline checklist by inventory section
153
153
 
@@ -155,7 +155,7 @@ Runtime floor note: package metadata keeps optional Pi core package peer ranges
155
155
  | --- | --- | --- | --- | --- | --- |
156
156
  | Built-in skills | 19 canonical tokens from baseline section `skills`; see [`scripts/agent-browser-capability-baseline.mjs`](../scripts/agent-browser-capability-baseline.mjs) and generated [`COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#built-in-skills). | [`COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#built-in-skills), generated baseline block, README proof section, release docs. | `needsManagedSession` keeps read-only skills inspection sessionless while preserving thin upstream passthrough; upstream `@agent-browser/sandbox` remains external package guidance, not a bundled wrapper dependency. | Runtime and extension-validation skills/provider matrix; real-upstream inspection/skills group. | Supported. |
157
157
  | Core page, element, navigation, and extraction commands | Canonical tokens from baseline section `core-commands`; see [`scripts/agent-browser-capability-baseline.mjs`](../scripts/agent-browser-capability-baseline.mjs) and generated [`COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#core-page-and-element-commands). | [`COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#core-page-and-element-commands), [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md), README quick start. | Thin passthrough with wrapper-owned JSON/session planning, ref guidance, artifact verification, page-change summaries, click-dispatch diagnostics only after native temporary-marker target identity readback (otherwise abstaining), inspection-only recovery for upstream overlay-blocked clicks, deterministic document scroll before wheel/no-op diagnostics and instant CSS-container movement, shorthand compilers, and redaction. | Real-upstream core matrix (including snapshot-ref select) plus fake core matrix for passthrough, ordering, diagnostics, and compiler validation. | Supported. Upstream semantics remain upstream-owned. |
158
- | Sessions, state, tabs, frames, dialogs, and windows | 28 canonical tokens from baseline section `state-tabs-frames-dialogs`; see [`scripts/agent-browser-capability-baseline.mjs`](../scripts/agent-browser-capability-baseline.mjs) and generated [`COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#session-state-frames-dialogs-windows-and-inspection-commands). | [`COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#session-state-frames-dialogs-windows-and-inspection-commands), stateful workflow notes, [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#details). | Stateful summaries redact credentials while preserving restore identifiers and every session/state list row; explicit targets and paths pass through. Runtime retains state artifact verification, sessionless local command planning, automatic managed restore, tab target pinning, active-target refresh after tab close, and close alias cleanup. | Extension tab/ref tests, real-upstream stable-id/label tab lifecycle, runtime session/resume tests, presentation redaction tests, lifecycle harness. | Supported. External profile/auth state remains operator-owned. |
158
+ | Sessions, state, tabs, frames, dialogs, and windows | 28 canonical tokens from baseline section `state-tabs-frames-dialogs`; see [`scripts/agent-browser-capability-baseline.mjs`](../scripts/agent-browser-capability-baseline.mjs) and generated [`COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#session-state-frames-dialogs-windows-and-inspection-commands). | [`COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#session-state-frames-dialogs-windows-and-inspection-commands), stateful workflow notes, [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#details). | Stateful summaries redact credentials while preserving restore identifiers and every session/state list row; explicit targets and paths pass through. Runtime retains state artifact verification, sessionless local command planning, automatic managed restore, native-CDP-ID tab recovery across duplicate URLs and transcript replay, reached failed-batch target invalidation/live verification, active-target refresh after tab close, and close alias cleanup. | Extension tab/ref tests, real-upstream stable-id/label tab lifecycle, runtime session/resume tests, presentation redaction tests, lifecycle harness. | Supported. External profile/auth state remains operator-owned. |
159
159
  | Network, storage, artifacts, diagnostics, and performance | Canonical tokens from baseline section `network-storage-artifacts-diagnostics`; see [`scripts/agent-browser-capability-baseline.mjs`](../scripts/agent-browser-capability-baseline.mjs) and generated [`COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#page-state-finding-mouse-settings-network-and-storage). | [`COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#page-state-finding-mouse-settings-network-and-storage), diagnostic sections, [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#details). | Thin passthrough plus native-owned click/download execution for all downloads (including loopback, Blob exports, and redirects), compact diagnostics with identical credential redaction for page/origin/URL-filtered details and exports, route-mock warnings, useful-but-redacted storage output, stream idempotency normalization, FPS/cursor/contact-sheet-aware recording destinations/pinning and contact-sheet image verification, truthful ref-refresh and missing-ffmpeg warnings, positive WebMCP navigation hints and catalog updates, sensitive-data redaction, timeout bounds, and cleanup-pair guidance. | Fake non-core matrix and safe real-upstream coverage for network/HAR, diff, trace/profiler, console/errors/highlight, stream, vitals, React missing-renderer, native FPS recording path/page checks and WebMCP navigation hints. | Supported. Environment-sensitive operations need suitable local/browser state. |
160
160
  | Batch, auth, confirmations, setup, dashboard, devices, and AI commands | Canonical tokens from baseline section `batch-auth-setup-ai`; see [`scripts/agent-browser-capability-baseline.mjs`](../scripts/agent-browser-capability-baseline.mjs) and generated [`COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#batch-auth-confirmations-sessions-chat-dashboard-devices-and-setup). | [`COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#batch-auth-confirmations-sessions-chat-dashboard-devices-and-setup), README security notes, release docs. | Native-tool batch stdin, generated QA/lookup batch plans, auth/confirmation redaction, sessionless local auth/setup/dashboard/doctor/plugin planning, upgrade-only native text normalization with error precedence and explicit-JSON results, plugin list/show JSON envelope normalization, bare-`mcp` validation with `mcp --help` preserved, timeout/cleanup guidance. | Parser/runtime plugin and MCP unit coverage; fake-upstream plugin list/show and MCP help/blocking coverage; registered upgrade text/success/nonzero/timeout/abort/JSON controls in `test/agent-browser.upgrade-output.test.ts`; real-upstream plugin list shape probe; structured input-mode tests. | Supported. Interactive side-effecting setup/auth/chat remains upstream-owned. `plugin` is local/sessionless; `mcp` is external-client-only except help; `auth login --credential-provider` resolves credentials via a plugin; `install --with-deps` failures remain upstream-owned. |
161
161
  | Global flags, config, providers, policy, and environment | Canonical tokens from baseline section `options-and-env`; see [`scripts/agent-browser-capability-baseline.mjs`](../scripts/agent-browser-capability-baseline.mjs) and generated [`COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#important-global-flags-config-and-environment). | [`COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#important-global-flags-config-and-environment), README provider/setup notes, [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#sessionmode), architecture/runtime docs. | Runtime preserves caller-requested JSON for early filters, scroll results, and preparation failures without output-file prose (help/version remains native text), and handles command discovery, value-flag prevalidation, launch-scoped flags, redacted echoes, fresh-session recovery hints, explicit sessions, provider/device launch-scoping, parent env forwarding with consistent managed idle-timeout/socket/operation-timeout overrides on top-level and helper subprocesses, subprocess completion, and package-owned Pi-scoped config for optional companion features. | Runtime tests for flags/planning/redaction/session behavior; process/ref tests for env consistency and stdio-linger completion; config/web-search/CLI tests; fake provider/specialized-skill matrix; package doctor. | Supported. Provider clouds, iOS/Appium, proxies, profiles, and credentials require external setup. |
@@ -92,6 +92,7 @@ Schema:
92
92
 
93
93
  Provider notes:
94
94
  - `provider` is optional; `auto` uses available keys plus `webSearch.preferredProvider`.
95
+ - `offset` skips results, not pages, for both providers. Each fetches the first `count + offset` results and slices locally; the maximum request is 19 results (`count: 10`, `offset: 9`), within Brave's 20-result page limit.
95
96
  - `searchType` applies to Exa only and supports `auto`, `fast`, `instant`, `deep-lite`, `deep`, and `deep-reasoning`. Effective precedence is the per-call field, then `webSearch.defaultSearchType`, then `auto`. Use `deep-lite` for implementation research, `deep` for hard multi-source work, and `deep-reasoning` only for the hardest or exhaustive work.
96
97
  - `includeDomains` and `excludeDomains` accept 1–20 Exa hostname, path-prefix, or wildcard-subdomain strings. `category` accepts `company`, `people`, `publication`, `news`, `personal site`, or `financial report`. `company` and `people` reject `freshness` and `excludeDomains` before a request is sent.
97
98
  - `additionalQueries` accepts 1–10 strings only when the effective type is `deep-lite`, `deep`, or `deep-reasoning`. HTTP timeouts are 15 seconds for non-deep types, 45 seconds for `deep-lite`, 60 seconds for `deep`, and 90 seconds for `deep-reasoning`.
@@ -690,7 +691,8 @@ Additional structured fields can appear when relevant:
690
691
  - `attachedBrowserSession: true` on successful calls that establish or reuse a wrapper-tracked CDP/auto-connect/Electron attachment, and on a failed fresh attachment only when `managedSessionOutcome.activeAfter` proves its daemon remained active for cleanup. The marker restores attachment continuity from the active transcript branch, including that active-after-failure case; live state and transcript replay remove it after a terminal successful close/cleanup even when aggregate verification failed; a close followed by a later step whose lifecycle reports a browser launch preserves it, while a successful non-launching diagnostic leaves the close terminal. Caller config, environment, paths, and file-access settings remain upstream-owned; the marker only adds live-URL verification and lifecycle continuity.
691
692
  - `lifecycle: { effectiveLaunch: { browserLaunched } }` when upstream returned that boolean. It separates starting the requested `agent-browser` CLI process from the effective Chrome session context. `readSource` exposes upstream's string `data.source` for direct `read` calls and identifies the raw HTTP fetch path; its lifecycle boolean can be `false` before a browser launch or `true` when the same managed session already has an active browser. Direct reads also append one visible `Read execution` line with the source, CLI-start result, managed browser lifecycle, and managed-session outcome so Pi models do not have to infer model-invisible details.
692
693
  - `browserWindow: { mode: "headed", ownership: "wrapper-managed", sessionName, visibility: "unverified" }` only after a successful first/fresh local wrapper-managed headed call (including `batch`) that is not an attachment and whose lifecycle proves a browser launched and whose managed-session outcome is `created` or `replaced`. One visible handoff sentence tells the user to complete the login in that window if they can see it, then continue with `sessionMode: "auto"`; the field never claims OS desktop visibility.
693
- - `sessionTabTargetUnknown: true` after a spawned `connect`, `state load`, history navigation, tab-selection/close, `window new`, or `diff url` call changes the active page without a trustworthy observed target. Direct `window new` / `diff url` and reached native batch rows retire the old target and refs; an intentional new blank window or an observed blank URL-diff destination never triggers old-tab recovery. URL diff inputs do not prove the final URL after redirects. Successful standalone tab selection/close now live-probes URL and a fresh non-blank title before state is committed, even when the new tab shares the prior URL; explicit selection of an existing `about:blank` tab and a post-close blank target are retained, so this marker remains only when the probe cannot verify the target. It is persisted and restored across branch/reload replay, clears stale refs and tab pinning, and blocks page inspection until `get url` or explicit navigation observes a target; `tab list`, tab selection, close, and blocking-dialog `status` / `accept` / `dismiss` remain available. A timeout against an unknown target removes standalone snapshot actions and returns `verify-page-target-after-timeout`, a session-scoped `batch --bail` whose stdin runs `get url` before `snapshot -i`.
694
+ - `sessionTabTarget` retains the observed `url`, optional `title`, and native CDP `targetId` when available. When an observed page lacks a native ID, a read-only `tab list` observes current identity; earlier commands and equal URLs cannot supply it. If another tab matches the observed URL while the active tab does not, existing URL-based drift recovery selects the matching tab instead of attaching the unrelated active ID. Resume and branch recovery select that exact target, even when several tabs share an address/title; a missing known ID never falls back to another same-address tab. Older transcripts without an ID retain URL-based recovery. An existing cold-managed-browser reopen records its new native ID. `sessionTabCorrection.selectionKind` can be `targetId`, `tabId`, `label`, or `index`.
695
+ - `sessionTabTargetUnknown: true` after a spawned `connect`, `state load`, history navigation, tab-selection/close, `window new`, or `diff url` call changes the active page without a trustworthy observed target. Direct `window new` / `diff url` and reached native batch rows retire the old target and refs; an intentional new blank window or an observed blank URL-diff destination never triggers old-tab recovery. URL diff inputs do not prove the final URL after redirects. Successful tab creation/selection/close, including dispatched batch rows, live-probes URL and a fresh non-blank title before state is committed, even when the new tab shares the prior URL; new blank tabs, explicitly selected `about:blank` tabs, and post-close blank targets are retained. It is persisted and restored across branch/reload replay, clears stale refs and tab pinning, and blocks page inspection until `get url` or explicit navigation observes a target; `tab list`, tab selection, close, and blocking-dialog `status` / `accept` / `dismiss` remain available. A timeout against an unknown target removes standalone snapshot actions and returns `verify-page-target-after-timeout`, a session-scoped `batch --bail` whose stdin runs `get url` before `snapshot -i`.
694
696
  - `compiledSemanticAction` when the call used `agent_browser_action` and the result includes the unified `details` merge: `{ action, locator, args }` for `find` actions or `{ action: "select", selector?, locator?, values, args }` for `select`, with the same redaction rules as `args` / `effectiveArgs`; omitted for plain direct native calls and omitted on some early error returns that omit this field (see the `agent_browser_action` section above)
695
697
  - `compiledJob` is internal QA batch-plan metadata: `{ args: ["batch", "--bail"], failFast: true, stdin, steps }`, with redacted step argv. It does not expose a public job input or compiler.
696
698
  - `compiledQaPreset` when the call used `agent_browser_qa`: the compiled native batch fields plus the QA `checks` object. `args` is `batch --bail` and `failFast` is `true` for QA presets. `checks.attached` is `true` for current-session QA, `checks.url` is present only for URL-opening QA, and `checks.diagnosticsResetAtStart` is `true` only for URL-opening QA because `agent_browser_qa` with `attached: true` preserves existing session diagnostics.
@@ -702,7 +704,7 @@ Additional structured fields can appear when relevant:
702
704
  - `compiledElectron` when the call used `agent_browser_electron`: redacted action plan for `list`, `launch`, `status`, `cleanup`, or `probe`.
703
705
  - `electron` when the call used `agent_browser_electron`: action-specific lifecycle, discovery, probe, and cleanup data; see the `agent_browser_electron` contract above.
704
706
  - `batchFailure` and `batchSteps` for `batch` rendering, including mixed-success runs
705
- - `navigationSummary` for navigation-style commands like `click`, `back`, `forward`, `reload`, `window new`, `diff url`, and successful standalone tab selection/close; reached window-new/URL-diff rows also probe the final page after native batches, including failed batches that may already have changed the page (not aborted or timed-out calls). Unreached rows do not change target or ref state; `urlChanged` records whether the live URL differs from a known pinned pre-command URL, so same-URL clicks and clicks without a comparison baseline remain dispatch-only rather than being mislabeled as observed navigation. Helper probes run `get url` first and run `get title` for any verified non-`about:blank` URL. The title read is skipped when the probed URL already carries a wrapper-observed title for this session, except after a tab selection/close, which always refreshes a non-blank title even when the URL is unchanged: titles are last-observed labels for that URL, while the URL itself is live-probed on every call. Href-less CSS selector clicks use this same post-command helper so `sessionTabTarget` cannot stay on the pre-click page; any click-dispatch check still runs first. A failed non-batch `eval`, `back`, `forward`, `reload`, `connect`, `state load`, or `tab` selection also runs this helper (browser started, not aborted, not watchdog-timed-out), so an observed page stays verified instead of forcing a manual `get url` round trip; a failed or empty probe keeps the prior unverified-page behavior. Because a failed transition can still have mutated or replaced the document, a successful probe also invalidates the prior page-scoped ref snapshot (matching the previous unknown-target behavior, which dropped refs), so the next `@e…` use requires a fresh `snapshot -i`.
707
+ - `navigationSummary` for navigation-style commands like `click`, `back`, `forward`, `reload`, `window new`, `diff url`, and successful tab creation/selection/close (including dispatched batch rows); reached window-new/URL-diff rows also probe the final page after native batches, including failed batches that may already have changed the page (not aborted or timed-out calls). Unreached rows do not change target or ref state; `urlChanged` records whether the live URL differs from a known pinned pre-command URL, so same-URL clicks and clicks without a comparison baseline remain dispatch-only rather than being mislabeled as observed navigation. Helper probes run `get url` first and run `get title` for any verified non-`about:blank` URL. The title read is skipped when the probed URL already carries a wrapper-observed title for this session, except after a tab creation/selection/close, which always refreshes a non-blank title even when the URL is unchanged: titles are last-observed labels for that URL, while the URL itself is live-probed on every call. Href-less CSS selector clicks and reached navigation-observable actions inside successful native batches use this same post-command helper so `sessionTabTarget` cannot stay on the pre-action page, including when a popup shares the original tab's URL and title. Terminal batch closes skip the helper; pending WebMCP results still keep the target unknown. Any click-dispatch check still runs first. A failed `eval`, `back`, `forward`, `reload`, `connect`, `state load`, or `tab` selection also runs this helper, including reached batch transitions and failed batch `open` rows (browser started, not aborted, not watchdog-timed-out), so an observed page stays verified instead of forcing a manual `get url` round trip; a failed or empty probe keeps the prior unverified-page behavior. Reached transition rows discard earlier batch targets even when the row fails; replay retains the final live observation. If native tab-list metadata still shows an attempted URL while `get url` reports Chrome's error page, recovery uses the exact native target and verifies its live URL. Because a failed transition can still have mutated or replaced the document, a successful probe also invalidates the prior page-scoped ref snapshot (matching the previous unknown-target behavior, which dropped refs), so the next `@e…` use requires a fresh `snapshot -i`.
706
708
  - `pageChangeSummary` for compact mutation/artifact/navigation summaries on commands that can change browser state
707
709
  - `clickDispatch` when a top-level non-Electron direct `click` reported upstream success but the target-specific probe found no trusted event reached the resolved XPath or accessible `@ref` target; shape follows `ClickDispatchDiagnostic` in `extensions/agent-browser/lib/orchestration/browser-run/types.ts`
708
710
  - `promptGuard` when the requested-artifact-before-close guard blocks browser close before required prompt artifact paths are verified; implementation lives in `extensions/agent-browser/lib/orchestration/browser-run/prompt-guards.ts`
@@ -723,7 +725,7 @@ Additional structured fields can appear when relevant:
723
725
  - `electronGetTextScopeWarning` after a successful wrapper-tracked attached Electron `get text <selector>` (standalone or successful `batch`) when a broad non-ref CSS selector such as `body`, `html`, `main`, `div`, or `[role=application]` may read the whole app shell. Ordinary browser pages do not qualify without wrapper-owned Electron launch provenance. Shape: `{ selector, summary, electronContext: { launchId?, sessionName?, url? } }`; multiple batched diagnostics use `electronGetTextScopeWarnings`. Visible text appends `Broad Electron get text selector warning`, and next actions use `snapshot-for-electron-text-scope` ids with session-scoped `snapshot -i` payloads.
724
726
  - `evalStdinHint` after a successful `eval --stdin` when caller stdin (trimmed) looks function-shaped to the wrapper’s lightweight detector (in `extensions/agent-browser/lib/orchestration/browser-run/diagnostics.ts`: leading `function` / `async function`, parenthesized arrow `(…) =>`, or a concise `name =>` / `async name =>` form) **and** upstream JSON `data` is an object whose `result` field is a plain empty object (`{}`). Arrays such as `[]` do not qualify. It includes `reason` and `suggestion`; visible output appends `Eval stdin hint` with the same guidance. This is a heuristic for the common mistake of returning a function object instead of invoking it or passing a plain expression, not a JavaScript parser or proof that the page returned no useful data. Before this diagnostic path runs, the wrapper also recovers the common malformed native-tool call `args: ["eval", "--stdin", "..."]` with no top-level `stdin` by moving trailing `args` tokens after `--stdin` into the process stdin stream.
725
727
  - `evalResultWarning` after a successful `eval --stdin` when the current or prior page URL is `file:` (from navigation summary, session tab target, or persisted session page state), upstream JSON `data.result` is strictly `null`, and stdin is non-empty and not a trivial literal `null`/`undefined`. Fields: `reason`, `suggestion`. Visible output appends `Eval result warning` without failing the tool. Use snapshot -i, ref-based getters, screenshots, or http(s) fixtures when file:// null results are inconclusive.
726
- - `timeoutPartialProgress` after `runAgentBrowserProcess` reports `timedOut` (wrapper child-process watchdog) when best-effort recovery finds useful context. `summary` is a short sentence counting recovered planned-step state and declared artifact paths, plus whether page context came from live session reads or only from a planned URL (when nothing in the plan declares an artifact path, the fraction may read `0/0` while `currentPage` can still carry session or planned URL context). `steps` lists planned argv from the compiled QA batch plan (`compiledJob` remains its internal metadata name) or, when that object is absent, from the effective upstream `batch` source: raw argument command strings exclusively when present, otherwise JSON-array stdin, whether caller-authored or wrapper-generated by advanced source tools (1-based indices). Ignored stdin does not contribute recovery steps or artifact evidence. Generated QA rows may include `generatedFrom`. Each step includes `status` (`completed`, `failed`, `pending`, or `unknown`) and optional `reason`; the first incomplete step becomes `retryStep`, but `retry` and top-level `retry-timeout-step` are emitted only for read-only or idempotent commands such as waits, snapshots, screenshots, navigation, and diagnostics. Each retry uses `args: ["batch"]` with `stdin` containing the one original row, preserving native row operands instead of reinterpreting them as outer CLI globals. Mutating steps such as clicks, fills, keyboard typing, presses, selects, or checks are still identified as the first incomplete step but omit executable retry args because they may already have run; when the timed-out session is still usable and its target is already verified, `details.nextActions` can instead include `inspect-current-page-after-timeout` (`snapshot -i`) so the agent verifies current state before continuing with a shorter split flow. When the target is unknown, every standalone snapshot action is removed and replaced by one executable `verify-page-target-after-timeout` action: a session-scoped `batch --bail` with stdin `[["get","url"],["snapshot","-i"]]`, so snapshot runs only after the wrapper's page-target guard is satisfied; visible failure text prints those redacted args and short stdin rather than pointing only to structured details. Dialog `status`, `accept`, and `dismiss` remain allowed while the target is unknown so timeout dialog recovery actions are executable. When a retryable step timed out during `sessionMode: "fresh"` and no live URL was recovered, `retry-timeout-step` uses top-level `sessionMode: "fresh"` instead of prefixing the abandoned generated session name. `currentPage` comes from session-scoped `get url` followed by `get title` when the session answers, otherwise a fallback URL may be inferred from the last `open` / `navigate` / `pushstate` step in the plan; `liveUrlRecovered` is true only when the wrapper recovered a live URL, so planned URLs are not treated as proof that the page actually opened. `openedButPostOpenTimedOut` is set when a live opened page was recovered and a later step appears to have timed out. `artifacts` covers declared output paths on `screenshot`, `pdf`, `download`, and `wait --download` steps (absolute path, existence, `state`, optional `sizeBytes`, `stepIndex`). It uses native operand positions, including literal dash-leading paths: first operand for `pdf`, second for `download`, and the next retained operand after the first timeout pair is removed for `wait --download` / `-d`. Visible text repeats the same block under `Timeout partial progress`, applying URL and path-segment redaction; the prose `Planned steps` list shows at most six steps, then an omitted-count line when the plan is longer. This is recovery evidence only; missing entries do not prove the upstream step never ran or that no other side effects occurred.
728
+ - `timeoutPartialProgress` after `runAgentBrowserProcess` reports `timedOut` (wrapper child-process watchdog) when best-effort recovery finds useful context. `summary` counts planned steps with unknown outcomes and observed declared artifact paths, plus whether page context came from live session reads or only from a planned URL. An existing file or matching URL is current-state evidence, not proof that any planned step ran. `steps` lists planned argv from the compiled QA batch plan (`compiledJob` remains its internal metadata name) or, when that object is absent, from the effective upstream `batch` source: raw argument command strings exclusively when present, otherwise JSON-array stdin, whether caller-authored or wrapper-generated by advanced source tools (1-based indices). Ignored stdin does not contribute recovery steps or artifact evidence. Generated QA rows may include `generatedFrom`. Each step has `status: "unknown"` and a `reason`: the watchdog fired before upstream reported its outcome. Multi-step timeouts do not identify a completed prefix, failed step, pending suffix, or retry position. For a single read-only or idempotent step such as a wait, snapshot, screenshot, navigation, or diagnostic, `retryStep.retry` and `retry-timeout-step` may offer a candidate after inspection; its outcome remains unknown. Each retry uses `args: ["batch"]` with `stdin` containing the original row, preserving native row operands instead of reinterpreting them as outer CLI globals. Otherwise, when the target is already verified, `inspect-current-page-after-timeout` (`snapshot -i`) lets the agent inspect state before deciding how to resume. When the target is unknown, every standalone snapshot action is removed and replaced by one executable `verify-page-target-after-timeout` action: a session-scoped `batch --bail` with stdin `[["get","url"],["snapshot","-i"]]`, so snapshot runs only after the wrapper's page-target guard is satisfied; visible failure text prints those redacted args and short stdin rather than pointing only to structured details. Dialog `status`, `accept`, and `dismiss` remain allowed while the target is unknown so timeout dialog recovery actions are executable. When a retryable step timed out during `sessionMode: "fresh"` and no live URL was recovered, `retry-timeout-step` uses top-level `sessionMode: "fresh"` instead of prefixing the abandoned generated session name. `currentPage` comes from session-scoped `get url` followed by `get title` when the session answers, otherwise a fallback URL may be inferred from the last `open` / `navigate` / `pushstate` step in the plan; `liveUrlRecovered` is true only when the wrapper recovered a live URL, so planned URLs are not treated as proof that the page actually opened. `artifacts` reports post-timeout path existence, not creation by this invocation or step completion. It covers declared output paths on `screenshot`, `pdf`, `download`, and `wait --download` steps (absolute path, existence, `state`, optional `sizeBytes`, `stepIndex`). It uses native operand positions, including literal dash-leading paths: first operand for `pdf`, second for `download`, and the next retained operand after the first timeout pair is removed for `wait --download` / `-d`. Visible text repeats the same block under `Timeout partial progress`, applying URL and path-segment redaction; the prose `Planned steps` list shows at most six steps, then an omitted-count line when the plan is longer. This is recovery evidence only; missing entries do not prove the upstream step never ran or that no other side effects occurred.
727
729
  - `managedSessionHeadedAutosaveInterval` on active/current-after-failure wrapper-owned headed session rows, containing the canonical effective launch-time `AGENT_BROWSER_AUTOSAVE_INTERVAL_MS` string (invalid or out-of-range explicit values resolve to upstream's `"30000"` default). It is `"0"` for the wrapper default and can hold an explicit interval such as `"1000"`; transcript replay and still-owned off-current helpers reapply the recorded value. It is omitted for sessionless, headless, caller-owned, abandoned, and closed calls. If a resumed Pi process explicitly requests a different value in either direction, non-close calls fail with close-plus-fresh recovery guidance while close still uses the recorded daemon value.
728
730
  - `managedSessionHeadedAutosaveDisabled: true` is the narrower compatibility marker that the targeted session uses the wrapper's default interval `0`, rather than an explicit caller interval. It accompanies `managedSessionHeadedAutosaveInterval: "0"` on active rows and remains omitted for explicitly configured autosave.
729
731
  - `managedSessionOutcome` after a managed-session plan reaches process execution (`buildManagedSessionOutcome` / `formatManagedSessionOutcomeText` in `extensions/agent-browser/lib/orchestration/browser-run/session-state.ts`). Populated when `buildExecutionPlan` injects an extension-managed implicit or fresh `--session`, and also when a successful explicit `--session <current-wrapper-managed-session> close` closes the current managed session. It remains omitted for unrelated explicit user-managed sessions and for sessionless inspection/local paths that skip injection. Successful nested-batch lifecycle rows are evaluated in order: a terminal close reports and replays `status: "closed"` even when aggregate artifact verification makes the tool result fail; a later lifecycle-proven browser launch (including a post-close `record stop`) keeps the session active, an explicitly non-launching diagnostic leaves it closed, and an unknown row stays conservatively active even when the failed batch was the first managed call. Fields: `status` (`created`, `replaced`, `unchanged`, `closed`, `preserved`, or `abandoned`), `sessionMode`, `attemptedSessionName`, `previousSessionName`, `currentSessionName`, optional `currentSessionNamespace`, optional `replacedSessionName`, optional `replacedSessionNamespace`, optional `replacedSessionClosed` (false means automatic close failed and the previous session remains wrapper-owned/restorable for explicit cleanup), `activeBefore`, `activeAfter`, `succeeded`, and `summary` (machine-oriented; may include generated session names). Use `currentSessionNamespace` with `currentSessionName` when following preserved-session recovery actions; retry-fresh actions stay in the attempted namespace. Model-visible echo: when `sessionMode` is `"fresh"` **and** `succeeded` is false, or when `replacedSessionClosed` is false after a replacement, the wrapper appends action-oriented `Managed session outcome` and `Recovery` lines without repeating generated session ids in visible prose; session names remain in `details.managedSessionOutcome`. Failed fresh launches may also append `details.nextActions` such as `run-agent-browser-doctor`, `verify-current-managed-session`, `snapshot-current-managed-session`, or `retry-fresh-managed-session`. When other trailing diagnostic prose is also emitted in the same result, that block is concatenated **after** semantic-action candidate lines, overlay/selector-visibility tails, eval hints/warnings, and `Timeout partial progress` (see `rawAppendedDiagnosticText` in `extensions/agent-browser/lib/orchestration/browser-run/final-result.ts`). For `"auto"` failures the same struct may appear on `details` without that extra line. When post-upstream analysis (for example **`qa`** preset failure) flips the overall tool result after a successful batch, or a fresh native batch opens the requested page and then a later step fails, the managed-session transition still reflects that the fresh browser became current. The visible recovery says the fresh launch became current and points to `failureCategory` / `qaPreset` / `batchFailure` for the post-launch failure instead of telling the agent that the old session was preserved.
@@ -836,7 +838,7 @@ If `agent-browser` is not on `PATH`, fail with a message that:
836
838
  - After open/goto/navigate calls with --profile, --restore, --session-name, or --state, agent_browser best-effort re-selects the tab whose URL matches the returned page when restored tabs steal focus during launch or reconnect.
837
839
  - After confirmed shutdown of an automatically restored managed session, the wrapper retains its complete recorded URL, including the fragment, until the first current-page operation (including get url and reload). Non-page calls such as tab list may start a daemon without fulfilling that reopen; explicit URL reads leave the managed browser and pending reopen untouched. The wrapper uses native open once, verifies the observed tab, and discards old refs/frame scope; it does not restore unsaved forms, JavaScript memory, or history. Explicit navigation, caller-owned/attached sessions, and restore-disabled sessions are not auto-reopened.
838
840
  - For a still-live browser after tab drift or resume, the wrapper verifies/selects the intended tab before ref/semantic helpers and page commands; failed selection stops the call without navigating. Local commands, read <url>, URL a11y/vitals, diff url, window new, and explicit tab/navigation/connection/state recovery do not require the prior tab. Batch checks follow effective rows past non-page prefixes and stop at explicit context changes, preserving caller argv/stdin and continue-on-error behavior. Same-tab reselection is avoided because it clears refs. Use exact batch --bail for fail-fast, not --bail=<value>. Routine same-session calls skip tab-list preflights.
839
- - For sessions with observed tab-drift risk, after a successful command on a known target tab, agent_browser also best-effort restores that intended tab if a restored/background tab steals focus after the command completes. Routine same-session commands skip this post-command tab-list probe.
841
+ - For sessions with observed tab-drift risk, after a successful command on a known target tab, agent_browser also best-effort restores that intended tab if a restored/background tab steals focus after the command completes. Routine same-session commands skip focus correction; observed pages without a native targetId use tab list to establish their identity.
840
842
  - If a known session target unexpectedly reports about:blank, agent_browser best-effort re-selects the prior intended target when it still exists; if recovery fails, it records the observed about:blank target and reports exact recovery guidance instead of treating the prior page as active.
841
843
  - If upstream reports tab_gone, the pinned bound tab is gone; use details.nextActions (tab list / tab new) instead of assuming another tab is yours.
842
844
  <!-- agent-browser-playbook:end wrapper-tab-recovery -->
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-agent-browser-native",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "pi extension that exposes agent-browser as a native tool for browser automation",
5
5
  "type": "module",
6
6
  "author": "Mitch Fultz (https://github.com/fitchmultz)",
@@ -24,7 +24,7 @@
24
24
  "url": "https://github.com/fitchmultz/pi-agent-browser-native/issues"
25
25
  },
26
26
  "engines": {
27
- "node": ">=22.19.0"
27
+ "node": ">=24.21.0"
28
28
  },
29
29
  "bin": {
30
30
  "pi-agent-browser-config": "scripts/config.mjs",
@@ -65,20 +65,15 @@
65
65
  "typebox": "*"
66
66
  },
67
67
  "devDependencies": {
68
- "@earendil-works/pi-ai": "0.87.0",
69
- "@earendil-works/pi-coding-agent": "0.87.0",
70
- "@earendil-works/pi-tui": "0.87.0",
68
+ "@earendil-works/pi-ai": "0.87.1",
69
+ "@earendil-works/pi-coding-agent": "0.87.1",
70
+ "@earendil-works/pi-tui": "0.87.1",
71
71
  "@types/cross-spawn": "6.0.6",
72
- "@types/node": "^25.9.3",
72
+ "@types/node": "24.19.0",
73
73
  "@types/which": "2.0.2",
74
- "tsx": "^4.21.0",
75
- "typebox": "^1.1.38",
76
- "typescript": "^6.0.3"
77
- },
78
- "overrides": {
79
- "basic-ftp": "6.0.1",
80
- "esbuild": "0.28.1",
81
- "protobufjs": "7.6.5"
74
+ "tsx": "4.23.15",
75
+ "typebox": "1.3.34",
76
+ "typescript": "7.0.2"
82
77
  },
83
78
  "scripts": {
84
79
  "docs": "node ./scripts/project.mjs docs",
@@ -100,7 +95,7 @@
100
95
  "startup-profile": "node ./scripts/profile-startup.mjs",
101
96
  "prepare": "node ./scripts/prepare.mjs"
102
97
  },
103
- "packageManager": "npm@11.14.0",
98
+ "packageManager": "npm@12.1.0",
104
99
  "peerDependenciesMeta": {
105
100
  "@earendil-works/pi-ai": {
106
101
  "optional": true
@@ -26,6 +26,6 @@ export default {
26
26
  // Parent crabbox-ready remains for other projects' linked clones.
27
27
  snapshot: "crabbox-ready-ab-0.34.0-d",
28
28
  },
29
- nodeValidationMajor: 22,
29
+ nodeValidationMajor: 24,
30
30
  agentBrowserVersion: PLATFORM_SMOKE_AGENT_BROWSER_VERSION,
31
31
  };
@@ -234,7 +234,7 @@ export async function runDoctor(config) {
234
234
  const failures = { count: 0 };
235
235
  const packageName = config?.packageName ?? "pi-agent-browser-native";
236
236
  const artifactRoot = config?.artifactRoot ?? ".artifacts/platform-smoke";
237
- const nodeMajor = config?.nodeValidationMajor ?? 22;
237
+ const nodeMajor = config?.nodeValidationMajor ?? 24;
238
238
  const agentBrowserVersion = config?.agentBrowserVersion;
239
239
 
240
240
  console.log("\n── Platform smoke config ──");
@@ -254,7 +254,7 @@ export function createLeaseWarmupFailureResult(config, targetName, warmupResult,
254
254
  return { ok: false, suiteDir, assertions };
255
255
  }
256
256
 
257
- export function buildPlatformBuildCommand(targetName, packageName = "pi-agent-browser-native", nodeValidationMajor = 22) {
257
+ export function buildPlatformBuildCommand(targetName, packageName = "pi-agent-browser-native", nodeValidationMajor = 24) {
258
258
  if (platformFor(targetName) === "powershell") {
259
259
  return `powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File .\\scripts\\platform-smoke\\platform-build-windows.ps1 -PackageName ${psSingleQuote(packageName)} -NodeValidationMajor ${nodeValidationMajor}`;
260
260
  }
@@ -461,7 +461,7 @@ async function runPlatformBuildSuite(config, targetName, suiteName, leaseSession
461
461
  const checks = [
462
462
  { id: "command-exit-zero", fn: () => result.code === 0, error: `exit ${result.code}` },
463
463
  { id: "platform-marker", fn: () => stdout.includes("PLATFORM_BUILD_OK") },
464
- { id: "node-version", fn: () => nodeMajor >= (config.nodeValidationMajor ?? 22), error: `Node major ${nodeMajor}` },
464
+ { id: "node-version", fn: () => nodeMajor >= (config.nodeValidationMajor ?? 24), error: `Node major ${nodeMajor}` },
465
465
  { id: "npm-ci", fn: () => /PLATFORM_NPM_CI_EXIT=0/.test(stdout) },
466
466
  { id: "npm-run-verify", fn: () => /PLATFORM_VERIFY_EXIT=0/.test(stdout) },
467
467
  { id: "npm-pack", fn: () => /PLATFORM_NPM_PACK_EXIT=0/.test(stdout) && marker(stdout, "PLATFORM_PACKED_TARBALL").length > 0 },