Skip to content

fix(daemon): consume the post-gesture record when its capture aborts - #2983

Open
okwasniewski wants to merge 1 commit into
callstack:mainfrom
okwasniewski:oskar/wait-stable-consumes-pending-stabilization
Open

okwasniewski wants to merge 1 commit into
callstack:mainfrom
okwasniewski:oskar/wait-stable-consumes-pending-stabilization

Conversation

@okwasniewski

@okwasniewski okwasniewski commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Refs #2885.

wait stable bounds each capture with runWithinWaitDeadline, which aborts the capture's signal with a TimeoutError at the wait's deadline. After a scroll, that capture routes through capturePostGestureStabilizedResult, which runs the post-gesture stability loop (up to the 3.5 s distrust cap, each poll a full simulator capture) inside what the stable loop counts as one capture. When the wait deadline aborted the loop mid-poll, the throw skipped clearPostGestureStabilization, so the pending record stayed armed and every later capture on the session paid the same loop again: the next wait stable also stalled with captures: 0, and the one after it, which is the 15-of-16 pattern in the issue.

The owner now clears the record in finally: entering the loop consumes it, on settle, timeout and abort alike. No other behavior changes; the stalled timeout still reports captureStalled: true and captures.

Touched: 1 source file, 1 test file.

Validation

Tested commit 8bc857c52.

  • pnpm check:affected --run: passed.
  • New test in post-gesture-stabilization.test.ts: a second capture that throws a TimeoutError mid-loop leaves session.postGestureStabilization undefined. Removing the finally fails it.
  • Live, iPhone 17 Pro / iOS 26.2 simulator, CLI at this head, open com.apple.Preferences then three rounds of scroll down 0.75 + wait stable: every wait settled with captures: 2 in 1.5 s to 4.2 s. I could not force the zero-capture stall on this host; the unit test pins the mechanism.
  • Not addressed here: the stable loop still charges one nested stabilization loop to its first capture, so a slow enough host can still stall the first wait stable after a gesture. That is a second seam (the stable loop is itself a settle observer and could skip the nested loop); this PR stops the stall from repeating.

Review in cubic

A wait stable poll bounds each capture with a deadline abort. When that
abort landed inside the post-gesture stabilization loop the pending record
stayed armed, so every later capture on the session paid the whole loop
again and wait stable kept timing out with zero captures. The record is now
cleared once the loop has been entered, on settle, timeout and abort alike.
Refs callstack#2885.
Copilot AI lite review requested due to automatic review settings September 25, 2026 15:46

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

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

Copy link
Copy Markdown

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 2 files

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

Fix all with cubic | Re-trigger cubic

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

Copy link
Copy Markdown
Contributor Author

[claude-fable-5-1] responding on behalf of Oskar

CI note: Smoke Tests failed at launch clear-state fixture through stored URL in the fixture E2E, before any settle step ran; this PR touches only the post-gesture record lifecycle. The ios.yml smoke lane failed on 8 of the last 12 PR runs and on main at 78c0ca4 today (#2491 / #2940 territory). I have no rerun rights; a maintainer rerun should clear it.

@thymikee

Copy link
Copy Markdown
Member

This looks unproven at 8bc857c. The only run in the description is scroll + wait stable where each wait settled with captures: 2, which is the normal return path through runPostGestureStabilizationLoop that main already clears the record on — see https://github.com/callstack/agent-device/blob/8bc857c/src/daemon/deferred-interaction-outcome.ts#L356. Nothing shows a wait-deadline abort thrown out of the loop followed by a later capture in the same session, and the tests don't reach it either: the new case calls capturePostGestureStabilizedResult directly with a hand-built DOMException instead of going through wait stable. Is the iOS AX-bridge or runner capture confirmed to throw when its signal aborts, rather than just returning? If it returns instead of throwing, the loop runs to its cap and clears the record on main too, and #2885 would have a different cause.

To validate, open the fixture Automation lab (or Settings) on the iOS simulator at this head, run scroll down 0.75, then wait stable with a timeout shorter than the stabilization loop (roughly 300-500 ms) so the deadline aborts mid-loop, then run wait stable again with a normal timeout. The --debug/--json output or request log needs to show the first wait failing with a stable-timeout that reports captureStalled and no post_gesture_snapshot_stabilized entry, and the second wait settling with captures >= 2 and no post_gesture_snapshot_* diagnostic, meaning the record was consumed. The same sequence on main should show the second wait re-entering the loop, for contrast.

The failing CI step is replay step 1 (launchApp), where the simulator openurl of agent-device-test-app:///automation?event=cold.start failed with COMMAND_FAILED before any capture; the diff only changes when capturePostGestureStabilizedResult clears the record, which runs inside snapshot captures, so this failure looks unrelated to the change. I did not check the author's claim that this lane also fails on main.

Not blocking: the new test could exercise the real wait-stable path (runWithinWaitDeadline -> captureSelectorSnapshot -> captureSnapshot -> resolveDeferredInteractionOutcome) instead of calling capturePostGestureStabilizedResult directly with a stub exception, and the inline comment at deferred-interaction-outcome.ts:328-330 could state the consume-on-entry rule instead of narrating the issue history, but either can be taken or left.

The next thing needed before merge is that live simulator evidence: a run showing a wait-deadline abort mid-loop consumes the record, and the following wait stable settles with captures >= 2 without paying for another post-gesture loop.

This branch has not been deployed

No deployments
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.

3 participants