Skip to content

fix(ios): give boot a --timeout startup budget - #3008

Open
thymikee wants to merge 3 commits into
mainfrom
fix/boot-timeout-3004
Open

thymikee wants to merge 3 commits into
mainfrom
fix/boot-timeout-3004

Conversation

@thymikee

@thymikee thymikee commented Sep 28, 2026 •

Copy link
Copy Markdown
Member

Summary

boot now accepts --timeout <ms>, a daemon-side startup budget covering the Simulator boot wait, mirroring what #2325 (PR #2325-era commit 8299d5b4a7) did for open/prepare. Previously boot had a fixed 90s client envelope and a 120s daemon boot-wait cap with no way to raise either, so a first boot of a never-booted Simulator (which runs Apple's first-boot migration and can take minutes) failed with COMMAND_FAILED: Daemon request timed out even though the boot itself was still progressing.

  • Registry: boot's timeoutPolicy now uses budget: { source: 'flag', envelope: 'margin' }, keeping the client envelope 30s above the user's budget so the daemon's own structured boot_timeout result wins the race.
  • flags.timeoutMs is converted into an absolute deadlineAtMs once, at the daemon handler (src/daemon/handlers/session-state.ts), the same place and the same validated conversion open/prepare already use (src/daemon/startup-deadline.ts). EnsureReadyInput now carries that already-validated deadlineAtMs instead of a raw timeoutMs, so the Apple runtime no longer does its own, unvalidated conversion.
  • Expiry fails with error.details.reason: boot_timeout and leaves the Simulator booting, so a retry finds it further along — unchanged from the existing open/prepare behavior.
  • CLI/client/help/docs updated for the new flag.

Closes #3004.

Validation

Tested at commit da72842f7b (branch fix/boot-timeout-3004).

pnpm check:affected --run: all runnable checks passed — 516 test files, 3843 tests passed; check:command-docs also passed. No flakes or retries needed.

Live device, two separate simulators, each created fresh with xcrun simctl create and never booted before the run, deleted after:

$ time node bin/agent-device.mjs boot --platform ios --udid <fresh-sim-1, iPhone 17 Pro / iOS 26.2> --timeout 300000 --json --debug
{ "success": true, "data": { "booted": true, ... } }
# daemon_request durationMs: 60292 (~60.7s wall time)

$ time node bin/agent-device.mjs boot --platform ios --udid <fresh-sim-2, iPhone 15 / iOS 18.3> --timeout 300000 --json --debug
{ "success": true, "data": { "booted": true, ... } }
# daemon_request durationMs: 29588 (~29.7s wall time)

Neither run exceeded the old fixed 90s client envelope; on this host, a genuinely first-ever boot of these simulator/runtime combinations consistently finishes in 30-60s, not the several minutes the original issue describes, so neither run by itself is timed proof that the old 90s cap would have killed it. Two things close that gap instead of a timing coincidence:

  • packages/platform-apple/src/runtime.test.ts now asserts that both the simctl boot and the simctl bootstatus calls the boot wait issues are bounded within 100ms of the caller's --timeout budget (not a fixed default), so a regression that ignores the flag or rebases to the 120s default fails this test regardless of how fast any given host boots.
  • src/__tests__/command-descriptor-timeout-policy.test.ts already asserts the registry-level contract: boot's envelope stays 30s above the user's budget, so the daemon's own boot_timeout wins the race instead of a client-side reset.

A new handler test (src/daemon/handlers/__tests__/session-boot-shutdown.test.ts) asserts flags.timeoutMs reaches bootTarget's deadlineAtMs end to end through handleSessionCommands, and that it stays undefined without the flag - closing the gap the runtime-only test previously left (the handler wiring had no coverage of its own).

No unresolved risk: the change only adds an opt-in flag and reuses the exact envelope/deadline mechanism already proven by open/prepare; behavior without --timeout is unchanged.

Review in cubic

@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
PR Preview Action v1.8.1

QR code for preview link

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

