Skip to content

fix(cooperation): stop live page text from refusing every action - #497

Merged
iuyo5678 merged 4 commits into
Tencent:mainfrom
NianJiuZst:fix/live-page-evidence-scope
Oct 27, 2026
Merged

iuyo5678 merged 4 commits into
Tencent:mainfrom
NianJiuZst:fix/live-page-evidence-scope

Conversation

@NianJiuZst

Copy link
Copy Markdown
Collaborator

Fixes #495

Summary

In shared-control mode, any text that updates by itself outside the action's region (a clock, countdown, live counter or ticker) could make every action fail with "The action's observed content changed; synchronize and replan". By the time the agent synced, replanned and acted, the text had changed again, so sync → act → refused could repeat forever. Until now, the only way out was --depends-on, and the skill docs tell agents to leave that out when they aren't sure what the decision depends on.

This PR keeps whole-page checking as the default. Text that changes by itself is now reported once and then tolerated, so the loop can't form.

Cause

  1. The content script's MutationObserver bumps revision and structure on every mutation (cooperation-capture.ts).
  2. A changed revision makes verifyEvidence re-capture the VOM before acting.
  3. Without depends_on there is no target scope, so sameObservationEvidence compares every node on the page, including name and text (observation-evidence.ts).
  4. structure changed too, so the next sync returns a full baseline. The clock ticks again while the model picks an action, and step 3 fails again.

Change

observation-evidence.ts

  • New contentChanges(before, after, known). It returns the nodes whose text changed when that is the only difference between two whole-page observations. These must be the same nodes (same frame and backendNodeId) in the same order, with no form value or checked state and not modal. Otherwise it returns undefined.
  • sameObservationEvidence takes an optional volatile set. It applies only to the whole-page check (no target): for those nodes, name and text are left out of the comparison. Everything else about them is still compared.

controller.ts (verifyEvidence)

  • Learning happens only when a whole-page check fails, and only if all of these hold:
    • no depends_on was given;
    • the human-input counter hasn't changed since the sync;
    • the action target's own region (its form, list, dialog or region) is unchanged;
    • contentChanges says the only differences are in-place text changes.
  • The action is still refused that first time, but the message now names the change, for example "10:00:00" → "10:00:01", and points to --depends-on. The nodes are remembered in TabState.volatile for the current documents (capped at 256 nodes). The remembered set is ignored once any frame's document changes.
  • Later whole-page checks tolerate further text-only changes to those nodes, so the replanned action goes through.
  • Sync guidance and the two skill/references/cooperation.md copies describe the new behavior, and so does docs/shared-browser-control.zh-CN.md.

What stays exact

  • The action target's region (form, list, dialog or region).
  • All form values and checked states, and all modal layers, anywhere on the page.
  • Added, removed or reordered nodes, and changes to identity, role, disabled, inert, visibility, pointer events or the semantic attributes.
  • Any explicit --depends-on / dependsOn check, even for nodes already learned as volatile.
  • Changes made while a human is interacting, which are never learned as page activity.

Tests

  • controller.test.ts → "self-changing page content":
    • ticking clock: refused once with the before → after text, then three sync → click cycles all succeed. This test fails on main, which reproduces the loop.
    • text inside the action's region: keeps refusing.
    • new content appearing next to a tolerated clock: refused.
    • --depends-on naming the clock: stays exact.
  • observation-evidence.test.ts:
    • contentChanges and tolerated comparison still catch a node being removed, disabled, hidden or added.
    • Explicit targets ignore the volatile set.
    • Form values and modals are never treated as page activity.
  • pnpm --filter @browser-skill/extension test (178 files, 2977 tests), tsc --noEmit, biome check and the DSH plugin tests (565) all pass.

Limitations

  • Verified with unit tests only. This hasn't been reproduced or tested in a live browser yet.
  • The agent still pays one refusal and one re-sync the first time it meets a live region on each document.
  • A widget that replaces its DOM nodes on every tick, instead of updating text in place, gets new backendNodeIds and is still treated as a structural change. Those pages still need --depends-on.
  • This doesn't change the permission_denied code used for sync-required refusals, which is tracked separately.

Without --depends-on, the pre-action evidence check compares the whole
observed page, including every node's text. A clock, countdown or live
counter outside the action's region changes that text between sync and
action, so sync -> act -> "observed content changed" could repeat forever.

When a whole-page check fails only because persistent, unprotected nodes
changed their text in place, with no human input since the sync and the
action's own region unchanged, report the change once (with before/after
text) and remember those nodes for the current documents. Later default
checks ignore further text-only changes to them. Identity, structure,
visibility, form values, modal state, target regions and explicit
--depends-on dependencies stay exact.

