Skip to content

fix(ios): press Open again when the runner watchdog abandons the launch-URL accept - #4017

Open
thymikee wants to merge 12 commits into
mainfrom
maint/3861-reanswer-open-after-runner-timeout
Open

thymikee wants to merge 12 commits into
mainfrom
maint/3861-reanswer-open-after-runner-timeout

Conversation

@thymikee

@thymikee thymikee commented Oct 28, 2026 •

Copy link
Copy Markdown
Member

Summary

A freshly started runner can fail the "Open in" accept with MAIN_THREAD_TIMEOUT (typed details.runnerErrorCode) while the prompt stays up. open then failed with launch_confirmation_unanswered. Now it presses Open again, but only when the request's remaining time covers a whole press and its read: 2 × (1 s preflight + 10 s watchdog).

  • The deadline gates only that re-press. The first press and every read keep main's bounds.
  • Deadline: the enforced daemon deadline (batch, replay), otherwise the admitting transport's client-envelope end less the 30 s result margin. Nested attempts inherit their parent's; under an unbounded test nothing is re-pressed.
  • Shared runner code: only the moved preflight constant and the typed timeout predicate.

Follow-ups: #4023 (runner-wide deadline), #4024 (bounded test-attempt deadline).

Fixes #3861

Validation

  • Regression tests written first; each failed on its prior head. A first press with 5 s left is refused on 101a8cef38 and sent now.
  • index-launch-confirmation.test.ts: real alert legs and runner client; the verify read passes two RUNNER_BUSY resends, then the second press lands.
  • Live, iPhone 17e (iOS 27.0), fresh schemes: one plain attempt accepted on the first press. Three attempts paused this simulator's SpringBoard 12 s after the accept: watchdog abandoned, RUNNER_BUSY resends, drain, re-press (remainingMs 36822/36533/33523), second press ok=1, launchConfirmation: "accepted", no alert left. Logs are in the review reply. iOS 17.5 not measured.
  • pnpm check:affected --run --head e3fe452aed: all runnable checks passed (exactCommit).

🤖 Generated with Claude Code

@vercel

vercel Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
agent-device-website Ignored Ignored Preview Oct 28, 2026 3:22pm UTC

Request Review

@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 6 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 28, 2026 •

Copy link
Copy Markdown
Contributor

Size Report

Metric Base Current Diff
Installed (including dependencies) 5.56 MB 5.56 MB +1.3 kB
Package (unpacked) 5.56 MB 5.56 MB +1.3 kB
Package (download) 1.69 MB 1.69 MB +515 B

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 41.2 ms 38.9 ms -2.3 ms
CLI --help 79.5 ms 81.5 ms +1.9 ms

@github-actions

github-actions Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://callstack.github.io/agent-device/pr-preview/pr-4017/

Built to branch gh-pages at 2026-10-28 15:29 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@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 17 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 packages/platform-apple/src/alert.ts Outdated
Comment thread packages/platform-apple/src/launch-confirmation.test.ts Outdated
Comment thread packages/host-kit/src/internal/request-cancel.ts Outdated

@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 12 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 packages/platform-apple/src/__tests__/alert.test.ts Outdated
Comment thread packages/daemon/src/server/__tests__/http-server-rpc-lifetime.test.ts Outdated
Comment thread packages/host-kit/src/internal/request-cancel.ts Outdated

@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 12 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/commands/management/app.ts Outdated
Comment thread packages/platform-apple/src/runner/__tests__/runner-exchange-deadline.test.ts Outdated

@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 2 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 packages/contracts/src/interactor-types.ts Outdated
@thymikee

Copy link
Copy Markdown
Member Author

Changes needed: the new re-press route has no live run yet, and the deadline now also gates the first press. CI is green (21 checks, none failing). I did not run tests locally, so I have not confirmed that each regression test fails on the prior head. I read the diff for the main re-press test only. This review is of 101a8ce.

