fix(webAgent): keep the aria snapshot fresh on client-rendered pages (SPA readiness + stale-ref recovery) - #625
fix(webAgent): keep the aria snapshot fresh on client-rendered pages (SPA readiness + stale-ref recovery)#625lmorchard wants to merge 6 commits into
Conversation
…ive tree Client-rendered pages (or ones mid client-side redirect) can yield an aria snapshot with no actionable elements. Because every action tool is ref-only (no coordinate/vision fallback), an empty interactive tree strands the agent: it sees a form in the screenshot but has no [ref=] to act on, and aborts. In addPageSnapshot(), if the tree carries no interactive refs, wait briefly and re-capture (bounded: 3 tries, 1s apart) before handing it to the model. Hypothesis-agnostic: fixes pre-hydration, pre-redirect, and briefly-hidden cases alike. Diagnosed via the Zoo eval forensic → source-join chain (Mattermost login abort). Experimental — validating against the eval. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Export hasInteractiveRefs and test it against the exact sparse pre-hydration shell that stranded the agent (generic > img,img -> false) vs a hydrated login form (textbox/button with refs -> true), plus a link and a ref-less role.
cb173b3 to
37427e9
Compare
- Anchor INTERACTIVE_REF_RE to the node role at line-start so an interactive word inside a non-interactive node's accessible name (e.g. `- generic "button" [ref=E5]`) no longer yields a false positive. - Clarify the retry-loop comment: initial capture + up to MAX_RETRIES recaptures. - Make the test mock snapshots use the real aria-YAML shape (`- <role> "<name>" [ref=…]`) so the readiness guard doesn't spuriously retry, and add regression tests for the name-vs-role false positive and yaml-quoted keys. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Addressed the Copilot review in 7446077:
Full |
On a recoverable "Invalid element reference" error the loop returned needsPageSnapshot:false, so no fresh snapshot was ever added — the model was stranded on a known-bad snapshot. The recovery prompt tells the model to "wait for the next page snapshot (it will arrive automatically)"; it never did, so the model confabulated refs off a frozen view until the consecutive-error budget killed the run. Observed on Magento (Zoo onestopshop) guest checkout, but the deadlock is general to any recoverable ref error on a client-rendered page. Force a snapshot refresh on the retry only for stale/invalid-ref errors (the one case where the model's view is known-bad and the prompt promised a refresh); other recoverable errors keep prior behavior. The existing consecutive-error budget bounds any refresh loop. Scoped via shouldRefreshSnapshotAfterError, which keys on the same phrase prompts.ts uses. Validated: with this refresh, checkout reps that threw 7-15 invalid-ref errors recover and pass instead of deadlocking at 5 consecutive. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Folded in a second, related fix ( Where the readiness guard (this PR's first fix) handles an empty initial snapshot, this handles mid-flow ref invalidation. On a recoverable The fix forces a refresh on the retry only for stale/invalid-ref errors (scoped via Validation (Zoo Magento guest-checkout, 11 reps each): baseline had 2/11 confabulation-deadlock failures; with the fix, 0/11 — and reps that threw 7–15 invalid-ref errors now recover and pass instead of dying at 5 consecutive. Full |
|
Going to give this a run through evals and see if things improve. 🤞 If so, this might fix a long-standing hard-to-fix bug. |
Correction + eval context (post-validation)Number correction: an earlier comment (and the original description) said reps threw "7–15 invalid-ref errors." That was a measurement artifact — I'd counted occurrences of the phrase "Invalid element reference" across all events, but that phrase lives in the recovery prompt ( This makes the fix's value clearer, not weaker: the trigger is occasional, but pre-fix a single stale ref was deterministically fatal — no refresh → the model confabulated refs off the frozen snapshot → 5 consecutive errors → dead. The before/after stands: 2/11 ref-deadlock failures → 0/11, plus an 11/11 confirmation run (same task, an in-stock product). The description has been updated. On the partial WebVoyagerX eval (run Follow-up: #636 tracks the more general element-ref robustness work (browser-layer self-heal on stale refs, nearest-valid-ref hints, etc.) — this PR is the targeted recovery fix; #636 is the broader direction. |
Retry eval landed — fix confirmed firing, not implicatedThe webSearch-credit-restored retry ( The one Remaining failures are dataset fragility (stale Booking dates + a stale Apple reference — pilo-evals-judge#99) and transient infra, not agent-quality regressions. |
Eval-scope note (calibrating the "validated" claim honestly)To be precise about what the evals do and don't support: We cannot claim this fix improved aggregate eval performance. Both WebVoyagerX partial runs (67% → 77%) are on the same post-fix build ( What the evidence does support (narrower, and solid):
Caveats on that count (why it's supporting evidence, not proof):
So: mechanism verified + a specific failure mode eliminated, not "raised the benchmark by X." A real score-delta measurement would need an interaction-heavy set (the Zoo mutation suite, not WebVoyagerX), larger N, and a clean same-task pre/post of the shipped build — and would still expect a small delta, because the fix's value is converting a silent confabulation-inducing failure into clean recovery, which a pass/fail score barely captures. |
Two related fixes for the same underlying problem: on client-rendered (SPA) pages, the agent can end up acting on a stale or empty aria snapshot. Every action tool is ref-only — a
[ref=…]minted by the snapshot, with no coordinate/vision fallback — so if the snapshot the model sees doesn't match the live DOM, the agent is stranded.Fix 1 — retry an empty initial snapshot (SPA readiness guard)
Problem: On a client-rendered page (or one mid client-side redirect) the captured aria snapshot can land on the pre-hydration shell: a tree with no actionable elements (e.g.
generic > img, img). With ref-only actions, an empty interactive tree strands the agent — it sees the form in the screenshot but has no ref to act on, and aborts. Observed on a Mattermost login page ("the accessibility tree is incomplete… I cannot proceed without valid element references").Fix: In
addPageSnapshot(), if the captured tree has no interactive refs, wait briefly and re-capture — bounded to 3 tries, 1s apart — before handing the snapshot to the model. Hypothesis-agnostic (pre-hydration, pre-redirect, briefly-hidden) and a no-op on any page that already has actionable elements.hasInteractiveRefskeys off the standard ARIA interactive roles that carry a ref.Fix 2 — refresh the snapshot after a stale-ref tool error (recovery deadlock)
Problem: On a recoverable
Invalid element referenceerror, the loop returnedneedsPageSnapshot: false, soaddPageSnapshotwas never called — the model stayed on a known-bad snapshot. Meanwhile the recovery guidance inprompts.tstells the model "wait for the next page snapshot (it will arrive automatically)." It never arrived, so the model confabulated refs off a frozen view (asserting "the tree now shows E44, which differs from previous snapshots" — when no new snapshot was ever sent) until the consecutive-error budget killed the run. A loop-vs-prompt contract mismatch, general to any recoverable ref error on a client-rendered page.Where Fix 1 handles an empty initial snapshot, Fix 2 handles mid-flow ref invalidation after the DOM mutates.
Fix: Force a snapshot refresh on the retry only for stale/invalid-ref errors (scoped via
shouldRefreshSnapshotAfterError, keyed on the same phraseprompts.tsuses); other recoverable errors keep prior behavior. The existing consecutive-error budget bounds any refresh loop.Validation (Zoo Magento guest-checkout, 11 reps each): baseline had 2/11 confabulation-deadlock failures; with the fix, 0/11, plus a follow-up 11/11 confirmation run. Invalid-ref errors are occasional — ~3% of actions, roughly one per checkout — but pre-fix a single one was deterministically fatal: no refresh → the model confabulated refs off the frozen snapshot → 5 consecutive errors → dead. The fix stops that cascade at the first error.
Tests
packages/core/test/webAgent.test.ts— 82 passing.hasInteractiveRefscovered against the exact sparse shell that stranded the agent vs. a hydrated form (+ link, ref-less role, name-vs-role false-positive, yaml-quoted key).shouldRefreshSnapshotAfterErrorcovered forInvalidRefException, theToolExecutionErrorcarrying the invalid-ref message, the alternate phrasing, an unrelated recoverable error (no refresh), and non-Error inputs.Provenance & confidence (honest)
Both were surfaced by an experimental eval-forensics pipeline (Zoo-benchmark failure → source-code join). Fix 1's specific trigger is a rare tail (the login task usually passes within the existing 1000ms settle) — treat it as a low-risk robustness guard. Fix 2's mechanism is verified in event logs (post-fix, the snapshot refreshes on every stale-ref error instead of freezing), and the confabulation-deadlock failure mode was eliminated in a small same-task comparison (2/11 → 0/11, N=11) plus an 11/11 confirmation on a different task. That is evidence the fix works; it is not a demonstrated benchmark-score gain (see the eval-scope note in the comments) — and on a Q&A-heavy set like WebVoyagerX we wouldn't expect one. It's the higher-impact of the two changes.
🤖 Generated with Claude Code