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.
- package/CHANGELOG.md +21 -0
- package/README.md +6 -6
- package/dist/extensions/agent-browser/index.js +0 -5
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +7 -80
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +4 -4
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +9 -2
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +26 -9
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +30 -3
- package/dist/extensions/agent-browser/lib/playbook.js +1 -1
- package/dist/extensions/agent-browser/lib/runtime.js +50 -46
- package/dist/extensions/agent-browser/lib/session-page-state.js +17 -7
- package/dist/extensions/agent-browser/lib/tool-surface.js +9 -3
- package/dist/extensions/agent-browser/lib/web-search.js +5 -3
- package/docs/ARCHITECTURE.md +4 -4
- package/docs/COMMAND_REFERENCE.md +5 -3
- package/docs/RELEASE.md +7 -9
- package/docs/SUPPORT_MATRIX.md +6 -6
- package/docs/TOOL_CONTRACT.md +6 -4
- package/docs/platform-smoke.md +2 -2
- package/package.json +10 -15
- package/platform-smoke.config.mjs +1 -1
- package/scripts/doctor.mjs +1 -1
- package/scripts/platform-smoke/doctor.mjs +1 -1
- package/scripts/platform-smoke/targets.mjs +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.0 - 2026-09-26
|
|
4
|
+
|
|
5
|
+
### Breaking Changes
|
|
6
|
+
|
|
7
|
+
- Require Node.js 24.21.0 or newer.
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Update the official Pi validation graph to 0.87.1, adopt TypeScript 7, npm 12, and the latest compatible development tooling, and remove stale transitive dependency overrides.
|
|
12
|
+
- Qualify official Pi, the forked Pi host, package smoke, native checkpoints, and platform builds on Node 24.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- Keep lifecycle sentinel replacement byte-stable outside its generated block without retaining the removed TypeScript compiler API.
|
|
17
|
+
|
|
18
|
+
## 0.7.1 - 2026-09-22
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- Defer the native replay helper import until awaited session restoration, preserving fast package registration and the complete restored tool surface. Public browser commands and saved-session behavior remain unchanged.
|
|
23
|
+
|
|
3
24
|
## 0.7.0 - 2026-09-22
|
|
4
25
|
|
|
5
26
|
### Changed
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ It is for Pi users who want agents to browse sites, inspect pages, click through
|
|
|
6
6
|
|
|
7
7
|
## Pi release qualification
|
|
8
8
|
|
|
9
|
-
Development host dependencies are pinned to official Pi **0.87.
|
|
9
|
+
Development host dependencies are pinned to official Pi **0.87.1**, with wildcard runtime peers. `npm run check:compat` checks the installed candidate SDK/CLI identity, typechecks, runs the existing offline tests (including the native Pi pipeline with a controlled provider and fake browser), and tests a packed runtime-only installation through both the SDK and the actual bundled RPC CLI. The fork lane additionally requires the native idle checkpoint hook. It does not substitute a fork SDK while leaving official types or child CLI installed.
|
|
10
10
|
|
|
11
11
|
Run in an empty HOME with a **short** TMPDIR outside your real home. Long temporary paths can exceed macOS's 103-byte Unix socket limit; keep the fixture's socket safety checks intact. Real-browser/checkpoint controls, live upstream help sampling, lifecycle dogfood, and platform qualification remain the separate gates documented below. A Pi-only compatibility run uses no live browser profiles, credentials, or agent-browser fork, and does not certify those external integrations.
|
|
12
12
|
|
|
@@ -84,7 +84,7 @@ The result is optimized for agent work:
|
|
|
84
84
|
| Agents build fragile shell commands or repeat browser calls for loops and branches | Exposes compact `agent_browser` for native commands and batch, `agent_browser_code` for fresh JavaScript against the same persistent browser, and discoverable action, QA, Electron, and source tools | `extensions/agent-browser/index.ts`, `extensions/agent-browser/lib/input-modes/`, [`docs/TOOL_CONTRACT.md`](docs/TOOL_CONTRACT.md) |
|
|
85
85
|
| Page snapshots are too large or viewport-blind | Shows compact, main-content-first summaries, surfaces an `Omitted high-value controls` section (plus `details.data.highValueControlRefIds`) when dense pages or desktop host screens hide editables, named surfaces/tabs, primary action buttons, and high-signal named links such as repository results from the trimmed ref lists, supports wrapper-side `snapshot -i --search <text>` / `--filter role=<role>` to trim dense pages while preserving full `details.refSnapshot`, supports `snapshot --viewport` for scroll/viewport metadata, supports `snapshot --diff` for quick ref-map deltas versus the prior tracked snapshot, and stores full redacted output in spill files when needed | `extensions/agent-browser/lib/results/snapshot.ts`, `extensions/agent-browser/lib/orchestration/browser-run/prepare.ts`, `test/agent-browser.presentation.test.ts`, `test/agent-browser.extension-validation.test.ts` |
|
|
86
86
|
| Screenshots/downloads get lost in text | Normalizes artifact paths, creates missing parent directories, leaves click/download execution to native download, and reports existence, size, cwd, session, and repair status | [`docs/COMMAND_REFERENCE.md`](docs/COMMAND_REFERENCE.md#download-screenshot-and-pdf-files) |
|
|
87
|
-
| Profile restores and tab drift confuse agents | Tracks managed sessions, keeps every upstream helper probe on the same idle-timeout launch configuration so the background browser is not restarted between a snapshot and action, re-selects target tabs after observed drift, live-verifies the active target and refreshes its title after successful tab selection or `tab close` (while retaining deliberate `about:blank` selections and post-close blank targets), rehydrates branch-backed session state on Pi session-tree changes, and pins later commands only for sessions with drift/restored-session risk | generated tab-recovery notes below; `test/agent-browser.extension-tab-recovery.test.ts` (drift and about:blank recovery), `test/agent-browser.extension-tabs.test.ts` (post-close target), `test/agent-browser.extension-ref-guards.test.ts` (snapshot/action environment and session-tree rehydration), `test/agent-browser.resume-state.test.ts` (persisted session / resume planning) |
|
|
87
|
+
| Profile restores and tab drift confuse agents | Tracks managed sessions, keeps every upstream helper probe on the same idle-timeout launch configuration so the background browser is not restarted between a snapshot and action, retains native CDP target IDs so same-address tabs remain distinct after resume, re-selects target tabs after observed drift, live-verifies the active target and refreshes its title after successful tab selection or `tab close` (while retaining new blank tabs, deliberate `about:blank` selections, and post-close blank targets, including batch transitions), rehydrates branch-backed session state on Pi session-tree changes, and pins later commands only for sessions with drift/restored-session risk | generated tab-recovery notes below; `test/agent-browser.extension-tab-recovery.test.ts` (drift and about:blank recovery), `test/agent-browser.extension-tabs.test.ts` (post-close target), `test/agent-browser.extension-ref-guards.test.ts` (snapshot/action environment and session-tree rehydration), `test/agent-browser.resume-state.test.ts` (persisted session / resume planning) |
|
|
88
88
|
| Auth/profile workflows can leak secrets | Supports `auth save --password-stdin`, redacts sensitive args, SAML/OAuth-bearing URLs, stdout/stderr, details, and snapshot spills, and discards malformed oversized stdout instead of persisting a parse-failure spill | `test/agent-browser.extension-security-redaction.test.ts` |
|
|
89
89
|
| Stateful cookies/storage/auth output bloats or leaks context | Presentation layer redacts `details.data` for cookies and credential-like storage values while keeping low-risk local QA values such as `theme: dark` readable; recursively scrubs other structured upstream JSON (network, diff, trace/profiler, stream, dashboard, chat, auth, dialog, frame, state, and similar) using sensitive key names plus string heuristics; masks sensitive argv flags and positionals, including raw batch rows and ordered code journals; scrubs secrets from failed batch step errors; and exposes a compact redacted `batch` matrix on top-level `details.data` | `extensions/agent-browser/lib/results/presentation.ts`, `extensions/agent-browser/lib/results/presentation/diagnostics.ts`, `extensions/agent-browser/lib/runtime.ts`, `test/agent-browser.presentation-diagnostics.test.ts` |
|
|
90
90
|
| Stale `@eN` refs fail mysteriously | Records per-session `details.refSnapshot`, rejects mismatched URLs / unknown refs / unsafe `batch` stdin ordering before spawn, adds recovery guidance to rerun `snapshot -i` or use stable `find` locators | `extensions/agent-browser/index.ts`, `extensions/agent-browser/lib/session-page-state.ts`, `test/agent-browser.session-page-state.test.ts`, `test/agent-browser.results.test.ts`, `test/agent-browser.extension-ref-guards.test.ts`, `test/agent-browser.extension-semantic-recovery.test.ts` |
|
|
@@ -102,7 +102,7 @@ Artifact results show known requested paths separately from reported/resolved lo
|
|
|
102
102
|
|
|
103
103
|
## Fastest way to try it
|
|
104
104
|
|
|
105
|
-
Use Pi 0.87.0 or newer. This package keeps optional Pi core imports as wildcard `peerDependencies` because Pi package docs require the host Pi install to provide those packages, pins its direct Pi validation dependencies to 0.87.
|
|
105
|
+
Use Node 24.21.0 or newer and Pi 0.87.0 or newer. This package keeps optional Pi core imports as wildcard `peerDependencies` because Pi package docs require the host Pi install to provide those packages, pins its direct Pi validation dependencies to 0.87.1, and makes Pi hosts below 0.87.0 a setup failure through `pi-agent-browser-doctor`. There are no compatibility shims for older Pi releases.
|
|
106
106
|
|
|
107
107
|
Install upstream `agent-browser` first and make sure it is on `PATH`:
|
|
108
108
|
|
|
@@ -492,7 +492,7 @@ Use native `batch --bail` for known sequences. Add an explicit destination/text
|
|
|
492
492
|
}
|
|
493
493
|
```
|
|
494
494
|
|
|
495
|
-
Use the exact returned recovery payload after errors. Timeout partial progress
|
|
495
|
+
Use the exact returned recovery payload after errors. Timeout partial progress reports current page and file observations separately from planned steps, whose outcomes remain unknown without upstream results. An existing artifact or matching URL does not prove any step ran. Multi-step timeouts offer inspection rather than guessing a retry position; a single retryable step may offer a retry candidate after inspection. For paced typing, native batch can interleave `keyboard type` characters and positional `wait <ms>` rows; see [the migration map](docs/TOOL_CONTRACT.md#07-migration). Do not assume a native `--delay` option reproduces the removed job behavior.
|
|
496
496
|
|
|
497
497
|
Both JSON and prose results expose a bounded canonical observation with recovery actions and artifact receipts. Oversized observations name an `observationPath` for the complete redacted result; audit-only `details` is not the model's sole recovery channel. Inner code calls avoid full model rendering, spills, and image encoding until output is selected.
|
|
498
498
|
|
|
@@ -720,7 +720,7 @@ Install upstream `agent-browser`, then install dependencies:
|
|
|
720
720
|
npm install
|
|
721
721
|
```
|
|
722
722
|
|
|
723
|
-
Use the npm version declared in `package.json` `packageManager` when refreshing `package-lock.json` (for example `npx -y npm@
|
|
723
|
+
Use the npm version declared in `package.json` `packageManager` when refreshing `package-lock.json` (for example `npx -y npm@12.1.0 install`) so optional-platform lockfile metadata does not drift. Use Node 24.21.0 and Pi 0.87.0 or newer for lifecycle and interactive browser smokes; the pinned Pi 0.87.1 devDependencies are validation fixtures, not an exact-version requirement for the host CLI. See [Environment and automation pitfalls](docs/RELEASE.md#environment-and-automation-pitfalls) in `docs/RELEASE.md`.
|
|
724
724
|
|
|
725
725
|
Checkout-only extension smoke test:
|
|
726
726
|
|
|
@@ -768,7 +768,7 @@ These calls return plain text and stay stateless: the extension does not inject
|
|
|
768
768
|
- After open/goto/navigate calls with --profile, --restore, --session-name, or --state, agent_browser best-effort re-selects the tab whose URL matches the returned page when restored tabs steal focus during launch or reconnect.
|
|
769
769
|
- After confirmed shutdown of an automatically restored managed session, the wrapper retains its complete recorded URL, including the fragment, until the first current-page operation (including get url and reload). Non-page calls such as tab list may start a daemon without fulfilling that reopen; explicit URL reads leave the managed browser and pending reopen untouched. The wrapper uses native open once, verifies the observed tab, and discards old refs/frame scope; it does not restore unsaved forms, JavaScript memory, or history. Explicit navigation, caller-owned/attached sessions, and restore-disabled sessions are not auto-reopened.
|
|
770
770
|
- For a still-live browser after tab drift or resume, the wrapper verifies/selects the intended tab before ref/semantic helpers and page commands; failed selection stops the call without navigating. Local commands, read <url>, URL a11y/vitals, diff url, window new, and explicit tab/navigation/connection/state recovery do not require the prior tab. Batch checks follow effective rows past non-page prefixes and stop at explicit context changes, preserving caller argv/stdin and continue-on-error behavior. Same-tab reselection is avoided because it clears refs. Use exact batch --bail for fail-fast, not --bail=<value>. Routine same-session calls skip tab-list preflights.
|
|
771
|
-
- For sessions with observed tab-drift risk, after a successful command on a known target tab, agent_browser also best-effort restores that intended tab if a restored/background tab steals focus after the command completes. Routine same-session commands skip
|
|
771
|
+
- For sessions with observed tab-drift risk, after a successful command on a known target tab, agent_browser also best-effort restores that intended tab if a restored/background tab steals focus after the command completes. Routine same-session commands skip focus correction; observed pages without a native targetId use tab list to establish their identity.
|
|
772
772
|
- If a known session target unexpectedly reports about:blank, agent_browser best-effort re-selects the prior intended target when it still exists; if recovery fails, it records the observed about:blank target and reports exact recovery guidance instead of treating the prior page as active.
|
|
773
773
|
- If upstream reports tab_gone, the pinned bound tab is gone; use details.nextActions (tab list / tab new) instead of assuming another tab is yours.
|
|
774
774
|
<!-- agent-browser-playbook:end wrapper-tab-recovery -->
|
|
@@ -2032,11 +2032,6 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
|
|
|
2032
2032
|
return finalizeObservation(result, params, ctx);
|
|
2033
2033
|
},
|
|
2034
2034
|
executionMode: beforeExecute ? "sequential" : undefined,
|
|
2035
|
-
renderCodeCall(args, theme, context) {
|
|
2036
|
-
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
2037
|
-
text.setText(formatAgentBrowserRenderCall(args, theme, context.expanded));
|
|
2038
|
-
return text;
|
|
2039
|
-
},
|
|
2040
2035
|
promptGuidelines: toolPromptGuidelines,
|
|
2041
2036
|
renderCall: agentBrowserTool.renderCall, renderResult: agentBrowserTool.renderResult,
|
|
2042
2037
|
});
|
|
@@ -784,81 +784,6 @@ function getTimeoutStepRetry(step) {
|
|
|
784
784
|
const command = step.args[0];
|
|
785
785
|
return command && TIMEOUT_RETRYABLE_COMMANDS.has(command) ? { args: ["batch"], stdin: JSON.stringify([step.args]) } : undefined;
|
|
786
786
|
}
|
|
787
|
-
function normalizeUrlForTimeoutComparison(url) {
|
|
788
|
-
if (!url)
|
|
789
|
-
return undefined;
|
|
790
|
-
try {
|
|
791
|
-
return new URL(url);
|
|
792
|
-
}
|
|
793
|
-
catch {
|
|
794
|
-
return undefined;
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
function currentUrlMatchesNavigationStep(currentUrl, plannedUrl) {
|
|
798
|
-
if (!currentUrl || !plannedUrl)
|
|
799
|
-
return false;
|
|
800
|
-
if (currentUrl === plannedUrl)
|
|
801
|
-
return true;
|
|
802
|
-
const current = normalizeUrlForTimeoutComparison(currentUrl);
|
|
803
|
-
const planned = normalizeUrlForTimeoutComparison(plannedUrl);
|
|
804
|
-
if (!current || !planned || current.origin !== planned.origin)
|
|
805
|
-
return false;
|
|
806
|
-
const plannedPath = planned.pathname.endsWith("/") ? planned.pathname : `${planned.pathname}/`;
|
|
807
|
-
const currentPath = current.pathname.endsWith("/") ? current.pathname : `${current.pathname}/`;
|
|
808
|
-
return planned.pathname === "/" || currentPath.startsWith(plannedPath);
|
|
809
|
-
}
|
|
810
|
-
function buildTimeoutProgressSteps(options) {
|
|
811
|
-
let retryStep;
|
|
812
|
-
let lastCompletedNavigationIndex;
|
|
813
|
-
const progressSteps = options.steps.map((step) => {
|
|
814
|
-
const stepArtifacts = options.artifacts.filter((artifact) => artifact.stepIndex === step.index);
|
|
815
|
-
const command = step.args[0];
|
|
816
|
-
const navigationUrl = isOpenNavigationCommand(command) || command === "pushstate" ? getLastPositionalToken(step.args) : undefined;
|
|
817
|
-
if (stepArtifacts.some((artifact) => artifact.exists)) {
|
|
818
|
-
return { ...step, reason: "Declared artifact exists on disk after timeout.", status: "completed" };
|
|
819
|
-
}
|
|
820
|
-
if (options.currentPageSource === "live" && currentUrlMatchesNavigationStep(options.currentPageUrl, navigationUrl)) {
|
|
821
|
-
lastCompletedNavigationIndex = step.index;
|
|
822
|
-
return { ...step, reason: "Live page URL was recovered after timeout.", status: "completed" };
|
|
823
|
-
}
|
|
824
|
-
return { ...step, reason: stepArtifacts.length > 0 ? "Declared artifact was not present when the watchdog fired." : undefined, status: "unknown" };
|
|
825
|
-
});
|
|
826
|
-
const highestCompletedIndex = Math.max(0, ...progressSteps.filter((step) => step.status === "completed").map((step) => step.index));
|
|
827
|
-
for (const step of progressSteps) {
|
|
828
|
-
if (step.status === "unknown" && step.index < highestCompletedIndex) {
|
|
829
|
-
step.status = "completed";
|
|
830
|
-
step.reason = "Later step completion evidence indicates the batch advanced past this step before timeout.";
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
for (const step of progressSteps) {
|
|
834
|
-
const command = step.args[0];
|
|
835
|
-
if (step.status === "completed" && (isOpenNavigationCommand(command) || command === "pushstate")) {
|
|
836
|
-
lastCompletedNavigationIndex = Math.max(lastCompletedNavigationIndex ?? 0, step.index);
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
for (const step of progressSteps) {
|
|
840
|
-
if (step.status === "completed")
|
|
841
|
-
continue;
|
|
842
|
-
if (!retryStep) {
|
|
843
|
-
const retry = getTimeoutStepRetry(step);
|
|
844
|
-
retryStep = {
|
|
845
|
-
...step,
|
|
846
|
-
reason: step.reason ?? (retry ? "Likely active when the wrapper watchdog fired." : "Likely active when the wrapper watchdog fired; executable retry omitted because this step may have already mutated page state."),
|
|
847
|
-
retry,
|
|
848
|
-
status: "failed",
|
|
849
|
-
};
|
|
850
|
-
Object.assign(step, retryStep);
|
|
851
|
-
continue;
|
|
852
|
-
}
|
|
853
|
-
step.status = "pending";
|
|
854
|
-
step.reason = step.reason ?? `Pending behind timed-out step ${retryStep.index}.`;
|
|
855
|
-
}
|
|
856
|
-
return {
|
|
857
|
-
openedButPostOpenTimedOut: lastCompletedNavigationIndex !== undefined && retryStep !== undefined && retryStep.index > lastCompletedNavigationIndex,
|
|
858
|
-
retryStep,
|
|
859
|
-
steps: progressSteps,
|
|
860
|
-
};
|
|
861
|
-
}
|
|
862
787
|
export async function collectTimeoutPartialProgress(options) {
|
|
863
788
|
if ((options.commandTokens[0] === "session" && options.commandTokens[1] === "info") || isBrowserIndependentRead(options.commandTokens, options.stdin))
|
|
864
789
|
return undefined;
|
|
@@ -873,14 +798,16 @@ export async function collectTimeoutPartialProgress(options) {
|
|
|
873
798
|
const plannedUrl = recoveredUrl ? undefined : getPlannedCurrentPageUrl(rawSteps);
|
|
874
799
|
const url = recoveredUrl ?? plannedUrl;
|
|
875
800
|
const currentPageSource = recoveredUrl ? "live" : plannedUrl ? "planned" : title ? "live" : undefined;
|
|
876
|
-
|
|
801
|
+
// A matching URL or existing file is current state, not an execution receipt.
|
|
802
|
+
const steps = rawSteps.map(step => ({ ...step, status: "unknown", reason: "Upstream did not report this step's outcome before timeout." }));
|
|
803
|
+
const singleStep = steps.length === 1 ? steps[0] : undefined;
|
|
804
|
+
const retry = singleStep ? getTimeoutStepRetry(singleStep) : undefined;
|
|
805
|
+
const retryStep = singleStep && retry ? { ...singleStep, retry } : undefined;
|
|
877
806
|
if (rawSteps.length === 0 && artifacts.length === 0 && !url && !title)
|
|
878
807
|
return undefined;
|
|
879
808
|
const foundArtifacts = artifacts.filter((artifact) => artifact.exists).length;
|
|
880
|
-
const completedSteps = stepProgress.steps.filter((step) => step.status === "completed").length;
|
|
881
809
|
const pageStateSummary = recoveredUrl || title ? " and current page state" : plannedUrl ? " and planned page URL" : "";
|
|
882
|
-
|
|
883
|
-
return { artifacts, currentPage: url || title ? { source: currentPageSource, title, url } : undefined, liveUrlRecovered: recoveredUrl !== undefined, openedButPostOpenTimedOut: stepProgress.openedButPostOpenTimedOut, retryStep: stepProgress.retryStep, steps: stepProgress.steps.length > 0 ? stepProgress.steps : undefined, summary: `Timed out before upstream returned final results; recovered ${completedSteps}/${rawSteps.length} planned step state${rawSteps.length === 1 ? "" : "s"} and ${foundArtifacts}/${artifacts.length} declared artifact path${artifacts.length === 1 ? "" : "s"}${pageStateSummary}.${retrySummary}` };
|
|
810
|
+
return { artifacts, currentPage: url || title ? { source: currentPageSource, title, url } : undefined, liveUrlRecovered: recoveredUrl !== undefined, retryStep, steps: steps.length > 0 ? steps : undefined, summary: `Timed out before upstream returned final results; ${steps.length} planned step outcome${steps.length === 1 ? " is" : "s are"} unknown. Found ${foundArtifacts}/${artifacts.length} declared artifact path${artifacts.length === 1 ? "" : "s"}${pageStateSummary}; these observations do not prove step execution.` };
|
|
884
811
|
}
|
|
885
812
|
function redactSensitivePathSegmentsForDiagnostic(path) {
|
|
886
813
|
return path.split(/([/\\]+)/).map((segment) => segment === "/" || segment === "\\" || /^[/\\]+$/.test(segment) ? segment : redactSensitiveText(segment) !== segment || /(?:secret|token|password|passwd|credential|auth|api[-_]?key|bearer)/i.test(segment) ? "[REDACTED]" : segment).join("");
|
|
@@ -934,7 +861,7 @@ export function formatTimeoutPartialProgressText(progress, pageTargetUnknown = f
|
|
|
934
861
|
const payload = JSON.stringify({ ...progress.retryStep.retry, stdin: JSON.stringify([redactInvocationArgs(progress.retryStep.args)]) });
|
|
935
862
|
lines.push(pageTargetUnknown
|
|
936
863
|
? `Retry candidate for step ${progress.retryStep.index}: ${payload}. Verify the current URL before running it.`
|
|
937
|
-
: `Retry
|
|
864
|
+
: `Retry candidate for step ${progress.retryStep.index} (outcome unknown): ${payload}`);
|
|
938
865
|
}
|
|
939
866
|
for (const artifact of progress.artifacts)
|
|
940
867
|
lines.push(`Artifact from step ${artifact.stepIndex}: ${redactSensitivePathSegmentsForDiagnostic(artifact.path)} (${artifact.exists ? `exists${typeof artifact.sizeBytes === "number" ? `, ${artifact.sizeBytes} bytes` : ""}` : "missing"})`);
|
|
@@ -217,7 +217,7 @@ function buildTimeoutPartialProgressNextActions(options) {
|
|
|
217
217
|
args: withOptionalSessionArgs(options.executionPlan.sessionName, ["batch", "--bail"]),
|
|
218
218
|
stdin: JSON.stringify([["get", "url"], ["snapshot", "-i"]]),
|
|
219
219
|
},
|
|
220
|
-
reason: `Verify the current URL, then inspect the page after timeout${stepIndex === undefined ? "" : ` before
|
|
220
|
+
reason: `Verify the current URL, then inspect the page after timeout${stepIndex === undefined ? "" : ` before considering a retry of step ${stepIndex}`}.`,
|
|
221
221
|
safety: "Fail-fast read-only recovery: snapshot runs only after get url succeeds, satisfying the wrapper page-target guard without trusting the planned URL.",
|
|
222
222
|
tool: "agent_browser",
|
|
223
223
|
}];
|
|
@@ -229,8 +229,8 @@ function buildTimeoutPartialProgressNextActions(options) {
|
|
|
229
229
|
? { ...retry, sessionMode: "fresh" }
|
|
230
230
|
: { ...retry, args: withOptionalSessionArgs(options.executionPlan.sessionName, retry.args) },
|
|
231
231
|
reason: freshSessionAbandoned
|
|
232
|
-
? `
|
|
233
|
-
: `
|
|
232
|
+
? `Consider retrying the single timed-out step${stepIndex === undefined ? "" : ` ${stepIndex}`} in a fresh browser session because the timed-out fresh session was not proven live.`
|
|
233
|
+
: `Consider retrying the single timed-out step${stepIndex === undefined ? "" : ` ${stepIndex}`} against the current browser session.`,
|
|
234
234
|
safety: "Only read-only or idempotent timeout steps get executable retry args; inspect current page/artifact state before using the action.",
|
|
235
235
|
tool: "agent_browser",
|
|
236
236
|
}];
|
|
@@ -240,7 +240,7 @@ function buildTimeoutPartialProgressNextActions(options) {
|
|
|
240
240
|
return [{
|
|
241
241
|
id: "inspect-current-page-after-timeout",
|
|
242
242
|
params: { args: withOptionalSessionArgs(options.executionPlan.sessionName, ["snapshot", "-i"]) },
|
|
243
|
-
reason: `Inspect the current page after timeout before deciding how to resume${stepIndex === undefined ? "" : `
|
|
243
|
+
reason: `Inspect the current page after timeout before deciding how to resume${stepIndex === undefined ? "" : ` with step ${stepIndex}`}.`,
|
|
244
244
|
safety: "Read details.timeoutPartialProgress first. Do not blindly retry mutating steps such as clicks, fills, key presses, selects, or checks; split the remaining flow into shorter batches around the next navigation or DOM mutation boundary.",
|
|
245
245
|
tool: "agent_browser",
|
|
246
246
|
}];
|
|
@@ -8,6 +8,7 @@ import { prepareAgentBrowserSpawnArgs, withChromeStartupArgs } from "../../proce
|
|
|
8
8
|
import { cleanupElectronLaunchResources } from "../../electron/cleanup.js";
|
|
9
9
|
import { launchElectronApp } from "../../electron/launch.js";
|
|
10
10
|
import { pathExists } from "../../fs-utils.js";
|
|
11
|
+
import { isRecord } from "../../parsing.js";
|
|
11
12
|
import { getCompiledSemanticActionSessionPrefix } from "../../input-modes/semantic-action.js";
|
|
12
13
|
import { tryNetworkRequestsPageFilter } from "./prepare/network-page-filter.js";
|
|
13
14
|
import { tryContainerScroll, tryPageScrollTo } from "./prepare/scroll-shims.js";
|
|
@@ -599,14 +600,20 @@ export async function prepareBrowserRun(options) {
|
|
|
599
600
|
});
|
|
600
601
|
if (!executionPlan.validationError && !executionPlan.plainTextInspection && !knownStaleRef && !invalidStdin && priorSessionTabTarget && pinSessionTab) {
|
|
601
602
|
signal?.throwIfAborted();
|
|
602
|
-
const
|
|
603
|
+
const reopenedData = coldManagedSession ? await runSessionCommandData({
|
|
603
604
|
args: ["open", priorSessionTabTarget.url], cwd, namespace: executionPlan.namespace, sessionName: executionPlan.sessionName, signal, timeoutMs: params.timeoutMs,
|
|
604
605
|
onProcessResult: ({ agentBrowserStarted }) => {
|
|
605
606
|
// A started open may have navigated even if its CLI was aborted before replying.
|
|
606
607
|
if (agentBrowserStarted && sessionStateKey)
|
|
607
608
|
sessionPageState.setTabReopenPending({ pending: false, sessionName: sessionStateKey, update: options.sessionPageStateUpdate });
|
|
608
609
|
},
|
|
609
|
-
})
|
|
610
|
+
}) : undefined;
|
|
611
|
+
const reopened = !coldManagedSession || reopenedData !== undefined;
|
|
612
|
+
if (coldManagedSession && reopened)
|
|
613
|
+
priorSessionTabTarget = {
|
|
614
|
+
...priorSessionTabTarget,
|
|
615
|
+
targetId: isRecord(reopenedData) && typeof reopenedData.targetId === "string" ? reopenedData.targetId : undefined,
|
|
616
|
+
};
|
|
610
617
|
const selection = signal?.aborted ? undefined : reopened
|
|
611
618
|
? await ensureSessionTabTarget({ cwd, namespace: executionPlan.namespace, sessionName: executionPlan.sessionName, signal, target: priorSessionTabTarget })
|
|
612
619
|
: { error: "agent-browser could not reopen the remembered URL after the managed browser shut down. Navigate explicitly, then run snapshot -i before retrying." };
|
|
@@ -4,7 +4,7 @@ import { parseArgvDescriptor } from "../../argv-descriptor.js";
|
|
|
4
4
|
import { isBrowserIndependentRead, needsManagedSession } from "../../command-policy.js";
|
|
5
5
|
import { deleteIdentityKeysInNamespace, getAgentBrowserSessionIdentityKey, isAgentBrowserSessionIdentityKeyInNamespace } from "../../argv-grammar.js";
|
|
6
6
|
import { batchHasSuccessfulCloseAll, getSuccessfulBatchCloseLifecycle } from "../../batch-lifecycle.js";
|
|
7
|
-
import { isCloseAllCommand, isCloseCommand, isOpenNavigationCommand, isPageMutationCommand, isRecordPageTransitionCommand, isUnverifiedPageTransitionCommand, isWindowOrDiffPageTransitionCommand } from "../../command-taxonomy.js";
|
|
7
|
+
import { isCloseAllCommand, isCloseCommand, isNavigationObservableCommandName, isOpenNavigationCommand, isPageMutationCommand, isRecordPageTransitionCommand, isUnverifiedPageTransitionCommand, isWindowOrDiffPageTransitionCommand } from "../../command-taxonomy.js";
|
|
8
8
|
import { OPEN_RESULT_TAB_CORRECTION_FLAGS } from "../../launch-scoped-flags.js";
|
|
9
9
|
import { cleanupElectronLaunchResources, inspectElectronLaunchStatus } from "../../electron/cleanup.js";
|
|
10
10
|
import { getResultingPageTargetState, commandRequiresLivePageVerification } from "../../page-target-validation.js";
|
|
@@ -26,7 +26,7 @@ import { isManagedSessionRestoreKey } from "../../managed-session-storage.js";
|
|
|
26
26
|
import { createFreshSessionName, extractUpstreamCommandTokens, resolveManagedSessionState } from "../../runtime.js";
|
|
27
27
|
import { getUpstreamEffectiveBatchSteps } from "../batch-stdin.js";
|
|
28
28
|
import { closeManagedSession, inspectManagedSessionDaemon } from "./managed-session-daemon-policy.js";
|
|
29
|
-
import { applyOpenResultTabCorrection, buildAboutBlankRecoveryHint, buildAboutBlankWarning, buildElectronPostCommandHealthDiagnostic, buildElectronRefFreshnessDiagnostic, buildElectronSessionMismatch, buildManagedSessionOutcome, collectOpenResultTabCorrection, collectSessionTabSelection, commandChoosesSessionTabTarget, extractNavigationSummaryFromData, extractStringResultField, findElectronLaunchRecordForSession, formatElectronPostCommandHealthText, formatElectronSessionMismatchText, getSessionContextKey, getStaleRefArgs, mergeNavigationSummaryIntoData, runSessionCommandData, shouldCaptureNavigationSummary, shouldCorrectSessionTabAfterCommand, shouldInspectElectronPostCommandHealth, updateTraceOwnerState, } from "./session-state.js";
|
|
29
|
+
import { applyOpenResultTabCorrection, buildAboutBlankRecoveryHint, buildAboutBlankWarning, buildElectronPostCommandHealthDiagnostic, buildElectronRefFreshnessDiagnostic, buildElectronSessionMismatch, buildManagedSessionOutcome, collectOpenResultTabCorrection, collectSessionTabSelection, collectSessionTabTarget, commandChoosesSessionTabTarget, extractNavigationSummaryFromData, extractStringResultField, findElectronLaunchRecordForSession, formatElectronPostCommandHealthText, formatElectronSessionMismatchText, getSessionContextKey, getStaleRefArgs, mergeNavigationSummaryIntoData, runSessionCommandData, shouldCaptureNavigationSummary, shouldCorrectSessionTabAfterCommand, shouldInspectElectronPostCommandHealth, updateTraceOwnerState, } from "./session-state.js";
|
|
30
30
|
import { collectClickDispatchDiagnostic } from "./click-dispatch.js";
|
|
31
31
|
import { buildScrollNoopDiagnostic, collectComboboxFocusDiagnostic, collectElectronBroadGetTextScopeDiagnostics, collectElectronHandoff, collectFillVerificationDiagnostic, collectNavigationSummary, collectOverlayBlockerDiagnostic, collectQaAttachedTarget, collectSnapshotOverlayBlockerDiagnostic, collectRecordingDependencyWarning, collectScrollPositionSnapshot, collectSelectorTextVisibilityDiagnostics, collectTimeoutPartialProgress, sleepMs, formatQaAttachedTargetText, getArtifactCleanupGuidance, getEvalResultWarning, getEvalStdinHint, getSourceLookupElectronContext, } from "./diagnostics.js";
|
|
32
32
|
import { repairScreenshotData } from "./prepare.js";
|
|
@@ -296,7 +296,10 @@ export async function processBrowserOutput(input) {
|
|
|
296
296
|
presentationEnvelope = { ...(presentationEnvelope ?? {}), error: clickDispatchDiagnostic.summary, success: false };
|
|
297
297
|
}
|
|
298
298
|
}
|
|
299
|
-
const tabTransition =
|
|
299
|
+
const tabTransition = dispatchedCommands.some((step) => {
|
|
300
|
+
const [command, subcommand] = extractUpstreamCommandTokens(step);
|
|
301
|
+
return command === "tab" && subcommand !== undefined && subcommand !== "list";
|
|
302
|
+
});
|
|
300
303
|
// Non-page rows (including a failed prefix) cannot retire a cold target for a navigation that never ran.
|
|
301
304
|
const resultingPageState = sessionPageState.get(sessionStateKey).tabReopenPending === true
|
|
302
305
|
? { currentPageUrl: prepared.priorSessionTabTarget?.url, pageTargetMayHaveChanged: false, pageUrlUnknown: prepared.priorSessionTabTargetUnknown === true }
|
|
@@ -306,11 +309,12 @@ export async function processBrowserOutput(input) {
|
|
|
306
309
|
currentPageUrl: prepared.priorSessionTabTarget?.url,
|
|
307
310
|
pageUrlUnknown: prepared.priorSessionTabTargetUnknown === true,
|
|
308
311
|
});
|
|
309
|
-
if (succeeded &&
|
|
312
|
+
if (succeeded && !nestedBatchClosed &&
|
|
310
313
|
(shouldCaptureNavigationSummary(prepared.executionPlan.commandInfo.command, presentationEnvelope?.data, prepared.executionPlan.commandInfo.subcommand) ||
|
|
314
|
+
(prepared.executionPlan.commandInfo.command === "batch" && dispatchedCommands.some(([command, subcommand]) => isNavigationObservableCommandName(command, subcommand))) ||
|
|
311
315
|
shouldCaptureSemanticActionNavigationSummary(prepared.compiledSemanticAction, presentationEnvelope?.data) ||
|
|
312
316
|
commandRequiresLivePageVerification(prepared.executionPlan.effectiveArgs, prepared.runtimeToolStdin) ||
|
|
313
|
-
|
|
317
|
+
destinationTransition || tabTransition)) {
|
|
314
318
|
navigationSummary = await collectNavigationSummary({ cwd, namespace: prepared.executionPlan.namespace, priorTarget: prepared.priorSessionTabTarget, reusePriorTitle: !tabTransition, sessionName: prepared.executionPlan.sessionName, signal });
|
|
315
319
|
}
|
|
316
320
|
// Failed transitions may already have changed the page; keep only a live observed URL.
|
|
@@ -319,8 +323,11 @@ export async function processBrowserOutput(input) {
|
|
|
319
323
|
!processResult.aborted &&
|
|
320
324
|
!processResult.timedOut &&
|
|
321
325
|
!nestedBatchClosed &&
|
|
322
|
-
(
|
|
323
|
-
|
|
326
|
+
dispatchedCommands.some((step) => {
|
|
327
|
+
const [command, subcommand] = extractUpstreamCommandTokens(step);
|
|
328
|
+
return (prepared.executionPlan.commandInfo.command === "batch" && isOpenNavigationCommand(command))
|
|
329
|
+
|| isUnverifiedPageTransitionCommand(command, subcommand);
|
|
330
|
+
})) {
|
|
324
331
|
navigationSummary = await collectNavigationSummary({ cwd, namespace: prepared.executionPlan.namespace, priorTarget: prepared.priorSessionTabTarget, sessionName: prepared.executionPlan.sessionName, signal });
|
|
325
332
|
// Re-verifying a failed transition's URL does not make its prior refs valid.
|
|
326
333
|
failedTransitionReverification = navigationSummary !== undefined;
|
|
@@ -342,11 +349,21 @@ export async function processBrowserOutput(input) {
|
|
|
342
349
|
const failedWebMcpSettlement = prepared.priorSessionTabTargetUnknown === true && ((!succeeded && isWebMcpSettlementCommand(prepared.executionPlan.commandInfo.command, prepared.executionPlan.commandInfo.subcommand))
|
|
343
350
|
|| (prepared.executionPlan.commandInfo.command === "batch" && batchHasFailedWebMcpSettlement(presentationEnvelope?.data)));
|
|
344
351
|
const unsettledWebMcpMutation = pendingWebMcpMutation || failedWebMcpSettlement;
|
|
345
|
-
|
|
352
|
+
let observedSessionTabTarget = unsettledWebMcpMutation || (unobservedMutation && !failedTransitionReverification)
|
|
346
353
|
? undefined
|
|
347
354
|
: normalizeSessionTabTarget(navigationSummary)
|
|
348
355
|
?? (trustsReportedPageTarget ? extractSessionTabTargetFromBatchResults(presentationEnvelope?.data) : undefined)
|
|
349
356
|
?? (succeeded && trustsReportedPageTarget ? extractSessionTabTargetFromCommandData(prepared.commandTokens, presentationEnvelope?.data) : undefined);
|
|
357
|
+
if (observedSessionTabTarget && !browserIndependentRead && !nestedBatchClosed && !isCloseCommand(prepared.executionPlan.commandInfo.command)) {
|
|
358
|
+
const reportedTarget = prepared.executionPlan.commandInfo.command === "batch"
|
|
359
|
+
? extractSessionTabTargetFromBatchResults(presentationEnvelope?.data)
|
|
360
|
+
: prepared.commandTokens[0] === "tab" && prepared.commandTokens[1] === "close" ? undefined
|
|
361
|
+
: extractSessionTabTargetFromCommandData(prepared.commandTokens, presentationEnvelope?.data);
|
|
362
|
+
if (reportedTarget?.targetId)
|
|
363
|
+
observedSessionTabTarget.targetId = reportedTarget.targetId;
|
|
364
|
+
else
|
|
365
|
+
observedSessionTabTarget = await collectSessionTabTarget({ cwd, namespace: prepared.executionPlan.namespace, sessionName: prepared.executionPlan.sessionName, signal, target: observedSessionTabTarget });
|
|
366
|
+
}
|
|
350
367
|
const safeObservedSessionTabTarget = observedSessionTabTarget;
|
|
351
368
|
let currentSessionTabTarget = safeObservedSessionTabTarget;
|
|
352
369
|
if (!currentSessionTabTarget && nestedBatchClose === undefined && !unobservedMutation) {
|
|
@@ -459,7 +476,7 @@ export async function processBrowserOutput(input) {
|
|
|
459
476
|
: processResult.agentBrowserStarted && (directTransitionInvalidation || plannedBatchTransitionInvalidation)
|
|
460
477
|
? directTransitionInvalidation ?? plannedBatchTransitionInvalidation
|
|
461
478
|
: failedTransitionReverification
|
|
462
|
-
? buildPageTransitionRefSnapshotInvalidation("A failed
|
|
479
|
+
? buildPageTransitionRefSnapshotInvalidation("A failed page transition may still have changed the page, so the prior snapshot refs were invalidated. Run snapshot -i before using page-scoped refs.")
|
|
463
480
|
: batchRefSnapshotState?.invalidation?.reason === "page-transition"
|
|
464
481
|
? batchRefSnapshotState.invalidation
|
|
465
482
|
: undefined;
|
|
@@ -460,6 +460,8 @@ export function shouldCorrectSessionTabAfterCommand(options) {
|
|
|
460
460
|
!isSessionTabPostCommandCorrectionExcludedCommand(options.command));
|
|
461
461
|
}
|
|
462
462
|
function getTabSelection(tab) {
|
|
463
|
+
if (tab.targetId)
|
|
464
|
+
return { selectedTab: tab.targetId, selectionKind: "targetId" };
|
|
463
465
|
if (typeof tab.tabId === "string" && tab.tabId.trim().length > 0)
|
|
464
466
|
return { selectedTab: tab.tabId.trim(), selectionKind: "tabId" };
|
|
465
467
|
if (typeof tab.label === "string" && tab.label.trim().length > 0)
|
|
@@ -467,6 +469,10 @@ function getTabSelection(tab) {
|
|
|
467
469
|
return typeof tab.index === "number" ? { selectedTab: String(tab.index), selectionKind: "index" } : undefined;
|
|
468
470
|
}
|
|
469
471
|
function selectSessionTargetTab(options) {
|
|
472
|
+
if (options.target.targetId) {
|
|
473
|
+
const target = selectAnySessionTargetTab(options);
|
|
474
|
+
return target && options.tabs.find((tab) => tab.targetId === options.target.targetId)?.active !== true ? target : undefined;
|
|
475
|
+
}
|
|
470
476
|
return chooseOpenResultTabCorrection({
|
|
471
477
|
tabs: options.tabs,
|
|
472
478
|
targetTitle: options.target.title,
|
|
@@ -477,7 +483,9 @@ function selectAnySessionTargetTab(options) {
|
|
|
477
483
|
const targetUrl = typeof options.target.url === "string" ? normalizeComparableUrl(options.target.url) : undefined;
|
|
478
484
|
if (!targetUrl)
|
|
479
485
|
return undefined;
|
|
480
|
-
const matchingTabs = options.tabs.filter((tab) =>
|
|
486
|
+
const matchingTabs = options.tabs.filter((tab) => options.target.targetId
|
|
487
|
+
? tab.targetId === options.target.targetId
|
|
488
|
+
: normalizeComparableUrl(tab.url ?? "") === targetUrl);
|
|
481
489
|
const targetTitle = options.target.title?.trim() ?? "";
|
|
482
490
|
const titledTabs = targetTitle ? matchingTabs.filter((tab) => tab.title?.trim() === targetTitle) : [];
|
|
483
491
|
const selectedTab = titledTabs.find((tab) => tab.active) ?? titledTabs[0] ?? matchingTabs.find((tab) => tab.active) ?? matchingTabs[0];
|
|
@@ -538,6 +546,7 @@ export async function collectOpenResultTabCorrection(options) {
|
|
|
538
546
|
index: typeof tab.index === "number" ? tab.index : index,
|
|
539
547
|
label: typeof tab.label === "string" ? tab.label : undefined,
|
|
540
548
|
tabId: typeof tab.tabId === "string" ? tab.tabId : undefined,
|
|
549
|
+
targetId: typeof tab.targetId === "string" ? tab.targetId : undefined,
|
|
541
550
|
title: typeof tab.title === "string" ? tab.title : undefined,
|
|
542
551
|
url: typeof tab.url === "string" ? tab.url : undefined,
|
|
543
552
|
}));
|
|
@@ -551,10 +560,21 @@ function mapTabData(tabData) {
|
|
|
551
560
|
index: typeof tab.index === "number" ? tab.index : index,
|
|
552
561
|
label: typeof tab.label === "string" ? tab.label : undefined,
|
|
553
562
|
tabId: typeof tab.tabId === "string" ? tab.tabId : undefined,
|
|
563
|
+
targetId: typeof tab.targetId === "string" ? tab.targetId : undefined,
|
|
554
564
|
title: typeof tab.title === "string" ? tab.title : undefined,
|
|
555
565
|
url: typeof tab.url === "string" ? tab.url : undefined,
|
|
556
566
|
}));
|
|
557
567
|
}
|
|
568
|
+
export async function collectSessionTabTarget(options) {
|
|
569
|
+
const tabs = mapTabData(await runSessionCommandData({ ...options, args: ["tab", "list"] }));
|
|
570
|
+
const active = tabs?.find((tab) => tab.active);
|
|
571
|
+
const observedUrl = normalizeComparableUrl(options.target.url);
|
|
572
|
+
if (normalizeComparableUrl(active?.url) !== observedUrl && tabs?.some((tab) => normalizeComparableUrl(tab.url) === observedUrl))
|
|
573
|
+
return options.target;
|
|
574
|
+
return active?.targetId
|
|
575
|
+
? { ...options.target, targetId: active.targetId }
|
|
576
|
+
: options.target;
|
|
577
|
+
}
|
|
558
578
|
export async function collectSessionTabSelection(options) {
|
|
559
579
|
const { cwd, namespace, sessionName, signal, target } = options;
|
|
560
580
|
const tabData = await runSessionCommandData({ args: ["tab", "list"], cwd, namespace, sessionName, signal });
|
|
@@ -569,13 +589,20 @@ export async function ensureSessionTabTarget(options) {
|
|
|
569
589
|
const error = "agent-browser could not re-select and verify the intended tab before running the command. Run tab list and select the intended tab, then snapshot -i before retrying.";
|
|
570
590
|
if (!correction)
|
|
571
591
|
return { error };
|
|
592
|
+
const verifyUrl = async (tab) => {
|
|
593
|
+
if (normalizeComparableUrl(tab.url) === normalizeComparableUrl(options.target.url))
|
|
594
|
+
return true;
|
|
595
|
+
// Native tab metadata can retain the attempted URL while get url reports Chrome's error page.
|
|
596
|
+
const data = await runSessionCommandData({ ...options, args: ["get", "url"] });
|
|
597
|
+
return normalizeComparableUrl(extractStringResultField(data, "url")) === normalizeComparableUrl(options.target.url);
|
|
598
|
+
};
|
|
572
599
|
// Native tab selection clears refs and frame scope even when selecting the current tab.
|
|
573
600
|
if (active && getTabSelection(active)?.selectedTab === correction.selectedTab)
|
|
574
|
-
return {};
|
|
601
|
+
return await verifyUrl(active) ? {} : { error };
|
|
575
602
|
if (!await applyOpenResultTabCorrection({ ...options, correction }))
|
|
576
603
|
return { correction, error };
|
|
577
604
|
const selected = (await readTabs())?.find((tab) => tab.active);
|
|
578
|
-
return selected && getTabSelection(selected)?.selectedTab === correction.selectedTab &&
|
|
605
|
+
return selected && getTabSelection(selected)?.selectedTab === correction.selectedTab && await verifyUrl(selected)
|
|
579
606
|
? { correction }
|
|
580
607
|
: { correction, error };
|
|
581
608
|
}
|
|
@@ -92,7 +92,7 @@ export const WRAPPER_TAB_RECOVERY_BEHAVIOR = [
|
|
|
92
92
|
"After open/goto/navigate calls with --profile, --restore, --session-name, or --state, agent_browser best-effort re-selects the tab whose URL matches the returned page when restored tabs steal focus during launch or reconnect.",
|
|
93
93
|
"After confirmed shutdown of an automatically restored managed session, the wrapper retains its complete recorded URL, including the fragment, until the first current-page operation (including get url and reload). Non-page calls such as tab list may start a daemon without fulfilling that reopen; explicit URL reads leave the managed browser and pending reopen untouched. The wrapper uses native open once, verifies the observed tab, and discards old refs/frame scope; it does not restore unsaved forms, JavaScript memory, or history. Explicit navigation, caller-owned/attached sessions, and restore-disabled sessions are not auto-reopened.",
|
|
94
94
|
"For a still-live browser after tab drift or resume, the wrapper verifies/selects the intended tab before ref/semantic helpers and page commands; failed selection stops the call without navigating. Local commands, read <url>, URL a11y/vitals, diff url, window new, and explicit tab/navigation/connection/state recovery do not require the prior tab. Batch checks follow effective rows past non-page prefixes and stop at explicit context changes, preserving caller argv/stdin and continue-on-error behavior. Same-tab reselection is avoided because it clears refs. Use exact batch --bail for fail-fast, not --bail=<value>. Routine same-session calls skip tab-list preflights.",
|
|
95
|
-
"For sessions with observed tab-drift risk, after a successful command on a known target tab, agent_browser also best-effort restores that intended tab if a restored/background tab steals focus after the command completes. Routine same-session commands skip
|
|
95
|
+
"For sessions with observed tab-drift risk, after a successful command on a known target tab, agent_browser also best-effort restores that intended tab if a restored/background tab steals focus after the command completes. Routine same-session commands skip focus correction; observed pages without a native targetId use tab list to establish their identity.",
|
|
96
96
|
"If a known session target unexpectedly reports about:blank, agent_browser best-effort re-selects the prior intended target when it still exists; if recovery fails, it records the observed about:blank target and reports exact recovery guidance instead of treating the prior page as active.",
|
|
97
97
|
"If upstream reports tab_gone, the pinned bound tab is gone; use details.nextActions (tab list / tab new) instead of assuming another tab is yours.",
|
|
98
98
|
];
|