The changed route is: the accept fails with MAIN_THREAD_TIMEOUT, the verify read sees the same confirmation, the confirmation is pressed again, and the second press is accepted (launch-confirmation.ts:151). No live run reaches it. The only live run (iOS 27, 0de6310) accepted on the first press, and I have no log for it. The earlier pass at 097bead asked for this --debug run, and the head does not include it. Nothing shows that the verify read gets through RUNNER_BUSY (the runner client resends read-only commands up to 8 times) in time to leave the 22 s for another press, or that the second press lands. This is the failure #3861 reports. Please run open <bundle> --launch-url <scheme>://... --relaunch --foreground --json --debug on a fresh iOS 17.5 simulator, as in #3861. Repeat until one run hits the watchdog (the issue saw 5 in 22), or induce one with host load. Post the request log and the runner log for that run. They must show, in order: AGENT_DEVICE_RUNNER_MAIN_THREAD_WORK_ABANDONED operation=alert for the first accept, AGENT_DEVICE_RUNNER_ABANDONED_WORK_DRAINED, the ios_launch_confirmation_reaccept diagnostic with remainingMs >= 22000, a second alert accept with ok=1, and a final result with launchConfirmation 'accepted' and no alert left (alert get returns not found).

Is the deadline plumbing needed beyond the re-press gate? The gate at launch-confirmation.ts:150-151 already checks for a whole press plus its read (22 s) before it presses again, and the 30 s envelope margin absorbs a preflight overrun. Could you keep the result-deadline plumbing (timeout-policy, the two admission sites, request-cancel, the contract field) and use it only in that gate? Then RunnerCommand.deadline, bindRunnerCommandDeadline, RUNNER_COMMAND_DEADLINE_SPENT_REASON, the port's beforeDeadline wrapper, the alert.ts deadline spread and the URL-owner lookup cap could go. That removes the runner-contract, runner-exchange, alert.ts and interactor-types changes and their tests (about half the production lines). It also removes the behavior change for the first press described next, and #4023 is filed for the per-phase runner bounds.

The first-press change: the deadline in alert.ts:61 now governs the first press too. For accept, minTimeoutMs is the full 10 s watchdog, and bindRunnerCommandDeadline (runner-exchange.ts:287) refuses the command unsent when less than 10 s of the 60 s open budget is left. On main the first press was always sent. The prompt can sit behind openurl (up to 20 s), failed target discovery (15 s), cold runner startup (about 10 s or more on iOS 17.5), the 3 s presentation wait and the URL-owner lookup (up to 10 s). If those spend more than about 49 s, the open now fails with launch_confirmation_unanswered without pressing, although the client envelope still has 30 s or more. This timeline comes from the declared step bounds and the ~10 s cold start in #3861, not from a measurement. The rule should be that the deadline decides only whether to press again, applied at the one re-press gate. The first press and its verify read keep main's bounds. If the plumbing stays, give the first press a deadline at the client envelope (the result deadline plus REQUEST_TIMEOUT_BUDGET_MARGIN_MS), and add a test that a first press with 5 s left is still sent.

Not blocking, take or leave: launch-confirmation.test.ts:790 hand-writes the deadline-spent AppError instead of throwing runnerCommandDeadlineSpentError() from runner-contract.ts, so a changed reason string would keep the test green while production misclassifies the refusal; and in commands.md:114 the re-press and timeout-cap sentences sit before 'Answer it with alert accept or alert dismiss', so 'it' reads as the capped lookup timeout and not the prompt, which moving them after that sentence or into their own bullet would fix.

Before merge, please post the live iOS 17.5 run where the first accept hits the watchdog and the second press returns 'accepted', and decide whether the deadline should only gate the re-press.

thymikee and others added 12 commits October 28, 2026 16:21
…ch-URL accept

On a freshly started runner, the launch confirmation's Open press can time out
on the runner's main-thread watchdog (MAIN_THREAD_TIMEOUT) while the "Open in"
prompt stays up, and open then failed with launch_confirmation_unanswered with
budget to spare. The answer now presses Open again while the same confirmation
is still up, within a 25-second accept budget that holds a whole press. Any
other accept failure, or a prompt that is gone, ends the answer as before.

Fixes #3861

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… a later alert

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s deadline

The re-press after a watchdog-abandoned accept used a fresh 25 s budget from the
first press, which ignored the time open had already spent, and reserved only
the press watchdog. The daemon now hands the open its request deadline: the
enforced daemon deadline when the request has one, otherwise the transport
admission time plus what the client envelope leaves the daemon. Another press
starts only while that deadline covers the press and its verification read,
each with a readiness preflight, and every runner alert command and the URL
owner lookup are capped at the time left. A request without a deadline is not
re-pressed.

