pi-agent-browser-native 0.7.0 → 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.
@@ -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) {
@@ -1,4 +1,5 @@
1
- import { getCurrentSystemMessage } from "@earendil-works/pi-ai";
1
+ import { Text } from "@earendil-works/pi-tui";
2
+ import { formatAgentBrowserRenderCall } from "./pi-tool-rendering.js";
2
3
  import { AGENT_BROWSER_PARAMS, AGENT_BROWSER_CODE_PARAMS, AGENT_BROWSER_ACTION_PARAMS, AGENT_BROWSER_QA_PARAMS, AGENT_BROWSER_ELECTRON_PARAMS, AGENT_BROWSER_SOURCE_PARAMS, AGENT_BROWSER_NETWORK_SOURCE_PARAMS, AGENT_BROWSER_TOOLS_PARAMS, } from "./input-modes/params.js";
3
4
  import { ADVANCED_TOOL_PROMPT_GUIDELINES, buildToolPromptGuidelines } from "./playbook.js";
4
5
  export const AGENT_BROWSER_TOOL_INVENTORY = {
@@ -29,10 +30,13 @@ export function registerAgentBrowserToolSurface(pi, options) {
29
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.",
30
31
  promptSnippet: "Branch, loop, and aggregate browser observations with explicit JSON/image output.",
31
32
  parameters: AGENT_BROWSER_CODE_PARAMS,
32
- constrainedSampling: { type: "json_schema", strict: "prefer" },
33
33
  execute: options.executeCode,
34
34
  executionMode: options.executionMode,
35
- renderCall: options.renderCodeCall,
35
+ renderCall(args, theme, context) {
36
+ const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
37
+ text.setText(formatAgentBrowserRenderCall(args, theme, context.expanded));
38
+ return text;
39
+ },
36
40
  renderResult: options.renderResult,
37
41
  });
38
42
  pi.registerTool({
@@ -123,6 +127,8 @@ export function registerAgentBrowserToolSurface(pi, options) {
123
127
  const available = new Set(pi.getAllTools().map(({ name }) => name));
124
128
  if (!["agent_browser", "agent_browser_code", "agent_browser_tools", ...advancedNames].every(name => available.has(name)))
125
129
  return;
130
+ // Keep Pi's replay helper behind the awaited restoration boundary, not factory registration.
131
+ const { getCurrentSystemMessage } = await import("@earendil-works/pi-ai");
126
132
  const current = getCurrentSystemMessage(ctx.sessionManager.buildSessionProjection().messages);
127
133
  const restored = new Set(current?.toolsAdded?.map(({ name }) => name));
128
134
  const active = pi.getActiveTools().filter((name) => !advancedNames.has(name) || restored.has(name));
@@ -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
@@ -39,7 +39,7 @@ The minimum Pi runtime is 0.87.0, matching the native APIs used by the new tool
39
39
 
40
40
  Qualify official Pi 0.87 and the fork independently with the existing host/package/lifecycle infrastructure. Required evidence for the redesign includes compact schema and additive activation/history, native prompt sections, shared direct/code auth continuity, fresh JavaScript globals, selected real images and conservative geometry, visible handled failures/exact recovery, real cross-process helper/action and whole-cell ordering, cancellation/reaping, and ordered transition replay after interruption/branch changes. Retain native batch and advanced action/QA/Electron/source outcome coverage. Small synthetic timing comparisons are not universal performance claims or sandbox certification.
41
41
 
42
- Existing owner waivers remain authoritative. Windows qualification is waived; report diagnostic failures without claiming a pass or restarting waived repair work. GitHub Actions remains disabled under the current explicit owner instruction; use the authorized local checks and review evidence, and label CI as not run rather than passed. This does not revoke other approved verification or authorize unrequested platform work.
42
+ Existing owner waivers remain authoritative. Windows qualification is waived; report diagnostic failures without claiming a pass or restarting waived repair work. Routine GitHub Actions now runs on PRs to `main` and manual dispatch, with no Windows CI job. These focused checks complement, rather than replace, the local release and publication gates.
43
43
 
44
44
  ## Pre-release checks
45
45
 
@@ -71,7 +71,7 @@ npm run verify -- pre-pr
71
71
  3. `npm run verify -- package-pi`, which first validates package contents via `npm pack --json --dry-run` and then smoke-loads the packed package in Pi isolation
72
72
  4. `npm run smoke:platform:doctor` and the full Crabbox matrix from [`platform-smoke.md`](platform-smoke.md): macOS SSH, Ubuntu local-container, and native Windows Parallels targets running fast target-local `platform-build` plus `browser-dogfood-smoke`
73
73
 
74
- When using the documented, maintainer-approved [local macOS and hosted native-Windows alternatives](platform-smoke.md#alternate-native-transports), run and inspect those target-local suites separately alongside the remaining release checks. The default composed gate is unchanged; alternate evidence must not be labeled a passing Crabbox matrix.
74
+ When using the documented, maintainer-approved [local macOS alternative](platform-smoke.md#alternate-native-transports), run and inspect that target-local suite separately alongside the remaining release checks. The default composed gate is unchanged; alternate evidence must not be labeled a passing Crabbox matrix.
75
75
 
76
76
  `npm publish` runs npm’s `prepublishOnly` script from `package.json`, which executes the same `npm run verify -- release` gate and then `npm pack --dry-run`. That concatenated gate is everything in the default `npm run verify` step (generated playbook drift, clean `dist/` build, TypeScript, the unit/fake suite, generated command-reference blocks, and live upstream command-reference sampling against the targeted `agent-browser` on `PATH`), the configured-source lifecycle harness, the packaged Pi smoke in `package-pi`, and the release-blocking Crabbox platform matrix. Using `npm publish --ignore-scripts` skips that contract intentionally.
77
77
 
@@ -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. |
@@ -322,15 +322,13 @@ node --test test/agent-browser.checkpoint-native.test.mjs
322
322
 
323
323
  `PI_CHECKPOINT_TEST_EXTENSION` can select a separate baseline checkout. With `PI_CHECKPOINT_TEST_BASELINE_RESTORE=1`, only its existing root-restore path runs and every receipt must retain the old shutdown blocker; this is not positive sleep qualification. The SDK selector is test-only; it does not change package peers or activate an extension. This gate is separate from the default 0.84 unit fixtures and release/platform/model-based gates. Qualification is for ordinary native save-on-close and explicit URL reopen, not a live browser snapshot. Journal fault injection checks that ongoing native I/O errors reject acquisition and preserve prior bytes/accepted entries. Permission repair must recover through core capture without repeating the failed append; recording retry and cleanup leases remain extension-owned. The test also checks exact repaired entries/selection in a separate-process strict cold restore and rejection of a mismatched artifact. After explicit close it observes native daemon exit before asserting idle readiness; an acknowledgement alone is not proof of inactivity. The abnormal-restart regression kills only its exact SDK fixture PID after ordinary branch navigation, then reopens the real journal in a fresh SDK process. It verifies that the off-branch wrapper-created daemon blocks capture without acquiring cleanup ownership, and that explicit managed-looking names and a nonowned namespace retain ambient routing. Same-namespace/same-name controls check distinct live daemons in owned and ambient socket roots, continued blocking after closing only the caller, ambient-only blocking after closing the historical daemon, and positive readiness after both are explicitly closed. All browser ownership evidence comes from executed native tools, never injected ownership records.
324
324
 
325
- #### Required Linux PR gate
325
+ #### Routine GitHub PR checks
326
326
 
327
- [`.github/workflows/native-checkpoint.yml`](https://github.com/fitchmultz/pi-agent-browser-native/blob/main/.github/workflows/native-checkpoint.yml) runs **Native checkpoint Linux** on every PR to `main`, pushes to `main`, and manual dispatch. When Actions is authorized, this is the checkpoint PR check (repository settings are separate from the workflow). Under the current keep-Actions-disabled instruction, qualify locally and record the CI waiver rather than enabling workflows. It does not replace release/manual-publication gates or full Linux personal-workspace sleep/resume qualification.
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
- The CI-only recipe in [`scripts/ci-native-checkpoint.sh`](https://github.com/fitchmultz/pi-agent-browser-native/blob/main/scripts/ci-native-checkpoint.sh) builds the public fork target selected by `host-targets.json` in the workflow's pinned `fitchmultz/.github` automation revision, using the fork's lockfile and documented `npm ci --ignore-scripts`, `hydrate:model-data`, `build:offline` sequence. It uses explicit Node **26.9.0**, the extension's unchanged lockfile, stock **agent-browser 0.38.1**, a fresh stock Chrome-for-Testing download, and Ubuntu ffmpeg. No browser fork, 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
- Sources live under a private `/tmp` root, outside HOME ancestry. Setup and tests use `env -i`; verification runs non-root in a loopback-only network namespace after downloads finish. Shared compatibility checks select independent official/fork graphs for types, tests and consumer installation. This workflow additionally runs the locked official graph on Node 26 before network isolation, then generated-document checks and live command-reference sampling inside the loopback namespace. The separate native gate requires **13 passed, zero failed/cancelled/skipped/todo** in its TAP output. `PI_CHECKPOINT_TEST_REQUIRED=1` turns a missing SDK into a failure and rejects root/baseline-only runs; missing browser parameters, binaries, or checkpoint APIs fail normally. Without the flag or SDK, direct local invocation remains opt-in and skips.
332
-
333
- CI uploads raw setup/build/provenance logs, Node 26 and document verification output, and native TAP (including receipts), even on failure. Chrome is downloaded through the pinned stock installer, not separately version-pinned; the actual browser version is logged. These checks prove synthetic native behavior on that runner and sourcehost, not real-site authentication, Electron-app behavior, managed-key cold portability, or complete personal qualification. Passing this gate does not replace code review or full personal-workspace acceptance.
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.
334
332
 
335
333
  ### Native Linux socket-root regression
336
334
 
@@ -42,7 +42,7 @@ The compact direct/code/loader surface replaces public multimode inputs while re
42
42
  | Observation and geometry | Canonical bounded JSON/prose/code observations expose exact recovery and receipts; complete observation spills preserve large output. Image dimensions/CSS viewport/DPR/crop/scroll/frame evidence is measured or explicitly unknown; no viewport-coordinate assumptions for full-page/element images. |
43
43
  | Pi integration | Native additive activation/history and prompt sections on independently qualified official Pi 0.87 and fork; optional checkpoint hooks, no provider payload/image-detail overrides. |
44
44
 
45
- See [migration](TOOL_CONTRACT.md#07-migration), [canonical observations](TOOL_CONTRACT.md#canonical-observation), and [release qualification/rollback](RELEASE.md#07-upgrade-and-rollback). Current owner instructions keep GitHub Actions disabled and Windows qualification waived; local evidence and review must be reported separately from unrun CI or platform checks.
45
+ See [migration](TOOL_CONTRACT.md#07-migration), [canonical observations](TOOL_CONTRACT.md#canonical-observation), and [release qualification/rollback](RELEASE.md#07-upgrade-and-rollback). Routine GitHub Actions runs official/fork Linux compatibility, a macOS packed-Pi smoke, and native Linux checkpoints on PRs to `main`; Windows CI is excluded. Release/platform evidence and review remain separate from these focused checks.
46
46
 
47
47
  ## Audit result
48
48
 
@@ -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
 
@@ -123,7 +123,7 @@ At runtime source `3a9adf490883149e7f79ee070d1900877e17b891`, PR compatibility p
123
123
 
124
124
  Fresh precommit evidence for that runtime passed real Pi checkout/live-site and supported disposable Electron smoke, configured-source lifecycle, packaged runtime-only SDK/CLI loading, real-upstream checks, native macOS and the canonical named Ubuntu target (Debian 12). Local macOS is the documented alternate, not an SSH pass. Docs/workflow-only delivery may reuse this runtime evidence only after all 104 rebuilt runtime files match the checked artifact; changed published docs require a newly checked retained tarball. No additional paid/live gate is implied by an otherwise byte-identical runtime.
125
125
 
126
- Linux/macOS, declared Node floors, official/fork host identities, required consumer artifacts and Native checkpoint Linux remain required. Required checks follow current explicit owner instructions, including disabled Actions; retain all three final reviewer approvals before merge/release/activation. The unchanged composed `release`/`prepublishOnly` command still includes Windows and is not reported as passed under this waiver; see [release guidance](RELEASE.md#current-0616-rollout-waiver).
126
+ Linux/macOS, declared Node floors, official/fork host identities, required consumer artifacts and Native checkpoint Linux remain required. Routine GitHub Actions is enabled for PRs to `main` and manual dispatch; its focused results do not substitute for the release and platform gates. Retain all three final reviewer approvals before merge/release/activation. The unchanged composed `release`/`prepublishOnly` command still includes Windows and is not reported as passed under this waiver; see [release guidance](RELEASE.md#current-0616-rollout-waiver).
127
127
 
128
128
  ## Verification evidence
129
129
 
@@ -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. |