You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
feat(daemon): run a state-dir boot hook instead of booting a shut-down device - #4153
When a command finds its iOS simulator or Android emulator shut down and the daemon state directory holds boot-hook.json, agent-device runs the hook instead of booting the device, then reads the device state again. A device that is still not booted fails with COMMAND_FAILED, details.reason: boot_hook_left_device_down, and a message that names the hook and quotes its output. There is no fallback boot: a tool that owns device lifecycle, such as Stim, admits devices against capacity limits, and agent-device's own boot bypassed that admission. Without the file, behavior is unchanged.
The hook runs without a shell, in cwd, with AGENT_DEVICE_BOOT_HOOK_PLATFORM (ios/android), AGENT_DEVICE_BOOT_HOOK_DEVICE_ID and AGENT_DEVICE_BOOT_HOOK_DEVICE_NAME, for at most 5 minutes, less under boot/open --timeout and the Android startup budget. A timeout or failed start is reported as a run, so readiness still re-checks. Stderr goes to request diagnostics.
The file is read on each boot. An invalid file fails with invalid_boot_hook. Only the daemon's own state directory is read, so a remote client cannot supply a hook.
iOS: ensureBootedSimulator reads the hook from a per-runtime slot bound at daemon composition, so every simulator boot (readiness, screenshot retry, settings) goes through it; ensureAppleReady uses the deviceReadiness.bootHook port. Android: ensureEmulatorReady runs it before emulator -avd.
18 files (8 tests and fixtures, 3 docs). No scope expansion.
Validation
Tested SHA d267ce135. pnpm check:affected --run --head d267ce135 passed, exactCommit: true, 42 checks (receipt .tmp/validation/run-PSVAwB/receipt.json), run on Node 24.18.1. On Node 22.22.2 the same plan stops at docs-links because Astro requires Node >=22.23.4; an earlier run timed out once in provider-integrationmacos-recording.test.ts under host load near 60 and passed alone. CI owns coverage, provider integration and device lanes.
Live (iOS 27 simulator, paired Stim branch): with Metro idle-stopped by Stim and the simulator shut down (by Stim's idle shutdown in an earlier build's run, by simctl shutdown at this SHA), AGENT_DEVICE_STATE_DIR=<workspace> agent-device open <app> --udid <sim> --debug succeeded in 26 s with bootOccurred: true. Diagnostics: device_boot_hook_start (platform: ios, timeoutMs: 300000), device_boot_hook_exit (exitCode: 0, 22.8 s); Stim restarted Metro and relaunched the app, and agent-device screenshot showed the app's first screen. With an earlier build, after stim stop, open failed with boot_hook_left_device_down quoting Stim's STIM_NO_METRO JSON, and the simulator stayed Shutdown.
Not verified: the Android hook live (unit tests only). Residual: runner startup trusts device.booted before xcodebuild test-without-building, which could boot a shut-down simulator; not traced here.
The code looks right at d267ce1, but the Android hook route has no live proof yet. The Android readiness route now runs the hook before androidEmulator.launch and extends the boot deadline after a hook run. The PR body says this path is unit-tested only, and it is device-facing app-lifecycle code. Nothing shows that a real hook-started AVD is found by findByAvdName/isRunningEmulator, that waitForBoot completes, or that agent-device never spawns its own emulator -avd while a hook is configured. Please run it on a live Android emulator with a boot-hook.json whose command starts the stopped AVD (detached, stdio redirected) and exits. Then run agent-device open <app> --platform android --device <avd> --debug and show device_boot_hook_start (platform: android) and device_boot_hook_exit (exitCode 0), with no agent-device-launched emulator process. Also run once with a hook that exits without starting the AVD, and show details.reasonboot_hook_left_device_down.
Not blocking, take or leave: (1) runCmd settles on close, so a hook that backgrounds the device process with inherited stdio (for example emulator -avd X &) keeps the pipes open and the run only ends at the timeout. The rule should be that the hook run ends when its direct child exits, or configuration.md should say the hook must not leave a child holding stdout/stderr. (2) The hook's full stderr is emitted in device_boot_hook_exit, while only the error message is cut to HOOK_OUTPUT_TAIL_CHARS, so please apply the same bound there. (3) After a hook, iOS rejects 'Booting' and skips the bootstatus wait, while Android accepts a running emulator and waits for sys.boot_completed, so please pick one rule for both platforms. (4) The env contract has no headless intent, so boot --headless does not reach the hook; add AGENT_DEVICE_BOOT_HOOK_HEADLESS=1 or document that it does not apply. (5) The per-command readiness route passes no signal or deadline, so the slot hook ignores cancellation for up to 5 minutes. (6) boot_hook_left_device_down and invalid_boot_hook are bare strings; BOOT_TIMEOUT_REASON in packages/contracts/src/device-readiness-runtime.ts is the precedent for exported constants. (7) The hook is built twice, once for the readiness port and once for the slot, each recomputing the state dir; building it once from baseDir would do. (8) In the iOS test, leftDown as never should be a real AppError, the 'runs past its limit' case should use a real 1 s timeout on a sleeping child, and hookTimeoutMs should use host.clock.now() instead of Date.now().
No tests were run for this review, and I could not inspect the local check:affected receipt or the live iOS run. The only check not passing is Vercel, and its bot comment says a Callstack team member must authorize the deployment for this fork contributor, so that looks unrelated to the change. Before merge, the live Android run (success and boot_hook_left_device_down) must be shown.
This branch has not been deployed
No deployments
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
When a command finds its iOS simulator or Android emulator shut down and the daemon state directory holds
boot-hook.json, agent-device runs the hook instead of booting the device, then reads the device state again. A device that is still not booted fails withCOMMAND_FAILED,details.reason: boot_hook_left_device_down, and a message that names the hook and quotes its output. There is no fallback boot: a tool that owns device lifecycle, such as Stim, admits devices against capacity limits, and agent-device's own boot bypassed that admission. Without the file, behavior is unchanged.{ "command": ["stim", "revive", "--json"], "cwd": "/path/to/app" }cwd, withAGENT_DEVICE_BOOT_HOOK_PLATFORM(ios/android),AGENT_DEVICE_BOOT_HOOK_DEVICE_IDandAGENT_DEVICE_BOOT_HOOK_DEVICE_NAME, for at most 5 minutes, less underboot/open --timeoutand the Android startup budget. A timeout or failed start is reported as a run, so readiness still re-checks. Stderr goes to request diagnostics.invalid_boot_hook. Only the daemon's own state directory is read, so a remote client cannot supply a hook.ensureBootedSimulatorreads the hook from a per-runtime slot bound at daemon composition, so every simulator boot (readiness, screenshot retry, settings) goes through it;ensureAppleReadyuses thedeviceReadiness.bootHookport. Android:ensureEmulatorReadyruns it beforeemulator -avd.18 files (8 tests and fixtures, 3 docs). No scope expansion.
Validation
Tested SHA
d267ce135.pnpm check:affected --run --head d267ce135passed,exactCommit: true, 42 checks (receipt.tmp/validation/run-PSVAwB/receipt.json), run on Node 24.18.1. On Node 22.22.2 the same plan stops atdocs-linksbecause Astro requires Node >=22.23.4; an earlier run timed out once inprovider-integrationmacos-recording.test.tsunder host load near 60 and passed alone. CI owns coverage, provider integration and device lanes.Live (iOS 27 simulator, paired Stim branch): with Metro idle-stopped by Stim and the simulator shut down (by Stim's idle shutdown in an earlier build's run, by
simctl shutdownat this SHA),AGENT_DEVICE_STATE_DIR=<workspace> agent-device open <app> --udid <sim> --debugsucceeded in 26 s withbootOccurred: true. Diagnostics:device_boot_hook_start(platform: ios,timeoutMs: 300000),device_boot_hook_exit(exitCode: 0, 22.8 s); Stim restarted Metro and relaunched the app, andagent-device screenshotshowed the app's first screen. With an earlier build, afterstim stop,openfailed withboot_hook_left_device_downquoting Stim'sSTIM_NO_METROJSON, and the simulator stayed Shutdown.Not verified: the Android hook live (unit tests only). Residual: runner startup trusts
device.bootedbeforexcodebuild test-without-building, which could boot a shut-down simulator; not traced here.