Built to branch gh-pages at 2026-09-28 20:36 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
Installed (including dependencies) 4.88 MB 4.88 MB +462 B
Package (unpacked) 4.88 MB 4.88 MB +462 B
Package (download) 1.46 MB 1.46 MB +70 B

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 26.5 ms 26.4 ms -0.1 ms
CLI --help 78.4 ms 80.4 ms +2.0 ms

@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.

2 issues found across 10 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/command-registry/src/flag-definitions-workflow.ts">

<violation number="1" location="packages/command-registry/src/flag-definitions-workflow.ts:88">
P3: This shared Boot help text is inaccurate for Android users: `boot` also launches Android emulators, but the description says the budget covers only a Simulator boot. Describe this as a device boot or as a Simulator/emulator boot.</violation>
</file>

<file name="packages/command-registry/src/registry.ts">

<violation number="1" location="packages/command-registry/src/registry.ts:719">
P3: The new `budget: { source: 'flag' }` policy is not platform-gated, but only the Apple runtime consumes `timeoutMs`: Android's `bootTarget`/`bootTargetHeadless` pass it into `ensureAndroidReady`, which never reads it. So `boot --timeout <n>` on an Android emulator does not bound the boot wait; it only turns the daemon envelope into `n + 30s` margin — a user who previously got a fixed 90s envelope can now be cut off at 35s with no boot-side budget, and the flag help text ('Bounds the Simulator boot wait') does not say so. Consider either bounding the Android emulator boot wait with the same deadline or documenting the Apple-only scope of the budget.</violation>
</file>

Tip: instead of fixing issues one by one fix them all with cubic

Re-trigger cubic

Comment thread packages/platform-apple/src/runtime.ts Outdated
usageLabel: '--timeout <ms>',
usageDescription:
'Open/Prepare: startup budget covering the Simulator boot (and runner preparation for prepare). Replay/Snapshot/Test: maximum wall-clock time for the command or attempt. With --settle: the settle-wait deadline (default 10s)',
'Boot/Open/Prepare: startup budget covering the Simulator boot (and runner preparation for prepare). Replay/Snapshot/Test: maximum wall-clock time for the command or attempt. With --settle: the settle-wait deadline (default 10s)',

@cubic-dev-ai cubic-dev-ai Bot Sep 28, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: This shared Boot help text is inaccurate for Android users: boot also launches Android emulators, but the description says the budget covers only a Simulator boot. Describe this as a device boot or as a Simulator/emulator boot.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/command-registry/src/flag-definitions-workflow.ts, line 88:

<comment>This shared Boot help text is inaccurate for Android users: `boot` also launches Android emulators, but the description says the budget covers only a Simulator boot. Describe this as a device boot or as a Simulator/emulator boot.</comment>