@iuyo5678 iuyo5678 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for addressing the shared-control retry loop. Tolerating unrelated live text should improve the experience, but I found two regressions that need to be fixed before merging: the learned tolerance can bypass protection of a later action's own region, and the initial refusal does not actually require a fresh sync. Details and reproduction steps are in the inline comments.

Validation: the existing cooperation suite passes all 145 tests. I added two controller regression tests; both pass on the base commit c0b8e57 and fail on this PR's head 075cf43, with one mocked native-input dispatch in each failing case. This validation is at unit-test level, not in a live browser.

Please add coverage for switching action regions after learning volatile text and retrying the refused action with the same context without syncing. Once these guards are corrected, the original clock scenario should still be covered to ensure the retry-loop fix is preserved.

Comment thread apps/extension/src/cooperation/controller.ts Outdated
Comment thread apps/extension/src/cooperation/controller.ts Outdated
…refusal

Address review on Tencent#497:

- The tolerant whole-page check now also requires the current action's
  own region to match exactly, so a node learned while acting elsewhere
  stays protected when a later action targets its region.
- Any evidence refusal consumes the refused context, and newly learned
  text changes stay pending until the next successful sync. Retrying with
  the refused context_id can no longer dispatch input.

Add regression tests for switching action regions after learning and
for retrying the refused context without a sync; both fail on 075cf43.
…ayouts

Follow-up review fixes for Tencent#497:

- Learned self-updating text is tolerated only for actions with a resolved
  ref or selector. A bare key press, script or scroll acts on whatever is
  focused, so it keeps the exact whole-page check; previously Enter on a
  focused form could submit after a learned price there changed.
- Learned text is also tolerated around a target whose region is a <main>
  landmark or the whole page, never in the target or its contents. Forms,
  dialogs, lists, tables, sections, articles and positioned panels stay
  exact. This fixes the refusal loop for buttons placed directly in <main>
  or on pages without landmarks, including with --depends-on, where the
  target region is checked with the same rule and dependencies stay exact.
- Clocks that mirror their time into aria-label or title are learned too.
- Reword the refusal message, sync guidance and skill docs to match, add
  a CHANGELOG entry, reset the captureVomObservation mock between tests,
  and truncate quoted text by code point instead of UTF-16 unit.

@iuyo5678 iuyo5678 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the update. I rechecked cfee648 and confirmed that a bare Enter now keeps the whole-page check exact: after the learned price changes again, the action is refused and the focused Pay button receives no click. The refused-context and explicit-target regression tests also pass.

Two issues still need to be addressed before merging:

  • The new <main> relaxation can bypass the boundary of a positioned panel inside it. A price change within the current target's panel is accepted; the preceding revision a98cc56 correctly refuses the same action.
  • The ordinary clock case still loops with real Chrome observations because InlineTextBox nodes without a backendNodeId remain in the exact comparison but cannot be learned.

I reran 164 cooperation-related unit and review regression tests, all passing, and checked that all 11 CI checks are green. The two findings below were reproduced with an isolated headless Chrome using the production VOM capture and interaction handlers. The cooperation state bridge is supplied by the test harness, so this is integration coverage rather than a full installed-extension end-to-end test. Please add browser regression coverage for both cases alongside the fixes.

Comment thread apps/extension/src/cooperation/observation-evidence.ts Outdated
Comment thread apps/extension/src/cooperation/observation-evidence.ts Outdated
… exact

Address review on Tencent#497, verified with real Chrome observations:

- Plain text clocks are learned again. Chrome adds an InlineTextBox with
  no backend node under each StaticText; such virtual text now belongs to
  its nearest backed ancestor instead of blocking learning.
- Container names are derived from descendant text, so they are never
  learned themselves. They are compared with learned text removed and stay
  exact otherwise, which keeps aria-hidden content such as a price that
  exists only in a container's name protected.
- Inside <main> or a page without regions, the target's own branch stays
  exact: the child of the first layout node that is not a single-child
  wrapper, never descending into a positioned panel. A price in the
  target's fixed panel or card can no longer change unnoticed.
- A selector target missing from the observation (such as inside
  aria-hidden content) keeps the exact whole-page check, as before this
  PR, instead of being refused after any cosmetic change.
- The refusal reports one clock once, not once per node.

Add a real-Chrome regression suite that drives the controller with
production VOM capture, target resolution and native input, and run it in
the browser input CI job. Its plain clock, positioned panel and
outside-observation cases fail on cfee648.
@iuyo5678
iuyo5678 merged commit b1caed0 into Tencent:main Oct 27, 2026
20 of 21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cooperation: self-updating page text (clocks, counters) makes every action refuse with 'observed content changed'

2 participants