Skip to content

fix(daemon): settle the first capture after any iOS open that did not read the app - #3376

Merged
thymikee merged 3 commits into
mainfrom
fix/ios-open-settle-any-unobserved-open
Oct 11, 2026
Merged

thymikee merged 3 commits into
mainfrom
fix/ios-open-settle-any-unobserved-open

Conversation

@thymikee

@thymikee thymikee commented Oct 10, 2026 •

Copy link
Copy Markdown
Member

Summary

#3356 settled the first screen read after an iOS Simulator open only when the open reported unobservable. On CI that never happens. Across 30 CI artifact bundles, 61 opens reported 54 probe-failed, 7 observable, and 0 unobservable. All 17 stale-looking taps (General tapped at y=319 instead of the settled 406) followed a probe-failed open, including the original evidence run 37815426076. So #3356 never fired where the failures happen.

The mark now fires for any verdict other than observable. That is the same condition open-policy.ts already uses for its fixed post-open delay. An open with no verdict (a physical device) still doesn't mark.

Behavior note: while a settle is pending, the direct iOS selector tap path stays off. So the first Maestro-compatible click after such an open goes through the snapshot route.

Tests: scripted fixtures whose opens report probe-failed gain the extra settle reads. The interaction-contract and press-readiness harnesses take one settling snapshot right after open, so each scenario's own runner calls stay exactly as scripted.

Docs, commands.md, and the TAP_OUTCOME_NOT_OBSERVED_GAP cell now name the wider rule.

Refs #3367, #3354.

Validation

  • Reverting the condition to unobservable makes the 2 new verdict tests fail.
  • pnpm check:affected --run passed on 770466b60 (rebased). pnpm test:unit (13146 tests), test:integration:provider (232 tests), and the interaction-contract tests also pass.
  • CI Smoke run 38065900476 with per-step logs: the open reported probe-failed, two snapshot-route reads agreed (post_gesture_snapshot_stabilized, action open), and the tap landed at (201, 406).

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-10-11 10:21 UTC

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 13 files

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread website/docs/docs/commands.md Outdated
@github-actions

github-actions Bot commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Size Report

Metric Base Current Diff
Installed (including dependencies) 5.16 MB 5.16 MB +10 B
Package (unpacked) 5.16 MB 5.16 MB +10 B
Package (download) 1.55 MB 1.55 MB +5 B

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 26.0 ms 25.2 ms -0.8 ms
CLI --help 71.3 ms 70.3 ms -1.0 ms

@thymikee

Copy link
Copy Markdown
Member Author

