pi-agent-browser-native 0.9.2 → 0.9.3
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 +14 -0
- package/README.md +18 -6
- package/dist/extensions/agent-browser/index.js +39 -16
- package/dist/extensions/agent-browser/lib/batch-lifecycle.js +6 -3
- package/dist/extensions/agent-browser/lib/browser-transcript.js +2 -1
- package/dist/extensions/agent-browser/lib/native-output.js +2 -1
- package/dist/extensions/agent-browser/lib/orchestration/batch-stdin.js +2 -8
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.js +12 -2
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +58 -7
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +18 -12
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +33 -24
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +111 -53
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +32 -20
- package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +37 -32
- package/dist/extensions/agent-browser/lib/orchestration/native-session-defaults.js +26 -8
- package/dist/extensions/agent-browser/lib/orchestration/output-file.js +3 -1
- package/dist/extensions/agent-browser/lib/page-target-validation.js +10 -3
- package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +2 -1
- package/dist/extensions/agent-browser/lib/playbook.js +2 -2
- package/dist/extensions/agent-browser/lib/process-environment.js +12 -3
- package/dist/extensions/agent-browser/lib/process.js +6 -6
- package/dist/extensions/agent-browser/lib/read-confirmation.js +56 -16
- package/dist/extensions/agent-browser/lib/results/confirmation.js +3 -0
- package/dist/extensions/agent-browser/lib/results/envelope.js +5 -1
- package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +3 -0
- package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +34 -1
- package/dist/extensions/agent-browser/lib/results/presentation/registry.js +5 -1
- package/dist/extensions/agent-browser/lib/results/presentation.js +15 -2
- package/dist/extensions/agent-browser/lib/results/selector-recovery.js +5 -5
- package/dist/extensions/agent-browser/lib/runtime.js +5 -1
- package/dist/extensions/agent-browser/lib/session-page-state.js +35 -16
- package/docs/ARCHITECTURE.md +5 -5
- package/docs/COMMAND_REFERENCE.md +13 -9
- package/docs/RELEASE.md +6 -5
- package/docs/REQUIREMENTS.md +1 -0
- package/docs/SESSION_STORAGE.md +1 -1
- package/docs/SUPPORT_MATRIX.md +9 -3
- package/docs/TOOL_CONTRACT.md +20 -14
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.9.3 - 2026-10-05
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Avoid repeated JSON parsing exceptions for the exact redaction marker and ordinary text that cannot begin JSON, and skip redacting a discarded ref copy in browser-code results. Preserve native grammar validation, secret masking, complete output/spill content, retained snapshot/replay state and direct failure receipts.
|
|
10
|
+
- Run offline native discovery pipeline contracts automatically when the host exports its public instruction-groups factory; retain the explicit force-on diagnostic.
|
|
11
|
+
|
|
12
|
+
- Verify the current complete URL before one-use approved-capture actions in wrapper-owned managed sessions, preserving path/hash drift refusal and unchanged-page success.
|
|
13
|
+
- Preserve exact tab-creation confirmation decisions and visible conditional navigation guidance from narrowly validated transport metadata, without automatically executing continuation arguments.
|
|
14
|
+
|
|
15
|
+
- Complete semantic role click/check/fill and unique accessible select after snapshot approval by reusing only the eligible next-call capture. Render pending snapshots as native confirmation controls rather than empty pages.
|
|
16
|
+
|
|
17
|
+
- Keep the established native confirmation policy across same-session helpers, exact confirm/deny follow-ups, code/advanced calls and resume, without changing caller argv or native first-command precedence. Pending navigation, including native batch rows, never becomes an observed target; completed compound confirmations reconcile their actual target before follow-ups. Replacement cleanup retains the old identity's selected string and continuity state until successful close; native unset still uses native defaults. Exact native guarded-control markers avoid helper-slot replacement without command replay or native ID-validation claims. Completed click/tab/close effects and inner failures reconcile normally; cold pending helpers expose the exact decision before getters dispatch. Electron host helpers retain the setting, and definite unestablished requests no longer overwrite it.
|
|
18
|
+
|
|
5
19
|
## 0.9.2 - 2026-10-04
|
|
6
20
|
|
|
7
21
|
### 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
|
-
|
|
9
|
+
CI qualifies the latest stable official Pi and latest maintained fork `main`, resolving their version/commit once per run and retaining SDK/CLI identity evidence. Locked development dependencies are reproducible build snapshots, not qualification targets; runtime peers remain wildcard. `npm run check:compat` checks the installed candidate SDK/CLI identity, typechecks, runs the existing offline tests (including the native Pi pipeline with a controlled provider and fake browser), and tests a packed runtime-only installation through both the SDK and the actual bundled RPC CLI. It does not substitute a fork SDK while leaving official types or child CLI installed.
|
|
10
10
|
|
|
11
11
|
Run in an empty HOME with a **short** TMPDIR outside your real home. Long temporary paths can exceed macOS's 103-byte Unix socket limit; keep the fixture's socket safety checks intact. Native lifecycle controls, live upstream help sampling, lifecycle dogfood, and platform qualification remain the separate gates documented below. A Pi-only compatibility run uses no live browser profiles, credentials, or agent-browser fork, and does not certify those external integrations.
|
|
12
12
|
|
|
@@ -90,7 +90,7 @@ The result is optimized for agent work:
|
|
|
90
90
|
| Stateful cookies/storage/auth output bloats or leaks context | Presentation layer redacts `details.data` for cookies and credential-like storage values while keeping low-risk local QA values such as `theme: dark` readable; recursively scrubs other structured upstream JSON (network, diff, trace/profiler, stream, dashboard, chat, auth, dialog, frame, state, and similar) using sensitive key names plus string heuristics; masks sensitive argv flags and positionals, including raw batch rows and ordered code journals; scrubs secrets from failed batch step errors; and exposes a compact redacted `batch` matrix on top-level `details.data` | `extensions/agent-browser/lib/results/presentation.ts`, `extensions/agent-browser/lib/results/presentation/diagnostics.ts`, `extensions/agent-browser/lib/runtime.ts`, `test/agent-browser.presentation-diagnostics.test.ts` |
|
|
91
91
|
| Stale `@eN` refs fail mysteriously | Defines full per-session refs once in the native journal, exposes them on dedicated captures, rejects mismatched URLs / unknown refs / unsafe `batch` stdin ordering before spawn, adds recovery guidance to rerun `snapshot -i` or use stable `find` locators | `extensions/agent-browser/index.ts`, `extensions/agent-browser/lib/session-page-state.ts`, `test/agent-browser.session-page-state.test.ts`, `test/agent-browser.results.test.ts`, `test/agent-browser.extension-ref-guards.test.ts`, `test/agent-browser.extension-semantic-recovery.test.ts` |
|
|
92
92
|
| Agents need stable success/failure buckets | Exposes bounded `resultCategory`, `successCategory`, and `failureCategory` on tool `details` for branching without parsing prose; final results return native `isError` and a visible prose failure notice while preserving parseable caller-requested `--json` output. Native structured results expose the same bounded, redacted observations, not internal replay metadata | [`docs/TOOL_CONTRACT.md`](docs/TOOL_CONTRACT.md#details), `extensions/agent-browser/lib/results/categories.ts`, `extensions/agent-browser/index.ts`, `extensions/agent-browser/lib/pi-tool-rendering.ts`, `test/agent-browser.results.test.ts`, `test/agent-browser.extension-validation.test.ts`, `test/agent-browser.pi-pipeline.test.ts` |
|
|
93
|
-
| Clicks can report success without the page receiving the event | Top-level non-Electron direct `click` calls on `xpath=` targets or role-gated current `@e…` refs (`button`, `checkbox`, `menuitem`, `radio`, `switch`, `tab`) install a bounded target-specific DOM-event probe; eligible `@e…` refs require a unique role/name in both the latest snapshot and the live candidates. A per-probe temporary DOM marker must be read back by native `get attr <original-selector> <marker>` before the candidate can verify dispatch, including XPath scope. Missing or mismatched identity cleans up the probe and leaves the native click unverified, not failed. Duplicate-name refs pass through without a probe: their old ordinal cannot prove which element received the click. If upstream reports success but no trusted event reaches the resolved target, the wrapper fails the tool, exposes `details.clickDispatch`, and suggests explicit retry/inspect next actions (no in-page replay), including a nested-scroll `scrollintoview` action when the probe sees the target outside a scroll container or viewport. Unresolved locator clicks such as raw `find … click` are left upstream-owned to avoid false failures for frame-scoped targets. Other click results still expose `details.pageChangeSummary`; `observed: false` explicitly marks dispatch-only mutation summaries and adds a visible `Action dispatched; application change unverified` warning. Unchanged-URL clicks can surface evidence-backed `details.overlayBlockers` candidates. | [`docs/TOOL_CONTRACT.md`](docs/TOOL_CONTRACT.md#details), `extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.ts`, `extensions/agent-browser/lib/results/presentation/navigation.ts`, `test/agent-browser.presentation.test.ts`, `test/agent-browser.extension-click-dispatch.test.ts` |
|
|
93
|
+
| Clicks can report success without the page receiving the event | Top-level non-Electron direct `click` calls on `xpath=` targets or role-gated current `@e…` refs (`button`, `checkbox`, `menuitem`, `radio`, `switch`, `tab`) install a bounded target-specific DOM-event probe; eligible `@e…` refs require a unique role/name in both the latest snapshot and the live candidates. A per-probe temporary DOM marker must be read back by native `get attr <original-selector> <marker>` before the candidate can verify dispatch, including XPath scope. Missing or mismatched identity cleans up the probe and leaves the native click unverified, not failed. Normal collection cancels the probe timer; if a pending native decision blocks cleanup, listeners and the window marker self-expire after this call's process-timeout budget plus the existing two-second cleanup grace, without another browser command. Duplicate-name refs pass through without a probe: their old ordinal cannot prove which element received the click. If upstream reports success but no trusted event reaches the resolved target, the wrapper fails the tool, exposes `details.clickDispatch`, and suggests explicit retry/inspect next actions (no in-page replay), including a nested-scroll `scrollintoview` action when the probe sees the target outside a scroll container or viewport. Unresolved locator clicks such as raw `find … click` are left upstream-owned to avoid false failures for frame-scoped targets. Other click results still expose `details.pageChangeSummary`; `observed: false` explicitly marks dispatch-only mutation summaries and adds a visible `Action dispatched; application change unverified` warning. Unchanged-URL clicks can surface evidence-backed `details.overlayBlockers` candidates. | [`docs/TOOL_CONTRACT.md`](docs/TOOL_CONTRACT.md#details), `extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.ts`, `extensions/agent-browser/lib/results/presentation/navigation.ts`, `test/agent-browser.presentation.test.ts`, `test/agent-browser.extension-click-dispatch.test.ts` |
|
|
94
94
|
| Dashboard scroll commands can look successful while nothing moves | Handles standard `scroll <dir> [px]` against the document first (including pages whose smooth-scroll CSS defeats upstream wheel timing), falls back upstream when the document cannot move, and samples viewport/containers around the fallback; explicit CSS-container scrolling uses instant movement so smooth-scroll CSS cannot cause a premature no-movement failure; unchanged positions fail as `upstream-error` with `details.scrollNoop`, visible recovery guidance, and exact snapshot/screenshot checks. Unsupported `scrollintoview text=...` fails before dispatch, including inside effective batch rows, and shows exact native `find text ... hover` and snapshot/ref recovery payloads; help remains native pass-through. | [`docs/TOOL_CONTRACT.md`](docs/TOOL_CONTRACT.md#details), [`docs/COMMAND_REFERENCE.md`](docs/COMMAND_REFERENCE.md#core-page-and-element-commands), `test/agent-browser.extension-validation.test.ts` |
|
|
95
95
|
| Dropdown/combobox clicks can focus or hit native option box-model errors | Adds first-class `select <selector> <value...>` paths through native `args` and `agent_browser_action`; semantic role/name or label select resolves exactly one current visible combobox/listbox ref before action. Custom combobox clicks still detect focused controls with explicit `aria-expanded` state but no visible options and return `details.comboboxFocus` plus exact recovery `nextActions` | [`docs/TOOL_CONTRACT.md`](docs/TOOL_CONTRACT.md#details), [`docs/COMMAND_REFERENCE.md`](docs/COMMAND_REFERENCE.md#core-page-and-element-commands), `extensions/agent-browser/lib/input-modes/semantic-action.ts`, `test/agent-browser.extension-input-modes.test.ts`, `test/agent-browser.extension-validation.test.ts` |
|
|
96
96
|
| Recording workflows fail late when `ffmpeg` is missing or report stale lifecycle state | After successful `record start` / `record restart`, reports `successCategory: "artifact-pending"`, returns an exact `stop-pending-recording` action, warns when older natives report pending output without `ffmpeg`, and conservatively invalidates prior page-scoped `@e…` refs on every executed start attempt and URL-bearing restart to protect older supported natives. This is not evidence of a page change: 0.37 records the active page unless a URL is supplied; FPS-only calls keep the intended tab; an unbounded transcript-backed namespace/session index reserves active destinations across aliases, serializes artifact lifecycle and explicit wait/output writes, persists cross-branch close tombstones, retires every successful close path (including every matching namespace owner for `close --all`), rejects missing/stale restart output, coalesces terminal batch state, keeps only the newest pending path per identity, rejects recording starts after a nested close, folds Unicode path aliases, and retains exact cleanup actions with visible guidance on any later same-session failure | [`docs/TOOL_CONTRACT.md`](docs/TOOL_CONTRACT.md#details), [`docs/COMMAND_REFERENCE.md`](docs/COMMAND_REFERENCE.md#diff-debug-and-streaming), `test/agent-browser.extension-validation.test.ts`, `test/agent-browser.presentation-artifacts-batch.test.ts` |
|
|
@@ -103,7 +103,7 @@ Artifact results show known requested paths separately from reported/resolved lo
|
|
|
103
103
|
|
|
104
104
|
## Fastest way to try it
|
|
105
105
|
|
|
106
|
-
Use Node 24.21.0 or newer and Pi 1.0.0 or newer. This package keeps optional Pi core imports as wildcard `peerDependencies` because Pi package docs require the host Pi install to provide those packages,
|
|
106
|
+
Use Node 24.21.0 or newer and Pi 1.0.0 or newer. This package keeps optional Pi core imports as wildcard `peerDependencies` because Pi package docs require the host Pi install to provide those packages, retains a locked Pi development snapshot while qualifying the latest official/fork graphs, and makes Pi hosts below 1.0.0 a setup failure through `pi-agent-browser-doctor`. There are no compatibility shims for older Pi releases.
|
|
107
107
|
|
|
108
108
|
Install upstream `agent-browser` first and make sure it is on `PATH`:
|
|
109
109
|
|
|
@@ -327,7 +327,7 @@ Profile hints with `authenticated-only` remain advisory. Global/override profile
|
|
|
327
327
|
|
|
328
328
|
You usually prompt the agent in natural language. These JSON snippets show the exact native tool shape the agent should use.
|
|
329
329
|
|
|
330
|
-
Open a page and inspect it (first-call recipe: open → snapshot -i → interact with current `@refs` → snapshot -i after changes). Omit `--json` unless you need JSON text; structured details are
|
|
330
|
+
Open a page and inspect it (first-call recipe: open → snapshot -i → interact with current `@refs` → snapshot -i after changes). Omit `--json` unless you need JSON text; structured details are available by default. Explicit `--json false` selects native text without trusted row, confirmation, lifecycle, page-target or artifact receipts; see [the output contract](docs/TOOL_CONTRACT.md#wrapper-json).
|
|
331
331
|
|
|
332
332
|
```json
|
|
333
333
|
{ "args": ["open", "https://example.com"] }
|
|
@@ -427,6 +427,8 @@ Evaluate page JavaScript through stdin. Put the script in the top-level `stdin`
|
|
|
427
427
|
{ "args": ["eval", "--stdin"], "stdin": "({ title: document.title, url: location.href })", "outputPath": "logs/page-state.json" }
|
|
428
428
|
```
|
|
429
429
|
|
|
430
|
+
Native text cookie reads redact `name=value` values, including empty cookie names; storage reads redact `key: value`, including empty and colon-containing keys, using the same benign-value policy as JSON. Standalone `storage local|session get <key>` and native shorthand `storage local|session <key>` protect the entire sensitive value, including embedded newlines, even when invocation display redaction changes the supplied key. Names and benign preferences remain visible in content, details, spills and exports. Native all-entry storage and raw batches do not escape multiline values, so text redaction is heuristic: use structured JSON when sensitive storage must be unambiguous.
|
|
431
|
+
|
|
430
432
|
Use `outputPath` when `eval`, `get`, `snapshot`, or another extraction should be saved as a durable workspace file. Recording results also export on failure or timeout: their JSON envelope retains the failed attempt, native receipt, verification and any recovery evidence rather than writing misleading bare success data. Keep it distinct from screenshot, download, recording, and other browser artifact destinations; preflight rejects known same-call aliases before browser activity, and the result writer preserves the browser artifact if an alias becomes apparent only afterward. The wrapper writes `details.data` when present, otherwise the model-facing text content. When presentation compacted a large direct result, a result row, or the whole `batch`, it instead reads the full command-redacted pre-compaction payload only from the corresponding live wrapper-managed spill recorded in `details.artifactManifest`; if any required spill is unavailable or untrusted, the call fails without writing compact metadata to the requested path. `details.outputFile` reports the saved path and byte count. Explicit upstream `--json` content stays parseable, including early snapshot/network filters, scroll results, and preparation failures; metadata stays in `details` and the save notice lives only in `details.outputFile`. Help/version output remains native text.
|
|
431
433
|
|
|
432
434
|
Recording receipts separate captured-frame rate and wall-clock capture duration from nominal/output FPS. `details.artifacts[].recording` includes native capture timestamps, received frame counts (not pixel-unique frames), encoded/written/held/dropped/skipped counts, and output duration. Missing native metrics stay unknown. Repeated, static, sparse or final-state-only frames cannot establish UI smoothness.
|
|
@@ -561,7 +563,9 @@ Each automatic root has its own stable native restore key. Native save/restore p
|
|
|
561
563
|
|
|
562
564
|
Explicit native `session` defaults (config or `AGENT_BROWSER_SESSION`) still override automatic root selection, including `sessionMode: "fresh"`. They intentionally share whichever browser the caller selected. Per-call session/namespace flags retain precedence. Ordinary launch options such as `--profile` or `--executable-path` still use the root-group name. Repeat explicit options when deliberately relaunching a persistent per-root profile after close. Explicit `fresh` and attachment choices retain their existing managed-session lifecycle; later auto calls follow that selected managed session. Use `session info` with the reported `sessionName` for read-only native daemon/browser evidence. Unknown native fields remain unknown.
|
|
563
565
|
|
|
564
|
-
|
|
566
|
+
Set `--confirm-actions` on the session's first command. Its established setting follows same-namespace/session helpers and ordinary, code, advanced and resumed calls, so exact returned confirm/deny actions work without repeating the policy. Bare follow-ups retain a selected string, including explicit `""`, over ambient environment or discovered-config changes. Native unset is not retained and keeps ordinary native default resolution. A new explicit `--confirm-actions` or `--config` selection uses native argv → environment → config precedence and can restart the daemon, losing pending confirmations. Successful close retires the setting. Replacement cleanup uses the old identity's setting and retains its continuity state if close fails or needs confirmation. Electron launch and host helpers use the same selected setting. Definite pre-dispatch, native CLI parse/config and local-inspection rejections do not replace an established setting unless an earlier browser helper admitted it; interrupted work retains its selected intent.
|
|
567
|
+
|
|
568
|
+
Native confirmation controls retain their exact ID, namespace/session and compact command/action provenance in the existing transcript state. Use the exact returned confirm/deny action: matching guarded decisions skip page helpers that could overwrite the native pending slot, then reconcile the completed browser action normally. In native batch, that helper suppression applies only to a matching first effective decision; raw arguments win over stdin and later page/ref checks remain. This does not prove native ID validation or make the action browser-independent. Page content cannot create this provenance. A pending `tab_new` transport may retain paired `after_confirmation: ["open", <requested URL>]` and nonempty `guidance` only when the continuation matches the original tab URL. Its conditional guidance stays visible: approve to create the blank/labeled tab, then explicitly navigate that new tab; denial never authorizes the continuation. The extension does not execute these arguments automatically. Completed confirmed full snapshots install their refs, including in batch row order. A final confirmed capture can supply the next same-identity call's freshness sample once, including semantic role click/check/fill and unique role/label select resolution; intervening calls or later rows consume that opportunity. Pending snapshots render a confirmation control, not an empty page. Target/membership/invalidation and native-generation checks still apply. On this one use, live verification compares the complete capture URL, including fragments, and rejects refs when it changes—including wrapper-owned managed sessions and warm sessions without resume. Ordinary ref/tab URL comparisons remain fragment-insensitive. Only a DOM-only rename on the same page after that sample is not resampled on this one use. Further ref operations use the normal freshness check. Policy-required explicit URL reads keep their separate two-call flow; only `capabilities.readRequiresConfirmation: true` grants browser-independent confirmation with native ID checking. Legacy URL-read prompts retain correct routing but normal page checks. A pending cold-reopen helper exposes its decision without dispatching the requested getter or replaying the original open.
|
|
565
569
|
|
|
566
570
|
Full live identity, browser-independent native reads/confirmations and detailed recording receipts require the companion upstream fixes; the current recommended release does not yet supply all of them. Older supported versions remain accepted with unavailable evidence marked unknown. The extension does not upgrade or restart your installed browser to obtain these fields.
|
|
567
571
|
|
|
@@ -737,6 +741,14 @@ The upstream browser engine remains [`agent-browser`](https://agent-browser.dev/
|
|
|
737
741
|
- Real authenticated profile use is powerful but sensitive. Treat profile and cookie access as user-approved, task-specific behavior.
|
|
738
742
|
- Wrapper tab/session recovery is best effort around observed upstream behavior, not a replacement for explicit profile/session design.
|
|
739
743
|
|
|
744
|
+
## Automatic npm releases (maintainers)
|
|
745
|
+
|
|
746
|
+
Follow the [shared release procedure](https://github.com/fitchmultz/.github#automatic-npm-releases): merge a reviewed PR into `main` with an intentional `package.json` version bump and a matching versioned `CHANGELOG.md` section. Automation never bumps versions, overwrites releases, or republishes an existing version. Once configured and enabled, it runs existing compatibility and native lifecycle checks, qualifies one candidate tarball, then waits for `fitchmultz` approval in the `npm` environment.
|
|
747
|
+
|
|
748
|
+
Approve only after checking the exact source commit, version, downloaded candidate tarball and summary against [the existing release gates](docs/RELEASE.md#pre-release-checks), including required lifecycle/live-site evidence, artifact checks and all three final reviewer approvals. Apply the documented Windows waiver without claiming a Windows or full composed-gate pass. Approval attests genuine satisfaction of those requirements; it does not create proof. Authenticated, model-backed and paid checks stay outside CI. Automated publication disables npm hooks to publish the already checked bytes; the manual publisher instructions and their gates remain valid.
|
|
749
|
+
|
|
750
|
+
Failed/unpublished candidates can retry daily at 12:17 UTC or via manual dispatch of `npm release` on `main`, without another bump. Set repository variable `NPM_RELEASE_ENABLED` to anything other than `true` to stop new release plans; cancel pending runs separately when needed. Workflow validation is not evidence of a completed real OIDC publication.
|
|
751
|
+
|
|
740
752
|
## Local development
|
|
741
753
|
|
|
742
754
|
Install upstream `agent-browser`, then install dependencies:
|
|
@@ -745,7 +757,7 @@ Install upstream `agent-browser`, then install dependencies:
|
|
|
745
757
|
npm install
|
|
746
758
|
```
|
|
747
759
|
|
|
748
|
-
Use the npm version declared in `package.json` `packageManager` when refreshing `package-lock.json` (for example `npx -y npm@12.2.0 install`) so optional-platform lockfile metadata does not drift. Use Node 24.21.0 and Pi 1.0.0 or newer for lifecycle and interactive browser smokes;
|
|
760
|
+
Use the npm version declared in `package.json` `packageManager` when refreshing `package-lock.json` (for example `npx -y npm@12.2.0 install`) so optional-platform lockfile metadata does not drift. Use Node 24.21.0 and Pi 1.0.0 or newer for lifecycle and interactive browser smokes; locked Pi devDependencies are reproducible build snapshots, not qualification targets. CI selects the once-resolved latest official graph for native lifecycle and package smoke, and independently qualifies the latest maintained fork. See [Environment and automation pitfalls](docs/RELEASE.md#environment-and-automation-pitfalls) in `docs/RELEASE.md`.
|
|
749
761
|
|
|
750
762
|
Checkout-only extension smoke test:
|
|
751
763
|
|
|
@@ -9,7 +9,7 @@ import { SessionPageState } from "./lib/session-page-state.js";
|
|
|
9
9
|
import { appendBrowserTransition, applyArtifactChanges, artifactChanges, browserStateEffects, getBrowserRecord, getBrowserResultMessage, pageChange, snapshotDefinition } from "./lib/browser-transcript.js";
|
|
10
10
|
import { appendBrowserRecord, captureBrowserBranch, hasPublishedBrowserJournal, readBrowserEntries, requirePublishedBrowserJournal } from "./lib/browser-journal.js";
|
|
11
11
|
import { buildExecutionPlan, canUseHeadlessCompatibilityUserAgent, createEphemeralSessionSeed, createFreshSessionName, createImplicitSessionName, extractUpstreamCommandTokens, getImplicitSessionCloseTimeoutMs, getImplicitSessionIdleTimeoutMs, isRestorableManagedSessionName, restoreManagedSessionStateFromBranch, validateToolArgs, redactInvocationArgs, redactSensitiveText, redactSensitiveValue, isPlainTextInspectionArgs, } from "./lib/runtime.js";
|
|
12
|
-
import { deleteIdentityKeysInNamespace, extractExplicitNamespace, extractExplicitSessionName, getAgentBrowserSessionIdentityKey, isAgentBrowserSessionIdentityKeyInNamespace, isUpstreamEnvFlagEnabled, resolveAgentBrowserNamespace, scanUpstreamGlobalFlagOccurrences } from "./lib/argv-grammar.js";
|
|
12
|
+
import { isBooleanFlagEnabled, deleteIdentityKeysInNamespace, extractExplicitNamespace, extractExplicitSessionName, getAgentBrowserSessionIdentityKey, isAgentBrowserSessionIdentityKeyInNamespace, isUpstreamEnvFlagEnabled, resolveAgentBrowserNamespace, scanUpstreamGlobalFlagOccurrences } from "./lib/argv-grammar.js";
|
|
13
13
|
import { parseArgvDescriptor } from "./lib/argv-descriptor.js";
|
|
14
14
|
import { needsManagedSession } from "./lib/command-policy.js";
|
|
15
15
|
import { ManagedSessionRestoreState } from "./lib/managed-session-restore.js";
|
|
@@ -43,7 +43,7 @@ import { appendUniqueAgentBrowserNextActions, applyNamespaceToNextActions, apply
|
|
|
43
43
|
import { canRegisterWebSearchTool, loadAgentBrowserConfigSync } from "./lib/config.js";
|
|
44
44
|
import { appendRecordingReservationTransition, applyRecordingArtifactsToReservations, restoreRecordingReservationStateFromBranch, retireRecordingReservation, } from "./lib/recording-reservations.js";
|
|
45
45
|
import { createAgentBrowserWebSearchTool } from "./lib/web-search.js";
|
|
46
|
-
import { scopeReadConfirmationArgs } from "./lib/read-confirmation.js";
|
|
46
|
+
import { isSuccessfulNativeConfirmedClose, scopeReadConfirmationArgs } from "./lib/read-confirmation.js";
|
|
47
47
|
import { isDirectAgentBrowserBashAllowed, isHarmlessAgentBrowserInspectionCommand, looksLikeDirectAgentBrowserBash, } from "./lib/bash-guard.js";
|
|
48
48
|
import { AgentBrowserResultComponent, formatAgentBrowserRenderCall, } from "./lib/pi-tool-rendering.js";
|
|
49
49
|
function browserExecutionFailure(error, signal, fallbackCategory = "upstream-error") {
|
|
@@ -113,6 +113,8 @@ function getArtifactPreflightValidationError(options) {
|
|
|
113
113
|
const artifactDestinations = new Map();
|
|
114
114
|
let sawBatchClose = false;
|
|
115
115
|
for (const [index, commandStep] of steps.entries()) {
|
|
116
|
+
if (commandStep.length === 0)
|
|
117
|
+
continue; // Native skips empty stdin rows.
|
|
116
118
|
if (batch) {
|
|
117
119
|
const stepValidationError = validateToolArgs(commandStep, { batchStep: true });
|
|
118
120
|
if (stepValidationError)
|
|
@@ -288,7 +290,7 @@ function restoreAttachedSessionKeysFromBranch(branch) {
|
|
|
288
290
|
if (!sessionName)
|
|
289
291
|
continue;
|
|
290
292
|
const sessionKey = getSessionContextKey(sessionName, namespace) ?? sessionName;
|
|
291
|
-
if ((succeeded && isCloseCommand(extractUpstreamCommandTokens(args)[0])) || terminalBatchClose)
|
|
293
|
+
if ((succeeded && (isCloseCommand(typeof details.command === "string" ? details.command : extractUpstreamCommandTokens(args)[0]) || isSuccessfulNativeConfirmedClose(extractUpstreamCommandTokens(args), details.data))) || terminalBatchClose)
|
|
292
294
|
attachedSessionKeys.delete(sessionKey);
|
|
293
295
|
else if (details.attachedBrowserSession === true || isAttachedBrowserInvocation(args, {}))
|
|
294
296
|
attachedSessionKeys.add(sessionKey);
|
|
@@ -563,10 +565,11 @@ function collectBranchManagedResourceEvents(branch) {
|
|
|
563
565
|
const replacedSessionNamespace = typeof outcome.replacedSessionNamespace === "string" ? outcome.replacedSessionNamespace : namespace;
|
|
564
566
|
setBranchRankForString(events.managedSessionCloseRanks, getSessionContextKey(typeof outcome.replacedSessionName === "string" ? outcome.replacedSessionName : undefined, replacedSessionNamespace), eventRank);
|
|
565
567
|
}
|
|
566
|
-
|
|
568
|
+
const closesSession = isCloseCommand(command) || isSuccessfulNativeConfirmedClose(extractUpstreamCommandTokens(getToolResultArgs(details)), details.data);
|
|
569
|
+
if (succeeded && !closesSession && sessionName && ((!explicitSessionName && (usedImplicitSession || sessionMode === "fresh")) || details.managedSessionHeadedAutosaveDisabled === true || typeof details.managedSessionHeadedAutosaveInterval === "string")) {
|
|
567
570
|
setBranchManagedSessionActive(events, sessionName, namespace, eventRank);
|
|
568
571
|
}
|
|
569
|
-
if (succeeded &&
|
|
572
|
+
if (succeeded && closesSession) {
|
|
570
573
|
setBranchRankForString(events.managedSessionCloseRanks, getSessionContextKey(explicitSessionName ?? sessionName ?? outcomeAttemptedSessionName ?? outcomeCurrentSessionName, namespace), eventRank);
|
|
571
574
|
}
|
|
572
575
|
if (closeAllApplied) {
|
|
@@ -618,12 +621,12 @@ function syncElectronCleanupManagedSessions(sessions, cleanupResults, fallbackNa
|
|
|
618
621
|
untrackOwnedManagedSession(sessions, identity.sessionName, identity.namespace);
|
|
619
622
|
}
|
|
620
623
|
}
|
|
621
|
-
async function closeOwnedManagedSessionsExcept(sessions, restoreState, keepSessionName, timeoutMs, attachedSessionKeys, keepNamespace, onClosed) {
|
|
624
|
+
async function closeOwnedManagedSessionsExcept(sessions, restoreState, sessionPageState, keepSessionName, timeoutMs, attachedSessionKeys, keepNamespace, onClosed) {
|
|
622
625
|
const keepKey = getSessionContextKey(keepSessionName, keepNamespace);
|
|
623
626
|
for (const [key, owner] of [...sessions]) {
|
|
624
627
|
if (key === keepKey)
|
|
625
628
|
continue;
|
|
626
|
-
const error = await closeManagedSession({ cwd: owner.cwd, headedManagedAutosaveInterval: owner.headedManagedAutosaveInterval, namespace: owner.namespace, preserveAttachedBrowserSession: attachedSessionKeys.has(key), restoreState, sessionName: owner.sessionName, socketDir: owner.socketDir, timeoutMs });
|
|
629
|
+
const error = await closeManagedSession({ confirmActions: sessionPageState.get(key).confirmActions ?? process.env.AGENT_BROWSER_CONFIRM_ACTIONS, cwd: owner.cwd, headedManagedAutosaveInterval: owner.headedManagedAutosaveInterval, namespace: owner.namespace, preserveAttachedBrowserSession: attachedSessionKeys.has(key), restoreState, sessionName: owner.sessionName, socketDir: owner.socketDir, timeoutMs });
|
|
627
630
|
if (!error) {
|
|
628
631
|
sessions.delete(key);
|
|
629
632
|
onClosed?.(owner);
|
|
@@ -943,6 +946,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
|
|
|
943
946
|
};
|
|
944
947
|
const closeScriptSessionLeaseWithinQueue = async (sessionName, cwd) => {
|
|
945
948
|
const closeError = await withIsolatedAgentBrowserEnvironment(() => closeManagedSession({
|
|
949
|
+
confirmActions: sessionPageState.get(getAgentBrowserSessionIdentityKey(sessionName, AGENT_BROWSER_SCRIPT_NAMESPACE)).confirmActions,
|
|
946
950
|
cwd,
|
|
947
951
|
namespace: AGENT_BROWSER_SCRIPT_NAMESPACE,
|
|
948
952
|
restoreState: managedSessionRestoreState,
|
|
@@ -991,6 +995,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
|
|
|
991
995
|
const previousManagedSessionName = managedSessionName;
|
|
992
996
|
const previousFreshSessionOrdinal = freshSessionOrdinal;
|
|
993
997
|
const previousAttachedSessionKeys = attachedSessionKeys;
|
|
998
|
+
const previousSessionPageState = sessionPageState;
|
|
994
999
|
managedSessionBaseName = createImplicitSessionName(ctx.sessionManager.getSessionId(), ctx.cwd, ephemeralSessionSeed);
|
|
995
1000
|
browserOwnerSessionId = ctx.sessionManager.getSessionId();
|
|
996
1001
|
let branch;
|
|
@@ -1160,6 +1165,11 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
|
|
|
1160
1165
|
}
|
|
1161
1166
|
}
|
|
1162
1167
|
if (!options.resetRuntimeOwnership) {
|
|
1168
|
+
// Off-branch live owners still need their established setting for cleanup.
|
|
1169
|
+
for (const key of ownedManagedSessions.keys()) {
|
|
1170
|
+
if (!branchResourceEvents.managedSessionActiveIdentities.has(key))
|
|
1171
|
+
sessionPageState.setConfirmActions(key, previousSessionPageState.get(key).confirmActions);
|
|
1172
|
+
}
|
|
1163
1173
|
for (const sessionKey of previousAttachedSessionKeys) {
|
|
1164
1174
|
if (ownedManagedSessions.has(sessionKey))
|
|
1165
1175
|
attachedSessionKeys.add(sessionKey);
|
|
@@ -1239,6 +1249,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
|
|
|
1239
1249
|
electronLaunchRecords: electronRecordsToCleanup,
|
|
1240
1250
|
managedSessionRestoreState,
|
|
1241
1251
|
ownedManagedSessions,
|
|
1252
|
+
sessionPageState,
|
|
1242
1253
|
timeoutMs: implicitSessionCloseTimeoutMs,
|
|
1243
1254
|
});
|
|
1244
1255
|
preservedElectronProfileDirs = [...new Set([
|
|
@@ -1249,10 +1260,10 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
|
|
|
1249
1260
|
for (const identity of getCleanupResultsClosedManagedSessionIdentities(electronCleanupResults))
|
|
1250
1261
|
retireRecordingSession(identity.sessionName, identity.namespace);
|
|
1251
1262
|
if (quitting) {
|
|
1252
|
-
await closeOwnedManagedSessionsExcept(ownedManagedSessions, managedSessionRestoreState, undefined, implicitSessionCloseTimeoutMs, attachedSessionKeys, undefined, (owner) => retireRecordingSession(owner.sessionName, owner.namespace));
|
|
1263
|
+
await closeOwnedManagedSessionsExcept(ownedManagedSessions, managedSessionRestoreState, sessionPageState, undefined, implicitSessionCloseTimeoutMs, attachedSessionKeys, undefined, (owner) => retireRecordingSession(owner.sessionName, owner.namespace));
|
|
1253
1264
|
}
|
|
1254
1265
|
else {
|
|
1255
|
-
await closeOwnedManagedSessionsExcept(ownedManagedSessions, managedSessionRestoreState, managedSessionActive ? managedSessionName : undefined, implicitSessionCloseTimeoutMs, attachedSessionKeys, managedSessionActive ? managedSessionNamespace : undefined, (owner) => retireRecordingSession(owner.sessionName, owner.namespace));
|
|
1266
|
+
await closeOwnedManagedSessionsExcept(ownedManagedSessions, managedSessionRestoreState, sessionPageState, managedSessionActive ? managedSessionName : undefined, implicitSessionCloseTimeoutMs, attachedSessionKeys, managedSessionActive ? managedSessionNamespace : undefined, (owner) => retireRecordingSession(owner.sessionName, owner.namespace));
|
|
1256
1267
|
}
|
|
1257
1268
|
flushRecordingReservations();
|
|
1258
1269
|
notifyRecordingPersistence(ctx);
|
|
@@ -1404,11 +1415,11 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
|
|
|
1404
1415
|
await beforeExecute?.(nativeToolCallId, { ...ctx, signal });
|
|
1405
1416
|
const runtimeBrowserConfig = loadAgentBrowserConfigSync({ cwd: ctx.cwd, includeProjectConfig: shouldIncludeProjectConfig(ctx) });
|
|
1406
1417
|
const rootProfile = runtimeBrowserConfig.trustedBrowserDefaultProfile;
|
|
1407
|
-
const pendingReadConfirmation = sessionPageState.findReadConfirmation(resolvedInput.toolArgs, resolveAgentBrowserNamespace(resolvedInput.toolArgs, getAgentBrowserProcessEnvironment().AGENT_BROWSER_NAMESPACE));
|
|
1418
|
+
const pendingReadConfirmation = sessionPageState.findReadConfirmation(resolvedInput.toolArgs, resolveAgentBrowserNamespace(resolvedInput.toolArgs, getAgentBrowserProcessEnvironment().AGENT_BROWSER_NAMESPACE), resolvedInput.toolStdin);
|
|
1408
1419
|
const rootSessionId = process.env.PI_SUBAGENT_CHILD === "1" && process.env.PI_SUBAGENT_ROOT_SESSION_ID
|
|
1409
1420
|
? process.env.PI_SUBAGENT_ROOT_SESSION_ID : ctx.sessionManager.getSessionId();
|
|
1410
1421
|
return withNativeSessionDefaults(resolvedInput, browserCwd, signal, async (resolvedInput, withLaunchDefaults) => {
|
|
1411
|
-
const readConfirmation = sessionPageState.findReadConfirmation(resolvedInput.toolArgs, resolveAgentBrowserNamespace(resolvedInput.toolArgs, getAgentBrowserProcessEnvironment().AGENT_BROWSER_NAMESPACE));
|
|
1422
|
+
const readConfirmation = sessionPageState.findReadConfirmation(resolvedInput.toolArgs, resolveAgentBrowserNamespace(resolvedInput.toolArgs, getAgentBrowserProcessEnvironment().AGENT_BROWSER_NAMESPACE), resolvedInput.toolStdin);
|
|
1412
1423
|
if (readConfirmation)
|
|
1413
1424
|
resolvedInput = { ...resolvedInput, toolArgs: scopeReadConfirmationArgs(resolvedInput.toolArgs, readConfirmation) };
|
|
1414
1425
|
if (resolvedInput.kind === "qa" && resolvedInput.compiledQaPreset.checks.attached && !managedSessionActive && !extractExplicitSessionName(resolvedInput.toolArgs)) {
|
|
@@ -1657,13 +1668,16 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
|
|
|
1657
1668
|
&& (needsManagedSession(parseArgvDescriptor(toolArgs), resolvedInput.toolStdin) || (selectedPlan.startupScopedFlags?.length ?? 0) > 0);
|
|
1658
1669
|
const operationId = randomUUID();
|
|
1659
1670
|
const selectedKey = getAgentBrowserSessionIdentityKey(selectedPlan.sessionName ?? "default", selectedPlan.namespace);
|
|
1671
|
+
browserRunState.confirmationPolicyIdentity = selectedKey;
|
|
1672
|
+
const confirmActions = getAgentBrowserProcessEnvironment().AGENT_BROWSER_CONFIRM_ACTIONS;
|
|
1673
|
+
workingPageState.setConfirmActions(selectedKey, confirmActions);
|
|
1660
1674
|
const begin = { event: {
|
|
1661
1675
|
version: 1, phase: "begin", operationId, toolCallId, commandIndex: commandIndex ?? 0, isError: true,
|
|
1662
1676
|
state: { args: redactInvocationArgs(toolArgs), sessionName: selectedPlan.sessionName, namespace: selectedPlan.namespace,
|
|
1663
1677
|
ownerSessionId: ctx.sessionManager.getSessionId(), usedImplicitSession: selectedPlan.usedImplicitSession,
|
|
1664
1678
|
wrapperManaged: selectedPlan.managedSessionName !== undefined || ownedManagedSessions.has(selectedKey), managedSessionCwd: browserCwd,
|
|
1665
1679
|
managedSessionSocketDir: resolveAgentBrowserSocketDir({ ownedManagedSession: selectedPlan.managedSessionName !== undefined || ownedManagedSessions.has(selectedKey) }) },
|
|
1666
|
-
pages: [{ key: selectedKey, refs: { kind: "unknown" }, unknown: true }],
|
|
1680
|
+
pages: [{ key: selectedKey, confirmActions: confirmActions ?? null, refs: { kind: "unknown" }, unknown: true }],
|
|
1667
1681
|
} };
|
|
1668
1682
|
if (browserAffecting) {
|
|
1669
1683
|
try {
|
|
@@ -1716,6 +1730,8 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
|
|
|
1716
1730
|
result = { ...failure, details: { ...browserRunState.observedBrowserEffects, ...(isRecord(failure.details) ? failure.details : {}) } };
|
|
1717
1731
|
}
|
|
1718
1732
|
const branchRestoreStillCurrent = branch.isCurrent();
|
|
1733
|
+
if (browserRunState.confirmationPolicyMayBeEstablished === false)
|
|
1734
|
+
workingPageState.setConfirmActions(selectedKey, priorPages.get(selectedKey)?.confirmActions);
|
|
1719
1735
|
const resultDetails = isRecord(result.details) ? result.details : undefined;
|
|
1720
1736
|
if (resultDetails && begin.event.state.wrapperManaged === true)
|
|
1721
1737
|
resultDetails.managedSessionSocketDir = begin.event.state.managedSessionSocketDir;
|
|
@@ -1736,7 +1752,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
|
|
|
1736
1752
|
const closeAllApplied = resultDetails?.closeAllApplied === true;
|
|
1737
1753
|
const attachedSessionRemainsActive = result.isError !== true
|
|
1738
1754
|
|| ((attachedSessionRequested || attachedSessionKnown) && managedSessionOutcome?.activeAfter === true);
|
|
1739
|
-
const closesAttachedSession = (result.isError !== true && isCloseCommand(extractUpstreamCommandTokens(toolArgs)[0]))
|
|
1755
|
+
const closesAttachedSession = (result.isError !== true && (isCloseCommand(extractUpstreamCommandTokens(toolArgs)[0]) || isSuccessfulNativeConfirmedClose(extractUpstreamCommandTokens(toolArgs), resultDetails?.data)))
|
|
1740
1756
|
|| resultBatchCloseLifecycle?.endsClosed === true;
|
|
1741
1757
|
if (closeAllApplied) {
|
|
1742
1758
|
deleteIdentityKeysInNamespace(attachedSessionKeys, resultNamespace);
|
|
@@ -1835,7 +1851,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
|
|
|
1835
1851
|
delete result.details.refSnapshot;
|
|
1836
1852
|
result.details.artifactManifest = invocationArtifactManifest(branch.isCurrent() ? artifactManifest : browserRunState.artifactManifest, initialArtifactManifest, result.details);
|
|
1837
1853
|
}
|
|
1838
|
-
return applyAgentBrowserOutputPath({ cwd: operationCwd, outputPath, preserveTextContent: Array.isArray(params.args) && params.args
|
|
1854
|
+
return applyAgentBrowserOutputPath({ cwd: operationCwd, outputPath, preserveTextContent: Array.isArray(params.args) && isBooleanFlagEnabled(params.args, "--json"), result: warnRecordingPersistence(result) });
|
|
1839
1855
|
};
|
|
1840
1856
|
const closesAllSessions = commandClosesAllSessions(toolArgs, resolvedInput.toolStdin);
|
|
1841
1857
|
const runWithLaunchDefaults = async () => {
|
|
@@ -1847,7 +1863,14 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
|
|
|
1847
1863
|
stdin: resolvedInput.toolStdin,
|
|
1848
1864
|
});
|
|
1849
1865
|
const owner = ownedManagedSessions.get(getAgentBrowserSessionIdentityKey(plan.sessionName ?? "default", plan.namespace));
|
|
1850
|
-
|
|
1866
|
+
// Resolve inside the identity queue, before daemon inspection or any page helper.
|
|
1867
|
+
const explicitConfirmationSetting = ["--confirm-actions", "--config"].some(flag => scanUpstreamGlobalFlagOccurrences(toolArgs, flag).length > 0);
|
|
1868
|
+
const retainedConfirmActions = sessionPageState.get(getAgentBrowserSessionIdentityKey(plan.sessionName ?? "default", plan.namespace)).confirmActions;
|
|
1869
|
+
const confirmActions = !explicitConfirmationSetting && retainedConfirmActions !== undefined ? retainedConfirmActions : resolvedInput.nativeConfirmActions;
|
|
1870
|
+
return withAgentBrowserProcessEnvironment({
|
|
1871
|
+
...(owner?.socketDir ? { PI_AGENT_BROWSER_SOCKET_DIR: owner.socketDir } : {}),
|
|
1872
|
+
AGENT_BROWSER_CONFIRM_ACTIONS: confirmActions,
|
|
1873
|
+
}, async () => {
|
|
1851
1874
|
const run = (executionSignal = signal) => {
|
|
1852
1875
|
const execute = () => withLaunchDefaults
|
|
1853
1876
|
? withLaunchDefaults(inactive => runBrowserCommand(inactive, executionSignal), executionSignal)
|
|
@@ -2033,7 +2056,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
|
|
|
2033
2056
|
if (result.isError && details.error === undefined)
|
|
2034
2057
|
details.error = details.validationError ?? details.summary ?? result.content.filter(item => item.type === "text").map(item => item.text).join("\n");
|
|
2035
2058
|
const priorManifest = isSessionArtifactManifest(details.artifactManifest) ? details.artifactManifest : undefined;
|
|
2036
|
-
const renderOptions = { content: result.content, details, json: "code" in params || params.args
|
|
2059
|
+
const renderOptions = { content: result.content, details, json: "code" in params || isBooleanFlagEnabled(params.args ?? [], "--json"), succeeded: result.isError !== true,
|
|
2037
2060
|
persistentArtifactStore: branch.isCurrent() ? getPersistentSessionArtifactStore(ctx) : undefined,
|
|
2038
2061
|
withArtifactWrite: (write) => artifactExecutionQueue.run(() => {
|
|
2039
2062
|
if (!branch.isCurrent())
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { extractUpstreamCommandTokens } from "./argv-descriptor.js";
|
|
2
2
|
import { isCloseAllCommand, isCloseCommand } from "./command-taxonomy.js";
|
|
3
3
|
import { isRecord } from "./parsing.js";
|
|
4
|
+
import { isSuccessfulNativeConfirmedClose } from "./read-confirmation.js";
|
|
5
|
+
import { detectConfirmationRequired } from "./results/confirmation.js";
|
|
4
6
|
export function batchHasSuccessfulCloseAll(data, fallbackCommands = []) {
|
|
5
7
|
if (!Array.isArray(data))
|
|
6
8
|
return false;
|
|
@@ -10,7 +12,7 @@ export function batchHasSuccessfulCloseAll(data, fallbackCommands = []) {
|
|
|
10
12
|
const rowCommand = Array.isArray(row.command) && row.command.every((token) => typeof token === "string")
|
|
11
13
|
? row.command
|
|
12
14
|
: fallbackCommands[index];
|
|
13
|
-
return rowCommand ? isCloseAllCommand(extractUpstreamCommandTokens(rowCommand)) : false;
|
|
15
|
+
return rowCommand ? !detectConfirmationRequired(row.result) && isCloseAllCommand(extractUpstreamCommandTokens(rowCommand)) : false;
|
|
14
16
|
});
|
|
15
17
|
}
|
|
16
18
|
export function getSuccessfulBatchCloseLifecycle(rows, fallbackCommands = []) {
|
|
@@ -41,12 +43,13 @@ export function getSuccessfulBatchCloseLifecycle(rows, fallbackCommands = []) {
|
|
|
41
43
|
continue;
|
|
42
44
|
}
|
|
43
45
|
const [command, subcommand] = extractUpstreamCommandTokens(rowCommand);
|
|
44
|
-
if (stepSucceeded && isCloseCommand(command)) {
|
|
46
|
+
if (stepSucceeded && !detectConfirmationRequired(result) && (isCloseCommand(command) || isSuccessfulNativeConfirmedClose(extractUpstreamCommandTokens(rowCommand), result))) {
|
|
45
47
|
sawClose = true;
|
|
46
48
|
endsClosed = true;
|
|
47
49
|
browserActiveAfterClose = false;
|
|
48
50
|
recordingClosedAfterBatch = true;
|
|
49
|
-
|
|
51
|
+
const closeData = command === "confirm" && isRecord(result?.result) ? result.result.data : result;
|
|
52
|
+
statePath = isRecord(closeData) && typeof closeData.statePath === "string" ? closeData.statePath : undefined;
|
|
50
53
|
}
|
|
51
54
|
else if (sawClose && command === "record") {
|
|
52
55
|
if (browserLaunched !== false) {
|
|
@@ -44,6 +44,7 @@ export function getBrowserRecord(entry) {
|
|
|
44
44
|
}
|
|
45
45
|
if (event.pages !== undefined && (!Array.isArray(event.pages) || !event.pages.every(page => isRecord(page)
|
|
46
46
|
&& typeof page.key === "string" && isRecord(page.refs) && ["reuse", "replace", "invalidate", "unknown"].includes(String(page.refs.kind))
|
|
47
|
+
&& (page.confirmActions === undefined || page.confirmActions === null || typeof page.confirmActions === "string")
|
|
47
48
|
&& (!["reuse", "replace"].includes(String(page.refs.kind)) || typeof page.refs.snapshotId === "string" && page.refs.snapshotId.length > 0)
|
|
48
49
|
&& (page.target === undefined || isRecord(page.target) && normalizeSessionTabTarget(page.target) !== undefined)
|
|
49
50
|
&& (page.unknown === undefined || page.unknown === true) && (page.clear === undefined || page.clear === true)
|
|
@@ -96,7 +97,7 @@ export function snapshotFromDefinition(definition) {
|
|
|
96
97
|
}
|
|
97
98
|
export function pageChange(key, page, previousSnapshotId) {
|
|
98
99
|
return {
|
|
99
|
-
key, target: page.tabTarget, unknown: page.tabTargetUnknown, reopenPending: page.tabReopenPending, pinningReason: page.pinningReason,
|
|
100
|
+
key, confirmActions: page.confirmActions ?? null, target: page.tabTarget, unknown: page.tabTargetUnknown, reopenPending: page.tabReopenPending, pinningReason: page.pinningReason,
|
|
100
101
|
refs: page.tabTargetUnknown ? { kind: "unknown", invalidation: page.refSnapshotInvalidation }
|
|
101
102
|
: page.refSnapshot && !targetsMatch(page.refSnapshot.target, page.tabTarget) ? { kind: "invalidate", invalidation: buildPageTransitionRefSnapshotInvalidation(`The saved refs came from a snapshot for ${page.refSnapshot.target?.url}; the current session target is ${page.tabTarget?.url}. Take a fresh snapshot before using page-scoped refs.`) }
|
|
102
103
|
: page.refSnapshot?.snapshotId ? { kind: page.refSnapshot.snapshotId === previousSnapshotId ? "reuse" : "replace", snapshotId: page.refSnapshot.snapshotId }
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { JsonSchema } from "./json-schema.js";
|
|
2
2
|
import { isRecord } from "./parsing.js";
|
|
3
|
+
import { isBooleanFlagEnabled } from "./argv-grammar.js";
|
|
3
4
|
import { finalizeAgentBrowserFailure } from "./pi-tool-rendering.js";
|
|
4
5
|
import { isPlainTextInspectionArgs } from "./runtime.js";
|
|
5
6
|
import { projectAgentBrowserObservation, OBSERVATION_INLINE_MAX_CHARS } from "./results/presentation/content.js";
|
|
@@ -32,7 +33,7 @@ export async function finalizeAgentBrowserNativeResult(result, input) {
|
|
|
32
33
|
const { renderAgentBrowserObservation } = await import("./results/presentation/large-output.js");
|
|
33
34
|
const rendered = await renderAgentBrowserObservation({
|
|
34
35
|
content: finalized.content, details, succeeded,
|
|
35
|
-
json: isRecord(input) && ("code" in input || Array.isArray(input.args) && input.args
|
|
36
|
+
json: isRecord(input) && ("code" in input || Array.isArray(input.args) && isBooleanFlagEnabled(input.args, "--json")),
|
|
36
37
|
// The caller's exact visible output stays inline (for example formatted search results); only the structured field is bounded.
|
|
37
38
|
preserveContent: true,
|
|
38
39
|
});
|
|
@@ -69,13 +69,7 @@ export function parseBatchCommandArgument(command) {
|
|
|
69
69
|
function validateUserBatchStep(step, index) {
|
|
70
70
|
if (!Array.isArray(step)) {
|
|
71
71
|
return {
|
|
72
|
-
error: `agent_browser batch stdin step ${index} must be
|
|
73
|
-
ok: false,
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
if (step.length === 0) {
|
|
77
|
-
return {
|
|
78
|
-
error: `agent_browser batch stdin step ${index} must not be empty.${BATCH_STDIN_EXAMPLE}`,
|
|
72
|
+
error: `agent_browser batch stdin step ${index} must be an array of string command tokens.${BATCH_STDIN_EXAMPLE}`,
|
|
79
73
|
ok: false,
|
|
80
74
|
};
|
|
81
75
|
}
|
|
@@ -137,5 +131,5 @@ export function getUpstreamEffectiveBatchSteps(commandTokens, stdin) {
|
|
|
137
131
|
});
|
|
138
132
|
if (argumentSteps.length > 0)
|
|
139
133
|
return argumentSteps;
|
|
140
|
-
return parseUserBatchStdin(stdin).steps ?? [];
|
|
134
|
+
return parseUserBatchStdin(stdin).steps?.filter(step => step.length > 0) ?? [];
|
|
141
135
|
}
|
|
@@ -2,6 +2,8 @@ import { isRecord, parseRefId } from "../../parsing.js";
|
|
|
2
2
|
import { redactSensitiveText } from "../../runtime.js";
|
|
3
3
|
import { withOptionalSessionArgs } from "../../results/next-actions.js";
|
|
4
4
|
import { runSessionCommandData } from "./session-state.js";
|
|
5
|
+
import { getAgentBrowserProcessTimeoutMs } from "../../process.js";
|
|
6
|
+
import { getAgentBrowserProcessEnvironment } from "../../process-environment.js";
|
|
5
7
|
const CLICK_DISPATCH_MARKER_PREFIX = "__piAgentBrowserClickDispatchProbe_";
|
|
6
8
|
const CLICK_DISPATCH_CLEANUP_TIMEOUT_MS = 2_000;
|
|
7
9
|
const ACCESSIBLE_REF_CLICK_DISPATCH_ROLES = new Set(["button", "checkbox", "menuitem", "radio", "switch", "tab"]);
|
|
@@ -34,7 +36,7 @@ function getEvalResultRecord(data) {
|
|
|
34
36
|
function getClickDispatchIdentityAttribute(probe) {
|
|
35
37
|
return `data-pi-click-dispatch-${probe.marker.toLowerCase()}`;
|
|
36
38
|
}
|
|
37
|
-
function buildClickDispatchProbeInstallScript(probe) {
|
|
39
|
+
function buildClickDispatchProbeInstallScript(probe, timeoutMs) {
|
|
38
40
|
const target = probe.target;
|
|
39
41
|
const resolveTarget = target.kind === "selector"
|
|
40
42
|
? `(() => { try { return document.querySelector(${JSON.stringify(target.selector)}); } catch { return null; } })()`
|
|
@@ -129,10 +131,15 @@ const listeners = eventTypes.map((type) => {
|
|
|
129
131
|
});
|
|
130
132
|
const identityAttribute = ${JSON.stringify(getClickDispatchIdentityAttribute(probe))};
|
|
131
133
|
state.removeIdentityMarker = () => element.removeAttribute(identityAttribute);
|
|
134
|
+
let cleanupTimer;
|
|
132
135
|
state.cleanup = () => {
|
|
136
|
+
clearTimeout(cleanupTimer);
|
|
133
137
|
state.removeIdentityMarker();
|
|
134
138
|
listeners.forEach(([type, listener]) => document.removeEventListener(type, listener, true));
|
|
139
|
+
try { delete window[marker]; } catch {}
|
|
135
140
|
};
|
|
141
|
+
// A native pending decision can forbid cleanup eval; expire without another command.
|
|
142
|
+
cleanupTimer = setTimeout(state.cleanup, ${timeoutMs + CLICK_DISPATCH_CLEANUP_TIMEOUT_MS});
|
|
136
143
|
window[marker] = state;
|
|
137
144
|
element.setAttribute(identityAttribute, marker);
|
|
138
145
|
return { status: "installed", marker, target: state.target };
|
|
@@ -205,13 +212,16 @@ export function buildClickDispatchNextActions(options) {
|
|
|
205
212
|
export async function prepareClickDispatchProbe(options) {
|
|
206
213
|
if (!options.sessionName || options.commandTokens[0] !== "click" || options.commandTokens.includes("--new-tab"))
|
|
207
214
|
return undefined;
|
|
215
|
+
// This optional diagnostic must not require approving a fresh eval on every click retry.
|
|
216
|
+
if (getAgentBrowserProcessEnvironment().AGENT_BROWSER_CONFIRM_ACTIONS?.split(",").some(action => action.trim().toLowerCase() === "evaluate"))
|
|
217
|
+
return undefined;
|
|
208
218
|
const target = getClickDispatchProbeTarget(options.commandTokens, options.refSnapshot);
|
|
209
219
|
if (!target)
|
|
210
220
|
return undefined;
|
|
211
221
|
const probe = { marker: `${CLICK_DISPATCH_MARKER_PREFIX}${Date.now().toString(36)}_${Math.random().toString(36).slice(2)}`, target };
|
|
212
222
|
let prepared = false;
|
|
213
223
|
try {
|
|
214
|
-
const installData = await runSessionCommandData({ args: ["eval", "--stdin"], cwd: options.cwd, namespace: options.namespace, sessionName: options.sessionName, signal: options.signal, stdin: buildClickDispatchProbeInstallScript(probe) });
|
|
224
|
+
const installData = await runSessionCommandData({ args: ["eval", "--stdin"], cwd: options.cwd, namespace: options.namespace, sessionName: options.sessionName, signal: options.signal, stdin: buildClickDispatchProbeInstallScript(probe, options.timeoutMs ?? getAgentBrowserProcessTimeoutMs()) });
|
|
215
225
|
if (getEvalResultRecord(installData)?.status !== "installed")
|
|
216
226
|
return undefined;
|
|
217
227
|
// Name matching and in-page XPath only find candidates. Native resolution
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import { runAgentBrowserProcess, withAttachedBrowserSessionContext, withChromeStartupArgs } from "../../process.js";
|
|
2
2
|
import { isRecord } from "../../parsing.js";
|
|
3
|
+
import { getAgentBrowserSessionIdentityKey, isBooleanFlagEnabled, resolveAgentBrowserNamespace } from "../../argv-grammar.js";
|
|
4
|
+
import { getAgentBrowserProcessEnvironment } from "../../process-environment.js";
|
|
5
|
+
import { parseArgvDescriptor } from "../../argv-descriptor.js";
|
|
6
|
+
import { isBrowserIndependentRead, needsManagedSession } from "../../command-policy.js";
|
|
7
|
+
import { nextReadConfirmation, parseReadConfirmation, suppressConfirmationPageHelpers } from "../../read-confirmation.js";
|
|
8
|
+
import { buildToolPresentation } from "../../results/presentation.js";
|
|
9
|
+
import { buildAgentBrowserResultCategoryDetails } from "../../results/categories.js";
|
|
3
10
|
import { collectNativeWebMcp, getNativeWebMcpCatalog } from "../../webmcp-observation.js";
|
|
4
11
|
import { isPlainTextInspectionArgs, redactSensitiveValue } from "../../runtime.js";
|
|
5
12
|
import { formatWebMcpCatalogUpdate } from "../../results/presentation/common.js";
|
|
6
13
|
import { withOwnedManagedSessionContext } from "../../managed-session-restore.js";
|
|
7
14
|
import { cleanupClickDispatchProbe } from "./click-dispatch.js";
|
|
8
|
-
import { applyBrowserRunStatePatch, getSessionContextKey } from "./session-state.js";
|
|
15
|
+
import { applyBrowserRunStatePatch, getSessionContextKey, nativePolicyDefinitelyUnestablished, withSessionCommandObservation } from "./session-state.js";
|
|
9
16
|
import { buildScreenshotGeometry, collectScreenshotSample } from "./screenshot-observation.js";
|
|
10
17
|
import { buildJsonVisibleContent, buildMissingBinaryFailureResult } from "./final-result.js";
|
|
11
18
|
import { prepareBrowserRun } from "./prepare.js";
|
|
@@ -13,14 +20,42 @@ import { processBrowserOutput } from "./process-output.js";
|
|
|
13
20
|
export { closeManagedSession } from "./managed-session-daemon-policy.js";
|
|
14
21
|
export { getSessionContextKey } from "./session-state.js";
|
|
15
22
|
export async function runAgentBrowserTool(options) {
|
|
16
|
-
const observed = await collectNativeWebMcp(() =>
|
|
23
|
+
const observed = await collectNativeWebMcp(() => withSessionCommandObservation({
|
|
24
|
+
allow: helper => {
|
|
25
|
+
const pending = parseReadConfirmation(options.state.observedBrowserEffects?.readConfirmation);
|
|
26
|
+
return !needsManagedSession(parseArgvDescriptor(helper.args), helper.stdin) && !isBrowserIndependentRead(helper.args, helper.stdin)
|
|
27
|
+
|| pending?.state !== "pending" || getAgentBrowserSessionIdentityKey(helper.sessionName ?? "default", resolveAgentBrowserNamespace([], helper.namespace ?? getAgentBrowserProcessEnvironment().AGENT_BROWSER_NAMESPACE)) !== getAgentBrowserSessionIdentityKey(pending.sessionName, pending.namespace);
|
|
28
|
+
},
|
|
29
|
+
observe: (helper, processResult, envelope) => {
|
|
30
|
+
const namespace = resolveAgentBrowserNamespace([], helper.namespace ?? getAgentBrowserProcessEnvironment().AGENT_BROWSER_NAMESPACE);
|
|
31
|
+
const sessionName = helper.sessionName ?? "default";
|
|
32
|
+
const key = getAgentBrowserSessionIdentityKey(sessionName, namespace);
|
|
33
|
+
if (key === options.state.confirmationPolicyIdentity && !nativePolicyDefinitelyUnestablished(processResult, envelope, helper.args))
|
|
34
|
+
options.state.confirmationPolicyMayBeEstablished = true;
|
|
35
|
+
const confirmation = nextReadConfirmation({ commandTokens: helper.args, current: options.state.sessionPageState.getReadConfirmation(key), data: envelope?.data, namespace, sessionName, succeeded: envelope?.success === true && processResult.exitCode === 0 && !processResult.aborted && !processResult.timedOut });
|
|
36
|
+
if (confirmation) {
|
|
37
|
+
options.state.sessionPageState.applyReadConfirmation(confirmation, options.sessionPageStateUpdate);
|
|
38
|
+
if (helper.args[0] === "open" && confirmation.state === "pending") {
|
|
39
|
+
const invalidation = options.state.sessionPageState.get(key).refSnapshotInvalidation;
|
|
40
|
+
options.state.sessionPageState.markTabTargetUnknown({ sessionName: key, update: options.sessionPageStateUpdate });
|
|
41
|
+
if (invalidation)
|
|
42
|
+
options.state.sessionPageState.applyRefSnapshotInvalidation({ sessionName: key, update: options.sessionPageStateUpdate, invalidation });
|
|
43
|
+
}
|
|
44
|
+
options.state.observedBrowserEffects = { ...options.state.observedBrowserEffects, readConfirmation: confirmation };
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
}, () => withChromeStartupArgs(options.input.persistentChromeArgs, () => withAttachedBrowserSessionContext(options.preserveAttachedBrowserSession === true, () => runAgentBrowserToolInContext(options)))));
|
|
17
48
|
const result = observed.result;
|
|
18
49
|
let details = isRecord(result.details) ? result.details : undefined;
|
|
50
|
+
if (details?.readConfirmation === undefined && options.state.observedBrowserEffects?.readConfirmation) {
|
|
51
|
+
details = { ...details, readConfirmation: options.state.observedBrowserEffects.readConfirmation };
|
|
52
|
+
result.details = details;
|
|
53
|
+
}
|
|
19
54
|
if (observed.catalog) {
|
|
20
55
|
const catalog = redactSensitiveValue(observed.catalog);
|
|
21
56
|
details = { ...details, webMcpCatalog: catalog };
|
|
22
57
|
result.details = details;
|
|
23
|
-
if (!options.input.toolArgs
|
|
58
|
+
if (!isBooleanFlagEnabled(options.input.toolArgs, "--json") && JSON.stringify(getNativeWebMcpCatalog(details.data)) !== JSON.stringify(catalog)) {
|
|
24
59
|
const notice = formatWebMcpCatalogUpdate(catalog);
|
|
25
60
|
if (result.content[0]?.type === "text")
|
|
26
61
|
result.content[0] = { ...result.content[0], text: `${result.content[0].text}\n\n${notice}` };
|
|
@@ -42,11 +77,25 @@ export async function runAgentBrowserTool(options) {
|
|
|
42
77
|
};
|
|
43
78
|
}
|
|
44
79
|
async function runAgentBrowserToolInContext(options) {
|
|
45
|
-
|
|
80
|
+
let preparedResult = await prepareBrowserRun(options);
|
|
46
81
|
applyBrowserRunStatePatch(options.state, preparedResult.kind === "ready" ? preparedResult.prepared.statePatch : preparedResult.statePatch);
|
|
82
|
+
const helperConfirmation = parseReadConfirmation(options.state.observedBrowserEffects?.readConfirmation);
|
|
83
|
+
if (helperConfirmation?.state === "pending") {
|
|
84
|
+
if (preparedResult.kind === "ready")
|
|
85
|
+
await preparedResult.prepared.managedSessionPolicyLock?.release();
|
|
86
|
+
const data = { confirmation_required: true, confirmation_id: helperConfirmation.id, action: helperConfirmation.action ?? "read" };
|
|
87
|
+
const presentation = await buildToolPresentation({ commandInfo: { command: helperConfirmation.command, commandTokens: [helperConfirmation.command ?? "read"] }, cwd: options.cwd, envelope: { success: true, data }, namespace: helperConfirmation.namespace, sessionName: helperConfirmation.sessionName });
|
|
88
|
+
preparedResult = { kind: "early-result", result: {
|
|
89
|
+
content: [{ type: "text", text: `Native helper ${helperConfirmation.command} requires confirmation (${helperConfirmation.action}). The requested command was not dispatched.` }, ...presentation.content],
|
|
90
|
+
details: { ...options.state.observedBrowserEffects, agentBrowserStarted: false, args: options.input.redactedArgs, data, sessionName: helperConfirmation.sessionName, namespace: helperConfirmation.namespace, sessionTabTargetUnknown: options.state.sessionPageState.get(getAgentBrowserSessionIdentityKey(helperConfirmation.sessionName, helperConfirmation.namespace)).tabTargetUnknown, readConfirmation: helperConfirmation, nextActions: presentation.nextActions,
|
|
91
|
+
...buildAgentBrowserResultCategoryDetails({ succeeded: false, failureCategory: "confirmation-required" }) },
|
|
92
|
+
isError: true,
|
|
93
|
+
} };
|
|
94
|
+
}
|
|
47
95
|
if (preparedResult.kind === "early-result") {
|
|
96
|
+
options.state.confirmationPolicyMayBeEstablished ??= false;
|
|
48
97
|
const result = preparedResult.result;
|
|
49
|
-
if (options.modelVisible !== false && options.input.toolArgs
|
|
98
|
+
if (options.modelVisible !== false && isBooleanFlagEnabled(options.input.toolArgs, "--json") && !isPlainTextInspectionArgs(options.input.toolArgs)) {
|
|
50
99
|
const details = isRecord(result.details) ? result.details : {};
|
|
51
100
|
const summary = result.content.filter(item => item.type === "text").map(item => item.text).join("\n");
|
|
52
101
|
result.content = buildJsonVisibleContent({
|
|
@@ -70,7 +119,7 @@ async function runAgentBrowserToolInContext(options) {
|
|
|
70
119
|
const artifactRunStartedAtMs = Date.now();
|
|
71
120
|
const processResult = await withChromeStartupArgs(prepared.chromeStartupArgs, () => runAgentBrowserProcess({
|
|
72
121
|
args: prepared.processArgs,
|
|
73
|
-
|
|
122
|
+
nativeConfirmationDecision: suppressConfirmationPageHelpers(prepared.readConfirmation),
|
|
74
123
|
cwd: options.cwd,
|
|
75
124
|
env: ownedManagedSession
|
|
76
125
|
? { AGENT_BROWSER_IDLE_TIMEOUT_MS: options.implicitSessionIdleTimeoutMs }
|
|
@@ -99,8 +148,10 @@ async function runAgentBrowserToolInContext(options) {
|
|
|
99
148
|
sessionMode: prepared.sessionMode,
|
|
100
149
|
sessionTabCorrection: prepared.sessionTabCorrection,
|
|
101
150
|
});
|
|
102
|
-
if (missingBinaryResult)
|
|
151
|
+
if (missingBinaryResult) {
|
|
152
|
+
options.state.confirmationPolicyMayBeEstablished ??= false;
|
|
103
153
|
return missingBinaryResult;
|
|
154
|
+
}
|
|
104
155
|
const output = await processBrowserOutput({ ...options, artifactRunStartedAtMs, prepared, processResult });
|
|
105
156
|
applyBrowserRunStatePatch(options.state, output.statePatch);
|
|
106
157
|
if (isRecord(output.result.details) && Array.isArray(output.result.details.imageObservations) && prepared.commandTokens[0] === "screenshot") {
|