Repository navigation
fix(cooperation): stop live page text from refusing every action - #497
Conversation
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
left a comment
There was a problem hiding this comment.
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.
…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
left a comment
There was a problem hiding this comment.
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 revisiona98cc56correctly refuses the same action. - The ordinary clock case still loops with real Chrome observations because
InlineTextBoxnodes without abackendNodeIdremain 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.
… 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.
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
MutationObserverbumpsrevisionandstructureon every mutation (cooperation-capture.ts).verifyEvidencere-capture the VOM before acting.depends_onthere is no target scope, sosameObservationEvidencecompares every node on the page, includingnameandtext(observation-evidence.ts).structurechanged 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.tscontentChanges(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 andbackendNodeId) in the same order, with no form value or checked state and not modal. Otherwise it returnsundefined.sameObservationEvidencetakes an optionalvolatileset. It applies only to the whole-page check (no target): for those nodes,nameandtextare left out of the comparison. Everything else about them is still compared.controller.ts(verifyEvidence)depends_onwas given;contentChangessays the only differences are in-place text changes."10:00:00" → "10:00:01", and points to--depends-on. The nodes are remembered inTabState.volatilefor the current documents (capped at 256 nodes). The remembered set is ignored once any frame's document changes.skill/references/cooperation.mdcopies describe the new behavior, and so doesdocs/shared-browser-control.zh-CN.md.What stays exact
--depends-on/dependsOncheck, even for nodes already learned as volatile.Tests
controller.test.ts→ "self-changing page content":main, which reproduces the loop.--depends-onnaming the clock: stays exact.observation-evidence.test.ts:contentChangesand tolerated comparison still catch a node being removed, disabled, hidden or added.pnpm --filter @browser-skill/extension test(178 files, 2977 tests),tsc --noEmit,biome checkand the DSH plugin tests (565) all pass.Limitations
backendNodeIds and is still treated as a structural change. Those pages still need--depends-on.permission_deniedcode used for sync-required refusals, which is tracked separately.