Refs #3861

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…p alert failures typed

A request's result deadline is now recorded by the transport that admitted it,
from the top-level command's client envelope, and an attempt registered under a
parent inherits the parent's. A replay test attempt under an unbounded `test`
request therefore has no deadline, instead of one counted from its own
registration with the nested open's budget.

An accept whose deadline already passed floored its retry window to 1 ms and
could skip the loop and throw undefined; the first attempt now always runs.

Refs #3861

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…me the deadline tests exactly

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… hold

The deadline only shrank the alert payload's timeout before the readiness
preflight, and a spent deadline still dispatched with a 1 ms floor. An alert
runner command now carries a host-side deadline that the exchange turns into
the runner timeout right before the payload, after the preflight; a press
left less than its whole watchdog, or any command left no time, is refused
unsent with the typed `runner_command_deadline_spent`. Once the deadline has
passed, the launch confirmation sends nothing, and a press refused unsent
leaves the confirmation unanswered. Runner startup and transport overhead stay
outside this bound.

Refs #3861

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…closure

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ine doc

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…line

Review of 101a8ce: the per-command deadline also governed the first press, so a
first press with less than 10 s of the open budget left was refused unsent where
main pressed it. The request deadline now decides only whether to press again,
at the one re-press gate (a whole press and its read, 22 s); the first press and
every read keep their own runner bounds. This removes the opt-in runner command
deadline, its exchange binding and typed refusal, the alert-leg deadline option
and the URL-owner lookup cap.

A new suite drives the answer through the real alert legs and runner client
against a fake runner: an abandoned press is verified through two RUNNER_BUSY
resends and pressed again with 49.4 s of a 60 s open left, and a first press
with 5 s left is still sent with its full watchdog (refused on 101a8ce).

Refs #3861

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the runner package index

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@thymikee
thymikee force-pushed the maint/3861-reanswer-open-after-runner-timeout branch from 46dea77 to e3fe452 Compare October 28, 2026 15:22
@thymikee

Copy link
Copy Markdown
Member Author