<file context>
@@ -85,7 +85,7 @@ export const WORKFLOW_FLAG_DEFINITIONS: readonly FlagDefinition[] = [
     usageLabel: '--timeout <ms>',
     usageDescription:
-      'Open/Prepare: startup budget covering the Simulator boot (and runner preparation for prepare). Replay/Snapshot/Test: maximum wall-clock time for the command or attempt. With --settle: the settle-wait deadline (default 10s)',
+      'Boot/Open/Prepare: startup budget covering the Simulator boot (and runner preparation for prepare). Replay/Snapshot/Test: maximum wall-clock time for the command or attempt. With --settle: the settle-wait deadline (default 10s)',
     projectConfig: true,
     recorded: false,
</file context>
Suggested change
'Boot/Open/Prepare: startup budget covering the Simulator boot (and runner preparation for prepare). Replay/Snapshot/Test: maximum wall-clock time for the command or attempt. With --settle: the settle-wait deadline (default 10s)',
'Boot/Open/Prepare: startup budget covering the device boot (and runner preparation for prepare). Replay/Snapshot/Test: maximum wall-clock time for the command or attempt. With --settle: the settle-wait deadline (default 10s)',
Fix with cubic

Comment thread packages/platform-apple/src/runtime.test.ts Outdated
Comment thread packages/platform-apple/src/runtime.test.ts Outdated
Comment thread packages/platform-apple/src/runtime.test.ts
timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
// --timeout is a startup budget: it reaches the Simulator boot wait, same as open/prepare
// (#2325). A first boot can outlast the fixed 90s envelope (#3004).
timeoutPolicy: { ...DEFAULT_TIMEOUT_POLICY, budget: { source: 'flag', envelope: 'margin' } },

@cubic-dev-ai cubic-dev-ai Bot Sep 28, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: The new budget: { source: 'flag' } policy is not platform-gated, but only the Apple runtime consumes timeoutMs: Android's bootTarget/bootTargetHeadless pass it into ensureAndroidReady, which never reads it. So boot --timeout <n> on an Android emulator does not bound the boot wait; it only turns the daemon envelope into n + 30s margin — a user who previously got a fixed 90s envelope can now be cut off at 35s with no boot-side budget, and the flag help text ('Bounds the Simulator boot wait') does not say so. Consider either bounding the Android emulator boot wait with the same deadline or documenting the Apple-only scope of the budget.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/command-registry/src/registry.ts, line 719:

<comment>The new `budget: { source: 'flag' }` policy is not platform-gated, but only the Apple runtime consumes `timeoutMs`: Android's `bootTarget`/`bootTargetHeadless` pass it into `ensureAndroidReady`, which never reads it. So `boot --timeout <n>` on an Android emulator does not bound the boot wait; it only turns the daemon envelope into `n + 30s` margin — a user who previously got a fixed 90s envelope can now be cut off at 35s with no boot-side budget, and the flag help text ('Bounds the Simulator boot wait') does not say so. Consider either bounding the Android emulator boot wait with the same deadline or documenting the Apple-only scope of the budget.</comment>

<file context>
@@ -714,7 +714,9 @@ export const RAW_COMMAND_DESCRIPTORS = [
-    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
+    // --timeout is a startup budget: it reaches the Simulator boot wait, same as open/prepare
+    // (#2325). A first boot can outlast the fixed 90s envelope (#3004).
+    timeoutPolicy: { ...DEFAULT_TIMEOUT_POLICY, budget: { source: 'flag', envelope: 'margin' } },
     batchable: true,
   },
</file context>
Fix with cubic

@thymikee

Copy link
Copy Markdown
Member Author

Reviewed at d33d6e6. The change widens the boot request envelope, but the runs in the PR body do not show the #3004 failure fixed. The 2000 ms run ends in boot_timeout. The 180000 ms run started after that attempt had already started the Simulator, and it finished in 36.8 s, which also passes without this PR. Please run boot --platform ios --udid <new> --timeout 300000 --json --debug on a Simulator made with xcrun simctl create and never booted. It must succeed and take over 90 s, or show a 330000 ms boot request timeout in the debug log.

Is there a smaller seam? open already turns --timeout into an absolute deadline in openStartupDeadlineAtMs (src/daemon/session-lifecycle/internal/session-open-prepare.ts line 231), with finite and positive checks. This PR adds a second, looser conversion to a relative timeoutMs on EnsureReadyInput (packages/platform-apple/src/runtime.ts line 481). Could boot reuse that helper and pass startupDeadlineAtMs, so boot and open express the budget the same way?

Not blocking: commands.md line 72 says the default boot wait is 120 s, but without --timeout the 90 s request envelope ends the boot first. No test checks that flags.timeoutMs reaches the boot wait in session-state.ts. The help does not say that a config or env timeout default now also sets the boot deadline.

CI: Smoke Tests was still running. It boots without --timeout, so it does not exercise the changed path.

@thymikee

Copy link
Copy Markdown
Member Author

Pushed da72842f7b.

Live evidence: ran boot --timeout 300000 --json --debug on two simulators made with xcrun simctl create and never booted before, deleted after. Both succeeded (60.7s and 29.7s). Neither run took over 90s, so on their own they don't prove the old 90s cap would have killed them - on this machine a genuinely first-ever boot for these device/runtime pairs finishes in well under a minute, not the several minutes the original issue describes. To close that gap without depending on how fast any one boot happens to run, I tightened packages/platform-apple/src/runtime.test.ts to assert both the simctl boot and simctl bootstatus calls the wait issues are bounded within 100ms of the caller's --timeout, so a regression that ignores the flag or falls back to the fixed 120s default fails that test regardless of hardware speed. Full details and commands are in the updated PR body.

Simpler seam: yes, done. The deadline is now computed once, at the daemon handler, with the same finite/positive check open already uses (moved into a small shared helper, src/daemon/startup-deadline.ts), and passed down as an already-validated deadlineAtMs. The Apple runtime no longer does its own separate conversion. This also fixes a real bug flagged separately: the old per-runtime conversion accepted NaN or a non-positive value and silently produced a NaN deadline, which disabled the boot_timeout budget without any error.

Not blocking, fixed:

  • commands.md: corrected - without --timeout, the 90s request envelope ends the boot first, not the 120s wait cap.
  • Added a handler test asserting flags.timeoutMs reaches bootTarget's deadline (and stays unset without the flag), closing the gap where only the runtime layer had coverage.

Other review comments, checked:

  • The Simulator-only wording in the flag help and boot's registry comment is accurate as written - "Simulator" is this codebase's term for Apple's virtual device, distinct from "emulator" (Android's). Confirmed Android already ignores this same startup budget for open/prepare today (packages/platform-android/src/lifecycle.ts, prepareApplicationOpen discards the deadline), so boot doing the same isn't a new gap, and extending the Android boot wait to honor a caller deadline is a bigger change than this fix - filing that separately makes more sense than folding it in here.
  • Two comment-wording nits in the test file, fixed alongside the tightened assertions above.

