Skip to content

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

Merged
thymikee merged 3 commits into
callstack:mainfrom
okwasniewski:oskar/wait-stable-consumes-pending-stabilization
Sep 27, 2026
Merged

thymikee merged 3 commits 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 206d61c3a (654bb7f plus a comment rewording).

  • 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.5 simulator, Settings, --json --debug: scroll down 0.75, then wait stable 100 400 twice (deadline shorter than the stabilization loop), then wait stable twice. First short wait: timeout, captureStalled: true, captures: 0. Second short wait: captures: 1, so the record was consumed. Both normal waits settled with captures: 3 and no post_gesture_* diagnostic. Same recipe on main 3660c7f: second short wait still captures: 0 and the first normal wait logs post_gesture_snapshot_stabilized, meaning it re-entered the loop. Full tables in the PR comments.
  • 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

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.

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.
@okwasniewski
okwasniewski force-pushed the oskar/wait-stable-consumes-pending-stabilization branch from 8bc857c to 654bb7f Compare September 26, 2026 09:20
Copilot AI review requested due to automatic review settings September 26, 2026 09:20

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.

@thymikee

Copy link
Copy Markdown
Member

Following up on the earlier review (654bb7f), this is still not ready to merge.

The abort path that this fix targets still has no live evidence. The prior review asked for a specific run: scroll, then wait stable --timeout set to 300-500ms (shorter than the stabilization loop) to force a deadline abort mid-loop, then a second wait stable with a normal timeout, checking the first fails with captureStalled and the second settles with captures>=2 without re-entering the loop. The PR body instead ran three rounds of scroll + wait stable with default timeouts, which only exercises the already-working settle path (captures:2 each time) and never reaches the abort branch this diff changes. The author notes they could not force a zero-capture stall on their host, but that recipe does not depend on host speed — the CLI controls the deadline directly — and there's no report of trying it. The rule this PR relies on is that the leaf iOS capture actually throws (not silently returns) when its AbortSignal fires; right now the only evidence for that is the unit test's hand-built DOMException at https://github.com/callstack/agent-device/blob/654bb7f/src/daemon/deferred-interaction-outcome.ts#L330, not a real runner. If the real capture returns instead of throwing on abort, this fix doesn't address the underlying issue on device at all.

Not blocking: the finally at https://github.com/callstack/agent-device/blob/654bb7f/src/daemon/deferred-interaction-outcome.ts#L356 clears postGestureStabilization on any throw out of runPostGestureStabilityLoop, not just the timeout, so a transient capture error now also discards the pending record instead of retrying — worth a second look but fine to leave as-is given the typed-reasons constraint at this seam.

CI shows all checks passing (or unreported) at 654bb7f; the prior run's Smoke Tests failure was in an unrelated fixture step before any capture ran, so it doesn't overlap this diff, though I haven't independently pulled current logs. No conflicts known. The next thing needed before merge is a live iPhone or Simulator run using a short wait stable --timeout to force a genuine mid-loop deadline abort after a scroll, then a normal-timeout wait stable, showing the record is consumed with no re-entry and captures>=2 on the second wait.

Copilot AI review requested due to automatic review settings September 26, 2026 16:28

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.

@okwasniewski

Copy link
Copy Markdown
Contributor Author

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

Live run at 206d61c, iPhone 17 Pro simulator (iOS 26.5), Settings app, --json --debug. Recipe: open com.apple.Preferences, scroll down 0.75, wait stable 100 400 twice (quiet 100 ms, deadline 400 ms, shorter than the stabilization loop), then wait stable twice with the default timeout.

This branch:

step result wall request diagnostics
wait stable 100 400 #1 timeout, captureStalled: true, captures: 0 606 ms 2 snapshot_capture, no post_gesture_*
wait stable 100 400 #2 timeout, captureStalled: true, captures: 1 643 ms 2 snapshot_capture, no post_gesture_*
wait stable #1 settled, captures: 3 1167 ms 3 snapshot_capture, no post_gesture_*
wait stable #2 settled, captures: 3 1071 ms 3 snapshot_capture, no post_gesture_*

The first deadline aborts the loop mid-capture and consumes the record. The second short wait already counts a plain capture, and neither later wait pays the loop.

main at 3660c7f, same recipe on a second iPhone 17 Pro simulator:

step result wall request diagnostics
wait stable 100 400 #1 timeout, captureStalled: true, captures: 0 760 ms no post_gesture_*
wait stable 100 400 #2 timeout, captureStalled: true, captures: 0 667 ms no post_gesture_*
wait stable #1 settled, captures: 3 1397 ms 1 post_gesture_snapshot_stabilized
wait stable #2 settled, captures: 3 1060 ms none

On main the record survives both aborts (second short wait still captures: 0) and the first normal wait re-enters the loop, which is the post_gesture_snapshot_stabilized entry. So the leaf capture does throw on abort; a silent return would have run the loop to its cap and cleared the record on main too.

Also took the comment rewording at deferred-interaction-outcome.ts:328. Left the unit test as is: the daemon owner is the seam that changed, and the live run above covers the wait-stable path end to end.

@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 1 file (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Fix all with cubic | Re-trigger cubic

Comment thread src/daemon/deferred-interaction-outcome.ts Outdated
Copilot AI review requested due to automatic review settings September 26, 2026 16:44

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.

@okwasniewski

Copy link
Copy Markdown
Contributor Author

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

CI note on c23d1b7: one of the four iOS Smoke lanes failed at step 7, wait text Agent Device Tester 10000, with wait_readiness_exhausted / readinessPhase: runner-start right after a daemon_startup for that request, so the runner was being relaunched mid-suite. The runner log before it shows AGENT_DEVICE_RUNNER_SNAPSHOT_BACKEND_FAILED backend=tree error=the XCTest tree capture exceeded its 8s time slice and two Could not match active AX application failures. The other three smoke lanes passed on this head, and all four passed on 206d61c, which differs from this head by one comment. No rerun rights on my side; a maintainer rerun should clear it.

@thymikee

Copy link
Copy Markdown
Member

Reviewed c23d1b7: no code findings, and this looks ready for human review.

The change since 654bb7f only rewrites a comment. The live run you posted (#2983 (comment)) follows the recipe the earlier review asked for: on this branch the second short wait stable 100 400 reports captures: 1, so the record was consumed, while on main it stays at captures: 0 and the next normal wait re-enters the loop. That run was at 206d61c, which differs from this head only by a comment, so it still covers the change. I read the posted table; I did not repeat the run.

The one failing Smoke lane is likely unrelated. It fails at https://github.com/callstack/agent-device/blob/c23d1b7/test/integration/ios-simulator-e2e/live-automation-scenario.ts#L27 in wait text during runner-start readiness, before any gesture. The post-gesture loop only runs after a gesture action sets the record (https://github.com/callstack/agent-device/blob/c23d1b7/src/daemon/deferred-interaction-outcome.ts#L100-L104). The other three Smoke lanes passed, and all four passed on 206d61c, so a rerun should clear it.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Sep 26, 2026
@thymikee
thymikee merged commit de00955 into callstack:main Sep 27, 2026
13 of 14 checks passed
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.

3 participants