Hand off physical iOS runner sessions across daemon restarts

Open
#2,681 0 comments 0 reactions 0 assignees View on GitHub

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

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

enhancement needs-triage

Why

Daemon-to-daemon runner handoff exists, but both halves are simulator-gated:

  • detachIosSimulatorRunnerSessionsForShutdown skips any session whose device.kind !== 'simulator' (packages/platform-apple/src/runner/runner-session.ts:663).
  • tryAdoptRunnerSessionFromLease returns null for 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.

  1. Gate on the real target. device.kind === 'device' is not "physical iOS": macOS hosts are kind: 'device', and physical tvOS is too. Decide explicitly whether the change covers tvOS and the xctest backend (core/physical-device-control.ts:114) or gate on physical iOS / Apple device-family with isMacOs excluded, and say so in the code.
  2. Detach only a runner that reached ready. canWorkWithRunnerSession accepts '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 reached LISTENER_READY. Detach requires a state that proves it served requests.
  3. Settle process lifetime before claiming handoff. The runner is spawned detached: true while the daemon owns its stdout/stderr pipes (runner-process-launch.ts:53-59). After the daemon exits, a later write by xcodebuild — device lock, replug, testmanagerd chatter — gets EPIPE/SIGPIPE, and post-handoff runner output no longer reaches runner.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.
  4. Give the probe a route that can answer at cold start. Adoption's uptime probe already goes through the command route resolver (probeRunnerAnswersUptimesendRunnerCommandOncecreateRunnerCommandRouteResolver, 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 spends RUNNER_ADOPTION_PROBE_TIMEOUT_MS = 500 ms (runner-adoption.ts:38) including a devicectl tunnel fallback it cannot finish in time. The xctest backend is usbmux-only with no tunnel route (:54), so only coredevice needs a cold-tunnel budget; measure both and do not inherit 500 ms unexamined.
  5. Rename honestly. If the function also detaches physical sessions, detachIosSimulatorRunnerSessionsForShutdown is wrong. It is part of the AppleRunnerClient surface (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.json baseline entries (:96, :162) that move with the rename, and it is named in a comment at src/daemon/server/daemon-idle-reap.ts:17. Rename across all of it in the same PR.
  6. 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, and xctest if 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 plus startupTimings: adoption is already timed as adopt_detached_runner and the adopted session inherits the map (runner-session.ts:168-178), so the assertion is that step present and ensure_xctestrun / launch_xcodebuild (runner-session.ts:208, :246) absent.
  • Mock-only coverage does not count for the physical lanes: copied simulator tests with kind changed 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 (runnerMainThreadBusy is 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 main on 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-exports green.

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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from callstack/agent-device

All issues in callstack/agent-device

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.