Both points are addressed at e3fe452 (rebased on main after #3938; the open help keeps #3938's --launch-console text).

1. Live re-press route. iOS 17.5 is not installed on this host, and installing runtimes is out of scope, so I induced the watchdog on iPhone 17e (iOS 27.0) instead. The iOS 27 runtime has no shell to spawn a CPU loop inside the simulator. So for each attempt, a watcher SIGSTOPs only this simulator's SpringBoard for 12 s, starting the moment the runner accepts the alert accept command, then SIGCONTs it. Each attempt uses a fresh URL scheme (the app is re-signed ad hoc with a new CFBundleURLSchemes entry) and a fresh runner.

Attempt Head Induced Result reaccept remainingMs
1 db07fd0 no accepted on the first press n/a
2 db07fd0 yes accepted after a second press 36822
3 db07fd0 yes accepted after a second press 36533
4 e3fe452 yes accepted after a second press 33523

Attempt 4, open com.callstack.agentdevicelab --launch-url adt3861r4://automation --platform ios --udid F25C7515-… --relaunch --foreground --json --debug, runner log in order (ids shortened):

16:32:33.893 COMMAND_ACCEPTED  command=alert id=8d96bd76            # presence read
16:32:34.426 COMMAND_COMPLETED command=alert id=8d96bd76 ok=1
16:32:35.744 COMMAND_ACCEPTED  command=alert id=5554df54            # first accept
16:32:45.751 MAIN_THREAD_WORK_ABANDONED operation=command_execution slice=10.0
16:32:45.763 COMMAND_FAILED    command=alert id=5554df54 error=… Code=3 "main thread execution timed out"
16:32:45.784 RUNNER_BUSY       command=alert id=d4c7b3c9 abandonedForSeconds=0.0   # verify read
16:32:45.999 RUNNER_BUSY       command=alert id=d4c7b3c9 abandonedForSeconds=0.2
16:32:46.414 RUNNER_BUSY       command=alert id=d4c7b3c9 abandonedForSeconds=0.7
16:32:47.225 RUNNER_BUSY       command=alert id=d4c7b3c9 abandonedForSeconds=1.5
16:32:48.155 MAIN_THREAD_WORK_DRAINED operation=command_execution
16:32:48.680 COMMAND_COMPLETED command=alert id=d4c7b3c9 ok=1      # same confirmation still up
16:32:48.692 COMMAND_ACCEPTED  command=alert id=be395d1b            # second accept
16:32:50.246 COMMAND_COMPLETED command=alert id=be395d1b ok=1

The request log has {"ts":"2026-10-28T15:32:48.681Z","phase":"ios_launch_confirmation_reaccept","data":{"reason":"runner-main-thread-timeout","remainingMs":33523}}. The verify read got through four RUNNER_BUSY resends in 2.9 s, and the open still had 33.5 s, over the 22 s a whole press and its read cost.

After the second press the app was not yet observable. The existing settle then handed the URL over again (xcrun with the 20 s openurl bound at 15:32:50.875); seven presence reads in the 3 s window found no prompt, and the app became observable. The result was {"success":true,"launchConfirmation":"accepted"} (34.986 s), and the follow-up alert get answered alert not found. Attempts 2 and 3 show the same sequence without the second URL hand-over.

The test that pins the route is the new packages/platform-apple/src/runner/__tests__/index-launch-confirmation.test.ts. It runs the answer through the real Apple alert legs and the real runner client (command ids, preflights, the read-only RUNNER_BUSY resend, transport) against the fake runner. It shows an abandoned press verified through two RUNNER_BUSY resends and pressed again with 49.4 s of a 60 s open left.

2. First press. Taken as you proposed. The request deadline now decides only whether to press again, at the one re-press gate (remainingMs >= 22000). The first press and every read keep main's bounds. Removed: RunnerCommand.deadline, bindRunnerCommandDeadline, RUNNER_COMMAND_DEADLINE_SPENT_REASON, the port's beforeDeadline, the alert.ts deadline spread, AlertInteractorOptions.deadlineAtMs and the URL-owner lookup cap, with their tests. The shared runner diff is now only the moved 1 s preflight constant (the gate's cost uses it) and the typed isRunnerMainThreadTimeoutError. The result-deadline plumbing stays, feeding only the gate. New test: a first press with 5 s of the open left is sent with its full 10 s watchdog and accepted; on 101a8ce it was refused unsent (alert-still-present).

The take-or-leave notes are taken. The hand-written deadline-spent error test went with the refusal. In commands.md the re-press sentence is its own bullet after "Answer it with alert accept or alert dismiss", and it says the first tap is always sent.

Gate: pnpm check:affected --run --head e3fe452aed passed every runnable check (exact commit). The iOS 17.5 path itself is still unmeasured here.

@thymikee

Copy link
Copy Markdown
Member Author

The code looks good at e3fe452. The earlier evidence request from the 101a8ce review is now met, and CI is green with 21 checks and none failing. There are no conflicts.

Not blocking, take or leave: the added parenthetical in the answerHeldLaunch doc block (open-policy.ts:201) runs a comment line well past the file's wrap width, so please rewrap those lines.

Of the open inline threads, these are fixed at this commit and can be resolved: the accept-budget wording in commands.md (#4017 (comment)), the retry loop in alert.ts (#4017 (comment)), the runner-mock reset in launch-confirmation.test.ts (#4017 (comment)), the result-deadline doc in request-cancel.ts (#4017 (comment), #4017 (comment)), the real-sleep test in alert.test.ts (#4017 (comment)), the test name in http-server-rpc-lifetime.test.ts (#4017 (comment)), the capped-timeout help sentence (#4017 (comment)), the deleted runner-exchange-deadline test (#4017 (comment)), and the deadlineAtMs option doc (#4017 (comment)). No open thread still applies.

One limit on the evidence: the live run used iOS 27.0 with an induced SpringBoard stall, not iOS 17.5 as #3861 reports. The route and markers match, but the cold runner start and RUNNER_BUSY drain timing on 17.5 is still unmeasured. I read the live log excerpts only as quoted in the author comment, and I did not run the tests locally. The remaining step is for a maintainer to accept the iOS 27 induced-watchdog run in place of an iOS 17.5 run.

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

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

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 17.5: open --launch-url still leaves the "Open in" alert up in 5/22 runs on 0.21.29: the runner's accept times out (main thread execution timed out)

1 participant