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

Merged
TH
2mo ago
+157-13

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.