Hand off physical iOS runner sessions across daemon restarts
Nobody has claimed this yet.
Assessment
- Difficulty
- 5/5
- Estimated time
- Over a week
- Newbie friendliness
- 25/100
- Issue type
- Feature
- Clarity
- Mostly clear
- Activity status
- Active
- Tech stack
- ios, typescript
- Domain
- backend, mobile, testing-qa
Research direction
Start with detachIosSimulatorRunnerSessionsForShutdown in packages/platform-apple/src/runner/runner-session.ts and tryAdoptRunnerSessionFromLease in runner-adoption.ts, then trace the AppleRunnerClient surface and runner-command-route.ts. Run the physical-device verification from docs/agents/device-verification.md and the affected checks first. Done means physical adoption is proven per backend, refusal and startup-shutdown cases are covered, naming is updated, and simulator behavior remains comparable to main.
Written by the indexing model from the issue text.
Description
Why
Daemon-to-daemon runner handoff exists, but both halves are simulator-gated:
detachIosSimulatorRunnerSessionsForShutdownskips any session whosedevice.kind !== 'simulator'(packages/platform-apple/src/runner/runner-session.ts:663).tryAdoptRunnerSessionFromLeasereturnsnullfor the same condition (packages/platform-apple/src/runner/runner-adoption.ts:64).
The lease is not the blocker: buildRunnerLease/writeRunnerLease run for every kind (runner-session.ts:263, :299) and already carry the identity facts adoption needs (RunnerLease, runner-lease.ts:41-63).
So a healthy physical runner is killed and rebuilt whenever the daemon restarts. Beyond the wasted xcodebuild boot, this closes the only door an embedding consumer has: a caller that cannot guarantee one long-lived daemon (parallel worktrees, short-lived agent invocations, an SDK host that recycles its daemon) cannot reuse a live runner, so it builds private runner contact instead. An external report describes driving a live LISTENER_READY runner over usbmux precisely to avoid taking our lease. That report is secondhand; the gate above is not.
Task
Extend the existing detach + adoption pair to physical Apple devices. Do not add a second lease or a second adoption path.
- Gate on the real target.
device.kind === 'device'is not "physical iOS": macOS hosts arekind: 'device', and physical tvOS is too. Decide explicitly whether the change covers tvOS and thexctestbackend (core/physical-device-control.ts:114) or gate on physical iOS / Apple device-family withisMacOsexcluded, and say so in the code. - Detach only a runner that reached ready.
canWorkWithRunnerSessionaccepts'starting'(runner-session-types.ts:124-126), and physical startup runs tens of seconds, so a graceful shutdown mid-boot would currently hand off a runner that never reachedLISTENER_READY. Detach requires a state that proves it served requests. - Settle process lifetime before claiming handoff. The runner is spawned
detached: truewhile the daemon owns its stdout/stderr pipes (runner-process-launch.ts:53-59). After the daemon exits, a later write byxcodebuild— device lock, replug,testmanagerdchatter — gets EPIPE/SIGPIPE, and post-handoff runner output no longer reachesrunner.log. Prove the child survives, or restructure the handoff so it does. A live lease pid proves the host process only; it proves nothing about the on-device runner after a replug, reboot, or DDI remount. - Give the probe a route that can answer at cold start. Adoption's
uptimeprobe already goes through the command route resolver (probeRunnerAnswersUptime→sendRunnerCommandOnce→createRunnerCommandRouteResolver,runner-transport.ts:21), but the tunnel-IP cache is process-global (runner-command-route.ts:23) and a new daemon starts with it empty. For a CoreDevice device usbmuxd does not list, the probe spendsRUNNER_ADOPTION_PROBE_TIMEOUT_MS= 500 ms (runner-adoption.ts:38) including adevicectltunnel fallback it cannot finish in time. Thexctestbackend is usbmux-only with no tunnel route (:54), so onlycoredeviceneeds a cold-tunnel budget; measure both and do not inherit 500 ms unexamined. - Rename honestly. If the function also detaches physical sessions,
detachIosSimulatorRunnerSessionsForShutdownis wrong. It is part of theAppleRunnerClientsurface (runner/client.ts:20,45,76,core/runner-client.ts:36,runner-operations-facade.ts:3,src/platform-runtime-apple-application-tools.ts:116), it is named in two.fallowrc.jsonbaseline entries (:96,:162) that move with the rename, and it is named in a comment atsrc/daemon/server/daemon-idle-reap.ts:17. Rename across all of it in the same PR. - Keep
AGENT_DEVICE_IOS_RUNNER_DETACH(runner-adoption.ts:46-49) as the switch for both kinds, and keep simulator-set redirects excluded (runner-session.ts:665-669) after confirming that path is unreachable for physical devices rather than assuming it.
Acceptance criteria
- Physical evidence, per backend that stays in scope (
coredevice, andxctestif covered), recorded with the device, iOS version, and commit SHA: start a session, restart the daemon, next command is served by the adopted runner. Proof is the adoption diagnostic plusstartupTimings: adoption is already timed asadopt_detached_runnerand the adopted session inherits the map (runner-session.ts:168-178), so the assertion is that step present andensure_xctestrun/launch_xcodebuild(runner-session.ts:208,:246) absent. - Mock-only coverage does not count for the physical lanes: copied simulator tests with
kindchanged do not exercise the usbmux route and do not satisfy this issue. - Refusal matrix, one test each: lease absent, lease not stale, pid dead, pid recycled, artifact fingerprint mismatch,
session_identity_mismatch, owner alive but owner-state dir gone, probe exceeds budget on the cold tunnel route. - Shutdown during runner startup tears down instead of detaching; a wedged/in-flight runner is decided by a named detector, not by state alone (
runnerMainThreadBusyis the only occupancy fact available today). - Retained-after-close keep-hot behavior (#1013,
AGENT_DEVICE_IOS_RUNNER_IDLE_STOP_MS,runner-session.ts:517-522) is unchanged or deliberately changed with a stated reason — detaching a retained runner may be the intended payoff, so "close never leaves an adoptable lease" is not the requirement. - Simulator detach/adoption timings comparable to
mainon the simulator lane, with a stated threshold. - Physical verification follows
docs/agents/device-verification.md.pnpm check:affected --run,pnpm check:layering,pnpm check:production-exportsgreen.
Non-goals
A general runner-ownership protocol, remote leases (ADR 0007, #2598), or a runner-status CLI surface. The session-state enum and response-decoder consolidation are #2662.
Related: ADR 0005, #1403 (usbmux primary), #1013 (keep-hot), #2662.
- Dominant language
- TypeScript
- Stars
- 4.7k
- Forks
- 304
- Avg merge
- 11h 46m
- Merged PRs (30d)
- 471
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from callstack/agent-device
-
mutation-model: indirect-ownership self-test pins a test that stopped reaching scroll-edge-state Openbug needs-triage
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
callstack/agent-device#2547 ·
-
refactor
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
callstack/agent-device#2530 · 1 comment ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
callstack/agent-device#1869 ·
-
needs-triage
Difficulty 4/5 3-5 days Newbie friendliness 55/100
callstack/agent-device#2714 ·
-
iOS smoke fails in verify-installed-snapshot-bridge when a cold xcrun probe eats the shared deadline Openneeds-triage
Difficulty 4/5 3-5 days Newbie friendliness 48/100
callstack/agent-device#2712 ·
All issues in callstack/agent-device
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
0xMiden/bridge-portal#132 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
-
area:tools bug good first issue help wanted priority:P2
Difficulty 2/5 1-3 hours Newbie friendliness 90/100
TaewoooPark/Motifcode#14 ·
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
newrelic-experimental/preflight#793 · 1 comment ·