Skip to content

iOS selector click can still tap a stale point after layout shifts the post-open settle does not cover #3367

Description

@thymikee

Purpose

#3356 settles the next screen read after an iOS open that returns timing.postOpenObservation: "unobservable". Three cases from #3354 are still open. In each one a selector click can tap a stale point and report success:

  • The layout moves between the click's own pre-tap capture and its tap, at any time and not only after an open.
  • The layout moves after an observable open. observable means the tree was readable, not that it was at rest.
  • The post-open read never settles. The click gets postGestureOutcome: unsettled and a warning, then taps anyway.

Evidence

The evidence is the same as #3354: main run 37815426076, where General was at y=319 and the settled list had it at y=406. Nobody has reproduced the shift locally. In 5 live runs of 01-settings.ad on an iOS 27.0 simulator, every read settled before the tap. Collect evidence that these cases happen before choosing a design.

Required behavior

  • When the target's position can't be confirmed at dispatch, the click fails with a typed reason instead of tapping the old point. Declare the reason and its details shape in the error taxonomy.
  • Judging that "the target moved" requires the target's identity across reads. The whole-surface signature comparison (src/daemon/interaction-surface-signature.ts) can't establish it, and it collapses duplicate identities.
  • Do not add a capture or runner query to warm clicks whose layout is already stable.
  • Give any bounded wait a wall-clock deadline, not only a polling budget. Capture cancellation must propagate: abort the capture and wait for it to stop, rather than racing a timer. Today the post-open loop's 1.5 s is a polling budget, and its two reads took 2.55–2.57 s live.
  • A screen that animates constantly must not make a stable target unclickable.
  • Update TAP_OUTCOME_NOT_OBSERVED_GAP in packages/contracts/src/interaction-guarantees.ts.

Completion

  • A fixture where the target moves between the click's capture and its tap. The click either taps the new location or fails with the typed reason.
  • Warm-click p50 on 01-settings.ad, measured before and after the change, does not regress. It was about 1.3 s on the iOS 27.0 simulator.

Rejected so far

  • A runner-side frame check on every click. It would take an estimated 550–800 lines and add a query to every warm click.
  • Settling after every iOS open. The first click would cost about 1.8 s more, with no evidence that observable opens shift, and it still wouldn't cover shifts at other times.
  • Failing whenever the settle times out. A stable target next to an unrelated animation would be refused.

Related: #3354, #3356, #3335.

Activity

  1. thymikee commented on Oct 10, 2026

    @thymikee
    MemberAuthor

    CI evidence (2026-10-03 to 10-10)

    Sources. I took the ios.yml "Smoke Tests" job logs from 473 runs on main and PRs. 437 of those ran 01-settings.ad on an iOS 26.2 simulator with --retries 2. I also read 30 downloaded ios-artifacts bundles.

    How often attempt 1 fails. 37 of 437 runs (8.5%) failed attempt 1 of 01-settings.ad. Every failure was at step 4, the wait after click General.

    Stale-looking taps. 17 failed attempts in 14 runs tapped General at y=319, while a later attempt in the same run tapped y=406 and passed. In those 17:

    • the click's own snapshot took a median of 6.4 s (range 2.8–17.2 s)
    • the time from that snapshot finishing to the tap was a median of 2.8 s (range 0.76–6.2 s)

    In first-try passes, the snapshot takes 0.6–1.8 s and the gap is a median of 0.7 s. So stale taps go with a slow capture and a long wait before the tap on a loaded runner.

    Not stale-point failures. 11 failed attempts tapped the same point as the attempt that passed. Those are capture or wait failures (#2491/#3328), not stale points.

    Open verdicts. 61 opens: 54 probe-failed, 7 observable, and 0 unobservable. All 7 observable opens were on retries, and all of them passed.

    After #3356. It has run on 4 CI runs so far. post_gesture_snapshot_stabilized action=open never appears in them, which is expected: no CI open reports unobservable.

    What this shows for each case

    • CI's real case: a probe-failed open. It isn't one of the three cases this issue lists, and it accounts for all 17 stale-looking taps. open already uses a fixed delay for every verdict other than observable, so the next PR makes the post-open settle use the same rule.
    • (a) A shift between the click's capture and its tap. Plausible but not isolated. The artifacts don't keep snapshot contents or the click's request log. They can't tell "the layout moved after the read" apart from "the read caught a layout still moving".
    • (b) A shift after an observable open. No evidence: 7 of 7 passed. The sample is small and comes only from retries.
    • (c) The post-open settle timed out, then the click tapped a stale point. No evidence: there were no unobservable opens, and the artifacts don't record whether a click's settle timed out.

    Next steps

    1. Land the change that settles after any open that didn't read the app.
    2. Watch the 01-settings.ad attempt-1 failure rate on CI after it lands, against the 8.5% baseline.
    3. Hold the typed failure and the wall-clock deadline until (a) or (c) has evidence of its own. Getting that evidence needs the click's request log or its pre-tap frame kept in CI artifacts.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions