Skip to content

Hand off physical iOS runner sessions across daemon restarts #2681

Description

@thymikee

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 (Keep iOS simulator runner hot across session close #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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions