Skip to content

fix(daemon): surface proven no-effect gestures to the agent - #1601

Merged
thymikee merged 3 commits into
mainfrom
fix/gesture-no-effect-1600
Aug 4, 2026
Merged

fix(daemon): surface proven no-effect gestures to the agent#1601
thymikee merged 3 commits into
mainfrom
fix/gesture-no-effect-1600

Conversation

@thymikee

@thymikee thymikee commented Aug 4, 2026

Copy link
Copy Markdown
Member

Fixes #1600.

What

The #1542 post-gesture stabilization loop already proves when a scroll/swipe/pan moved nothing: its accept-stale verdict fires only when the quiet post-gesture capture still equals the pre-gesture baseline after the 3.5s distrust cap — with scope-drift and root/keyboard-chrome subtleties already handled by classifyBaselineSurfaceEvidence. But the verdict only reached the diagnostics stream; the agent-facing response reported plain success.

This PR returns the verdict from capturePostGestureStabilizedResult alongside the capture value, and the snapshot handler appends a warning through the existing annotations.warnings channel (already rendered by every client path — zero new plumbing):

scroll up produced no visible change: the tree still matches its pre-gesture state. Either the container is already at its edge, or it ignores synthesized scrolls — a raw drag moves such lists: swipe x1 y1 x2 y2 (start inside the list).

The wording deliberately admits the at-edge ambiguity: iOS has no authoritative can-scroll signal (Android does), so "at edge" vs "container ignores synthesized scrolls" cannot be distinguished platform-side — but either way the agent's next move is informed instead of blind.

Why

AppControlBench element-18 (gpt_high): reaching "Manage sessions" in Element's settings table cost 433s / 80 tool calls, ~40 of which were scroll/fling/pan attempts that all reported success over a byte-identical tree — only a raw swipe moved the list. The daemon's stabilization loop (post-#1542) detects exactly this condition; it just never told the agent.

Red evidence

Both new tests fail on base 3696f338b / current main pre-fix:

× capturePostGestureStabilizedResult keeps polling past the normal deadline when the AX tree is stuck at the pre-gesture baseline (iOS)
  → result.gestureNoEffect is undefined (verdict never surfaced)
× formatGestureNoEffectWarning names the gesture and the raw-drag escape hatch
  → function did not exist

Live verification (seeded bench Bluesky feed, worktree build)

action ground truth response
scroll up 1 at feed top no movement (at edge) warning
scroll down --pixels 600 mid-feed first item whiskers→buddy no warning ✓
raw fling at feed bottom first item unchanged (verified) warning

Notes

The #1542 post-gesture stabilization loop already PROVES when a scroll,
swipe, or pan moved nothing: its accept-stale verdict fires only when the
quiet post-gesture capture still equals the pre-gesture baseline after the
distrust cap. But the verdict went to the diagnostics stream only — the
agent-facing response reported plain success, and the benchmark showed the
cost: element-18 burned ~40 tool calls re-issuing scrolls the daemon knew
did nothing ("Scrolled up by 1200px" fifteen times over a byte-identical
tree) before stumbling onto raw swipe.

The stabilization loop now returns the verdict alongside the capture, and
the snapshot handler appends a warning through the existing annotations
channel: it names the exact gesture, admits the at-edge ambiguity the
platform cannot resolve, and hands over the raw-drag escape hatch that
moved the stuck list in the field.

Live-verified on the seeded Bluesky feed: scroll up at top warns, scroll
down with real room moves silently, a fling at the bottom edge warns —
all three truthful.
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
JS raw 1.96 MB 1.96 MB +1.0 kB
JS gzip 627.1 kB 627.4 kB +328 B
npm tarball 748.7 kB 749.1 kB +331 B
npm unpacked 2.62 MB 2.62 MB +1.0 kB

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 27.0 ms 28.1 ms +1.1 ms
CLI --help 63.7 ms 66.9 ms +3.2 ms

Top changed chunks:

Chunk Raw diff Gzip diff
dist/src/internal/daemon.js +1.0 kB +328 B

@thymikee

thymikee commented Aug 4, 2026

Copy link
Copy Markdown
Member Author

P1: Do not present accept-stale as proof that the gesture had no visible effect. classifyBaselineSurfaceEvidence is subset-tolerant: unchanged shared chrome can produce unchanged while newly visible list cells are absent from the baseline and ignored. #1573 now has live evidence of the same terminal stale_accept after content demonstrably moved. This PR would turn that known false negative into the agent-facing claim ‘tree still matches its pre-gesture state’ and recommend a different gesture. Keep the diagnostic heuristic internal, or require stronger full-surface evidence (including unmatched discriminating entries) before emitting a no-effect warning, with a fixed-chrome + replaced-list regression test.

…review P1)

