Repository navigation
refactor(apple-runner): own close retention behind one runner operation #2615
Description
Activity
Re-audited the #2605 head and implemented the remaining obligation in #2623 (stacked on #2605's branch, since this issue is blocked by it).
Audit: the seam this issue asks for already landed on that head in
029c9e97—finalizeApplicationClosenow issues one awaitedreleaseRunnerOnClose(deviceId, { retain }),runner-session.tsdecides using its ownrunnerMainThreadBusy, and the busy-only operation plus its forwarding exports are gone. So thefinalizeRunnerClose(deviceId, { retainForReuse })name is the only thing the merged shape differs on, and the issue calls that conceptual. What was still owed is the completion evidence.Added
packages/platform-apple/src/runner/__tests__/runner-close-finalization.test.ts(8 cases): the realfinalizeApplicationClosedriving the real runner module, composed as the root's lazy tools compose it. Row -> case: idle retain keeps the same session and launches nothing; busy retain disposes, releases the lease and the reopen boots a second process; non-retain stops and cancels the idle stop a retain armed (proven by the absence ofios_runner_idle_stopunder a 40 ms window); non-retain with no runner in memory still releases an owned lease; retain with no runner in memory starts and claims nothing; daemon shutdown never issues the release; disposal rejection propagates withdismissCloseAlertsunreached; and a drain that lands after close is issued (runner exit held open, occupancy cleared by a served reply) cannot return the runner to reuse. Busy is produced by a realRUNNER_BUSYrefusal and cleared by a real stamped reply. Four deadscheduleIosRunnerIdleStopmock entries went with it.Discrimination (temporary edits, all reverted): busy check removed -> 3 fail; release no longer awaited -> 6;
cleanupOwnedIosRunnerLeaseremoved -> the lease case;cancelIosRunnerIdleStopremoved -> the cancellation case.Runner-local extension proof: a temporary
ready !== falsereuse condition plus its own case touched exactlyrunner/runner-session.tsand the runner's test — no contract method, no forwarding adapter, no replay-fixture edit, typecheck and the close/replay/shutdown suites green unchanged. Removed before publishing.Gates:
pnpm check:affected --rungreen on455f9ec5c3(the earlier failure was 8 unrelated 5 s timeouts at host load 60+ from sibling worktrees' gates; those files pass in isolation and the tier went green on re-run). No live close/open run: this diff changes no production line, so the wedge scenario and device lanes stay with #2605.Closing: every obligation is on main at
17eabc8fd1.- Seam:
finalizeAppleApplicationCloseissues one awaitedreleaseRunnerOnClose(deviceId, { retain });runner-session.tsdecides from its ownrunnerMainThreadBusy. Landed in fix(ios-runner): dispose a busy runner on close so open boots clean (#2552) #2605 (029c9e97). The busy-only operation and its forwarding exports are gone (no*IfBusysymbol in tracked files);stopRunnerSessionstays on the contract for its remaining non-simulator caller incloseAppleApplication. - Truth table: all six rows pinned at the real lifecycle-to-runner boundary in
runner-close-finalization.test.ts(test(apple-runner): pin the close release truth table at the lifecycle seam #2623,465af754), with the runner-level cases kept inrunner-session-close.test.ts. 36/36 green on main today. - Live check: fix(ios-runner): dispose a busy runner on close so open boots clean (#2552) #2605 ran the close/open route on a fresh iOS 26.2 simulator at
57ab7ad(idle close reuses the runner; a 45 s longpress →MAIN_THREAD_TIMEOUT→ close disposes → reopen boots a new pid). - Extension proof: done as a temporary runner-local condition in test(apple-runner): pin the close release truth table at the lifecycle seam #2623 (one file plus its test; no contract, adapter or fixture edit).
Not needed as a separate PR: the refactor was absorbed into #2605 before merge, which was the better outcome.
- Seam:
refactor(apple-runner): own close retention behind one runner operation
Purpose and evidence
PR #2605 (
44f78a986d51aeb0aab3e031700a96c2302e299b) fixes reuse of a busy runner. Application lifecycle now combinesstopRunnerSessionIfBusy,scheduleRunnerIdleStopandstopRunnerSession, although the runner owns occupancy and disposal state. Adding the busy primitive also changes shared contracts, forwarding modules and four replay fixtures.Evidence: application close orchestration, runner state owner, host contract.
Required behavior
Expose one awaited close-finalization operation through the existing runner host seam, conceptually
finalizeRunnerClose(deviceId, { retainForReuse: boolean }): Promise<void>.The application lifecycle supplies reuse intent;
runner-session.tschooses using its own occupancy state. Preserve this truth table:Preserve alert-dismissal order, request behavior, error propagation, idle-stop timing and all occupancy update semantics from #2605. Keep lifecycle's shutdown decision outside this operation. Do not return early merely because the session map has no runner: unconditional stop also owns durable lease cleanup under its existing locks.
Scope and exclusions
Runner session implementation, Apple lifecycle, current client/facade/host bindings,
AppleApplicationTools, root composition and directly affected fixtures/tests. Retire the new busy-only public plumbing once unused. Keep unconditional stop wherever other callers still need it. Preserve ADR 0005 and ADR 0019's injected host and lazy loader; no direct lifecycle import of runner implementation, new hook bag, Swift protocol change or recovery-policy redesign. #2524 and #2475 own different recovery/readiness behavior.Completion and validation
pnpm check:affected --runpasses; preserve existing eager-closure constraints and followdocs/agents/device-verification.mdfor a live close/open check on a verified Apple target. Report native/CI obligations separately; a nondeterministic live wedge is not required proof of this behavior-preserving API refactor.Dependencies and readiness
Blocked by: #2605 landing. Re-audit the merged result and all primitive consumers first. Do not implement against pre-#2605 main. Stop if the proposed seam cannot preserve shutdown or lease ordering without moving application policy into the runner. This is a follow-up, not a blocker for #2605.
Effort: M. Risk: medium (shutdown, lease disposal, warm reuse). No existing issue found for this ownership change.