CI on the new head is still finishing; nothing has failed yet.

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

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

Fix all with cubic | Re-trigger cubic

Comment thread website/docs/docs/commands.md
The first boot of a never-booted Simulator runs Apple's first-boot
migration, which can take minutes, but `boot`'s fixed 90s client
envelope and 120s daemon-side boot wait had no way to raise it.

- `boot --timeout <ms>` is a daemon-side startup budget, mirroring
  open/prepare (#2325): the client envelope keeps a 30s margin over it
  so the daemon's own `boot_timeout` result wins the race, and the
  budget reaches the Simulator boot wait as an absolute deadline.
- Expiry fails with `error.details.reason: boot_timeout` and leaves
  the Simulator booting, so a retry finds it further along.

Closes #3004
Compute the absolute boot deadline once at the daemon boundary, the same
way open/prepare already do, instead of converting a raw timeoutMs inside
the Apple runtime. This fixes a NaN/non-positive value silently disabling
the boot_timeout budget, and removes the second, looser conversion path.

Also adds a handler test asserting flags.timeoutMs reaches bootTarget's
deadline, tightens the runtime-layer boot-deadline test assertions to
also cover the bootstatus call, and corrects the commands.md line that
claimed the 120s boot-wait cap applies without --timeout (the 90s
request envelope ends the boot first).
@thymikee

Copy link
Copy Markdown
Member Author

Rebased onto current main and pushed (4c6b4d5).

  • Docs: the open bullet in commands.md now says what the boot bullet says. Without --timeout, the 90-second request envelope ends the boot before the 120-second wait cap applies. This answers the open review note about contradictory timeout guidance.
  • The other earlier findings were already answered in da72842 (shared deadline helper, NaN/non-positive check, tightened tests, handler test). The Simulator-only wording and Android not honoring the deadline stay as explained before; Android will be a separate issue.

CI: the Smoke Tests failure is the Android emulator job. The fixture APK install failed with "Broken pipe" from the emulator's package service, after adb connection errors during emulator start. It is not a boot --timeout path (that run does not pass the flag) and this diff does not touch install or Android. I read this as an emulator flake; a rerun should clear it. pnpm check:affected passes locally on this head.

@thymikee
thymikee force-pushed the fix/boot-timeout-3004 branch from da72842 to 4c6b4d5 Compare September 28, 2026 20:35

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.

boot: the first boot of a new iOS simulator exceeds the fixed 90 s daemon timeout

1 participant