The widened trigger is not proven yet on the route that fails, so this needs one live run before merge. At 17d16c2, every iOS Simulator open that reports probe-failed, app-unidentified or not-eligible now makes the next capture run the quiet-window loop. It also pauses the direct selector tap path (deferred-interaction-outcome.ts#L123). The PR body says this was not checked live and defers proof to CI via #3374, which is still open. Nothing yet shows that the stale General tap (y=319 vs settled 406) is gone. Nothing shows what the first click after a probe-failed open now costs either. #3367 measured about 2.5 s for two settle reads, and 54 of 61 CI opens are probe-failed. Could you attach one iOS Settings replay smoke run on this head with #3374's per-step request logs? It should show three things. First, the open's response has timing.postOpenObservation: 'probe-failed'. Second, the next click's request log has the post-open settle phase with at least two agreeing reads, and it goes through the snapshot route, not the direct selector route. Third, the tap lands on General at the settled y (about 406). Please report the click's wall time next to that.

Not blocking, and fine to take or leave: some local Simulator opens have no verdict, because observeLaunchedApp returns undefined when there is no bridge or no appBundleId (open-policy.ts#L271-L272). open-policy.ts:164 still sleeps for those opens, but the mark skips them. So "same condition as open-policy" in the body and the "physical device" explanation in the docblock don't match for them. You could mark when the open-policy sleep fired, by having the owner report that the open returned on a fixed delay. Or you could state the carve-out in the body and docblock.

The cubic-dev-ai P2 thread on the docs wording still applies: website/docs/docs/commands.md:96 says "anything other than observable", but a Simulator open with no postOpenObservation does not mark. The docs should say a defined verdict other than observable.

CI is still pending. Smoke Tests, Repo Guards and Coverage were queued or running when I looked, with no failures yet. I could not verify the CI artifact numbers (54 of 61 probe-failed, 17 stale taps) from here. Before merge, the iOS Settings smoke run above must show a probe-failed open followed by a settled snapshot-route click on General at the settled y.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 3 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread src/daemon/deferred-interaction-outcome.ts Outdated
@thymikee

Copy link
Copy Markdown
Member Author

The code now matches the earlier review (17d16c2): the docs at website/docs/docs/commands.md:96 scope the settle to a present, non-observable postOpenObservation, as in https://github.com/callstack/agent-device/blob/7f9772f/src/daemon/deferred-interaction-outcome.ts#L123. One ask remains from that review: there is still no live iOS Settings replay on 7f9772f. The first click after a probe-failed open now adds about 2.5 s and skips the direct selector tap, so we need a trace from that exact route before we can say it removes the stale tap. Please attach the Smoke Tests run for 7f9772f with per-step request logs (https://github.com/callstack/agent-device/blob/7f9772f/.github/workflows/ios.yml#L175) that show four things: the open response has timing.postOpenObservation 'probe-failed'; the next click's log has the post-open settle phase with at least two agreeing reads and goes through the snapshot route, not the direct selector route; the tap lands on General at the settled y (about 406, not 319); and the click's wall time. If the run's open reports 'observable', it did not reach this route and cannot count. Smoke Tests is still queued on this head, not failed. That job runs 01-settings.ad, which is the route this change touches, so a failure there would count against the PR. The delta also adds --debug to that job's command line. Not blocking, take or leave: the docblock at src/daemon/deferred-interaction-outcome.ts:112 names "a URL-only open, a physical device" but omits a Simulator open that reports no verdict (no bridge or no appBundleId), so "an open that reports no verdict" would cover it without naming causes. The cubic-dev-ai thread on the docs scoping is fixed at this head and can be resolved (#3376 (comment)). The cubic-dev-ai P3 thread on gating by cause does not apply, so please resolve it: the mark is gated on typed presence and not 'observable', and open-policy.ts:164 sleeps a fixed delay for every non-observable verdict, so no settled read was proven for any of them (#3376 (comment)). I did not run the tests, so the PR body's test counts and the CI artifact figures (54 of 61 probe-failed, 17 stale taps) are unverified. Before merge, the Smoke Tests run on 7f9772f must show that settled snapshot-route click on General.

@thymikee
thymikee force-pushed the fix/ios-open-settle-any-unobserved-open branch from 7f9772f to 770466b Compare October 10, 2026 17:18
@thymikee

Copy link
Copy Markdown
Member Author

Smoke Tests on 7f9772f: run 38065900476, with per-step request logs from --debug. 01-settings.ad passed all 7 steps on attempt 1.

  1. The open reported probe-failed. open_timing has postOpenObservation: "probe-failed", and the bridge was bridge-preparation-pending.
  2. The settle ran on the snapshot route. The click's log shows two XCTest snapshot captures (each after ios_snapshot_route_fallback, because the bridge was still preparing). Then post_gesture_snapshot_stabilized {"action":"open","attempts":2,"durationMs":4402}. Then a separate runner tap. No direct-selector request was sent.
  3. The tap landed on the settled point. runner.log shows SYNTHESIZED_DISPATCH kind=tap point=(201.0,406.0), which is General's settled point, not 319.
  4. Wall time. The click took 11.93 s against 2.64 s in ci(ios): keep every step's request log in the Settings replay smoke #3374's run on main.
    • About 1.3 s of that is the settle's second read. The first read took 3.1 s and includes a 1.5 s pgrep (0.11 s on main).
    • About 6.4 s passed before the first read with no events logged. This run's host was slow across the board: from request start to first event, open took +2.9 s, appstate +1.4 s and wait +2.7 s, against 0.1–0.3 s in ci(ios): keep every step's request log in the Settings replay smoke #3374's run.
    • So I attribute that 6.4 s to host load, not to the settle. The settle loop's 4.4 s runs from its first read, and nothing in it waits before that read.

Caveat: both reads agreed, so the layout was already at rest here. This run proves the route, not that a 319 → 406 shift gets corrected. That case happens in about 8.5% of runs. With --debug in place, CI's attempt-1 failure rate and any stale tap's logs will show it.

New head 770466b60:

@thymikee

Copy link
Copy Markdown
Member Author

I found no problems in the code at 770466b. The earlier findings from 7f9772f are fixed. The docs in commands.md now say the settle runs only when postOpenObservation is present and not observable, and that an open with no value reads once. This matches the guard in deferred-interaction-outcome.ts. The docblock there now says a non-observable result means the open never read the launched app, so the two earlier cubic-dev-ai threads (#3376 (comment) and #3376 (comment)) no longer apply and can be resolved.

All 21 checks pass. The Smoke job runs 01-settings.ad, the route this change touches. No conflicts.

I read the diff but did not run the unit or integration tests. That the new tests fail on the old condition comes from reading the code, not from running them. I did not open Smoke run 38065900476. I rely on your quoted log lines for the probe failure, the two snapshot reads, post_gesture_snapshot_stabilized and tap=(201,406). That run shows the route is reached. Both reads already agreed, so it does not show a 319 to 406 shift being corrected. The click took 11.93 s against 2.64 s on main. You attribute about 6.4 s to host load, and I could not check that split. Is the remaining cost, about 1.3 s for the second read, acceptable?

Nothing else needs to change before merge. Once you are happy with that trace and cost, this is ready to merge.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Oct 10, 2026
@thymikee

Copy link
Copy Markdown
Member Author

On the cost question: yes, I think about 1.3 s for the second read is acceptable.

  • It's paid once. Only the first capture after an open that didn't read the app pays it, and only on an iOS Simulator. Later clicks, warm clicks, and opens that report observable don't.
  • It costs less than the retries it targets. In the CI evidence, a failed 01-settings.ad attempt 1 took a median of 32.8 s before its retry started (range 23.2–55.8 s, n=24). At the 8.5% attempt-1 failure rate, that's about 2.8 s wasted per run on average. That is roughly twice the settle's cost, before counting runs that fail all 3 attempts (main run 37419258095).
  • It's bounded. The loop always takes two reads, and the 1.5 s polling budget caps any further polling. A screen that keeps moving gets postGestureOutcome: unsettled and a warning, and the click still proceeds.

The real test is CI's attempt-1 failure rate on 01-settings.ad after merge, against the 8.5% baseline. With --debug in place, any stale tap that remains will keep its pre-tap reads in the artifacts, and #3367 tracks that. Both cubic threads were already resolved.

thymikee and others added 3 commits October 11, 2026 12:00
… read the app (#3367)

CI evidence across 30 runs of 01-settings.ad: 61 iOS opens reported 54
probe-failed, 7 observable, 0 unobservable, so the unobservable-only mark
from #3356 never fired. Every stale-tap failure followed a probe-failed
open. Mark the next capture for any defined verdict other than observable,
mirroring the open-policy fixed-delay condition; an open with no verdict
(physical device) still does not mark.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… not its cause

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@thymikee
thymikee force-pushed the fix/ios-open-settle-any-unobserved-open branch from 770466b to 6f93248 Compare October 11, 2026 10:02
@thymikee
thymikee merged commit 486e1d6 into main Oct 11, 2026
21 checks passed
@thymikee
thymikee deleted the fix/ios-open-settle-any-unobserved-open branch October 11, 2026 10:21
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.

1 participant