accept-stale alone is subset-tolerant by design: a successful scroll that
replaced every list cell under fixed chrome still classifies 'unchanged' on
the shared chrome alone, and #1573 has live evidence of that shape. The
agent-facing claim now additionally requires every discriminating entry of
the quiet capture to match the baseline exactly, in both directions
(haveIdenticalDiscriminatingSurfaces): any appeared or vanished real
element — including scope drift — vetoes the warning. Silence is the safe
failure mode for a message that steers the agent's next move.

Red evidence: the new fixed-chrome + replaced-list regression test fails on
the previous PR commit (gestureNoEffect wrongly present), passes with the
gate.
@thymikee

thymikee commented Aug 4, 2026

Copy link
Copy Markdown
Member Author

Addressed in 7af45ba — the P1 was correct, and the fix follows the "stronger full-surface evidence" option:

  • The claim is no longer keyed off accept-stale alone. gestureNoEffect now additionally requires haveIdenticalDiscriminatingSurfaces(baseline, quiet): every discriminating entry must match exactly, in both directions, rects within tolerance. Any appeared or vanished real element — the fixed-chrome + replaced-list shape, and also plain scope drift between baseline and capture — vetoes the warning. The subset-tolerant classifier stays untouched for the distrust loop, where a false 'unchanged' only buys extra polling.
  • Added the requested regression test: fixed tab-bar chrome + fully replaced list cells → the loop still accepts stale (staleAccepts: 1, loop semantics unchanged) but gestureNoEffect is absent. Red on the previous PR commit (the claim was wrongly present), green with the gate.
  • Corrected the overclaiming docs ("PROVABLY") to state the corroboration requirement explicitly.

Trade-off accepted: the warning becomes rarer (scope-drift no-ops now stay silent), which is the right direction for a message that steers the agent's next gesture. The live-verified cases from the PR body (top-edge scroll, bottom-edge fling on the seeded feed) still warn — identical presentations on both sides.

🤖 Addressed by Claude Code

capturePostGestureStabilizedResult crossed the complexity threshold (14
cyclomatic) after the #1601 review gate; the accept/veto construction now
lives in buildAcceptedStabilizedResult, which also keeps the corroboration
rule stated in one place.
@thymikee

thymikee commented Aug 4, 2026

Copy link
Copy Markdown
Member Author

Re-review at 7af45ba3c: the prior false no-effect finding is resolved. The agent-facing claim now requires equal discriminating-entry sets in both directions plus rect agreement, and the fixed-chrome/replaced-list regression correctly stays accept-stale internally while withholding gestureNoEffect. No further code finding.

Not ready on exact-head evidence: Fallow deterministically rejects capturePostGestureStabilizedResult at cyclomatic 14/cognitive 16/68 LOC, so extract the acceptance/result construction; iOS Smoke also timed out in the native-alert scenario and must be rerun/isolated before classifying it as infrastructure.

@thymikee

thymikee commented Aug 4, 2026

Copy link
Copy Markdown
Member Author

Re-review at 75492bf55: the complexity extraction is behavior-preserving; the full-surface corroboration conjunction and regression remain intact, and exact-head Fallow is now green. Code review is clean. Marking ready for human review while the remaining exact-head smoke/coverage/integration lanes complete.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Aug 4, 2026
@thymikee
thymikee merged commit 3a64ee7 into main Aug 4, 2026
30 of 31 checks passed
@thymikee
thymikee deleted the fix/gesture-no-effect-1600 branch August 4, 2026 19:22
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-08-04 19:23 UTC

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-human Valid work that needs human implementation, judgment, or maintainer merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

iOS: scroll/fling/pan silently no-op on some UITableViews while reporting success

1 participant