# iOS runner protocol optimization plan

Issue #656 is now split into protocol infrastructure plus follow-up optimizations. The lifecycle
protocol makes commands identifiable, but the performance wins come from changing when the daemon
uses `uptime`, retries, invalidates sessions, and asks the runner for lifecycle status.

## Work slices

### 1. Status-before-invalidate recovery

Status: in progress on `codex/ios-runner-status-recovery`.

Goal: when a command has been sent and the HTTP response is lost, ask the runner for
`status(statusCommandId)` before invalidating the session or surfacing an ambiguous transport
failure.

Acceptance criteria:

- Post-send retryable transport failures issue one bounded `status` probe with the original
  `commandId` before session invalidation.
- `completed` with retained small response JSON returns the recovered command result without
  invalidating or resending the command.
- `failed` returns the runner failure code/message/hint instead of a generic transport failure.
- `notAccepted`, status timeout, or status transport failure preserves the existing invalidation
  behavior.
- Read-only commands whose response was not retained keep the existing retry behavior.
- Status recovery probes are short-budget and do not consume the full command timeout.

iOS simulator validation:

- Unit: `pnpm exec vitest run src/platforms/ios/__tests__/runner-command-retry.test.ts`.
- Unit bundle: `pnpm exec vitest run src/platforms/ios/__tests__/runner-client.test.ts src/platforms/ios/__tests__/runner-session.test.ts src/platforms/ios/__tests__/runner-command-retry.test.ts src/platforms/ios/__tests__/runner-provider.test.ts`.
- Build: `pnpm build:xcuitest`.
- Manual sim smoke after build:
  - `pnpm build`
  - `pnpm clean:daemon`
  - run a simple iOS simulator session against Settings with `open`, `snapshot -i`, one selector
    interaction, and `close`.
  - confirm there is no visible behavior change and diagnostics show no unexpected session
    invalidation.

### 2. Adaptive `uptime` preflight policy

Status: implemented with guardrails (see ADR 0005). The earlier blanket "recent success" cache was
shipped and then reverted in #702 because XCTest could return a successful tap response and then fail
the runner while re-resolving a navigation-disappeared element, and because sparse AX-fallback
snapshots were cached as healthy state. #702's coordinate-first activation removed that teardown
trigger, so the skip is reintroduced as a structurally narrower "healthy mutation recency" signal.

Goal: skip the per-command `uptime` for hot allowlisted interaction loops only when a proven healthy
mutating response makes the runner's liveness already known, while every uncertain path keeps
preflighting.

Acceptance criteria (as shipped):

- First-command/startup, no-record, stale-record, app-activation-uncertain, and non-allowlisted
  (conservative) commands still preflight; readiness probes and read-only startup commands keep
  their existing skips.
- Recency is derived only from healthy (parsed ok, non-`runnerFatal`) responses of an explicit
  mutating allowlist (`tap`, `longPress`, `drag`, `swipe`, `scroll`, `sequence`) for the same
  `appBundleId`, within a 5s freshness window, and lives only on the session object so it dies with
  every invalidation/restart. Snapshots and read-only responses never refresh it.
- A transport failure after a skipped preflight clears the recency record and marks the error with
  the skip context (`runnerReadinessPreflightSkipped`, distinct from the restart predicate's
  `runnerReadinessPreflightFailed`). Connection-shaped failures run status recovery before
  invalidation — never a replay; timeout-shaped failures propagate with the skip context, matching
  the existing classification for preflighted sends.
- Diagnostics expose whether a command used, skipped, or recovered from a readiness preflight,
  including command type, skip reason, and recency age.
- Measured threshold: 1 runner request per hot allowlisted command after the first, with no increase
  in invalidation or failure rate.

iOS simulator validation:

- Start a fresh simulator session and run one interaction: verify the first mutating command still
  preflights.
- Run a hot loop of repeated selector interactions against the same visible control: verify the
  runner remains healthy and diagnostics explain any readiness probe that was skipped.
- Compare median command latency for a hot interaction loop before and after the change. A useful
  threshold is at least one fewer runner request per hot command and no increase in failure rate.

### 3. Status-visible transport path

Goal: make `accepted` and `started` states practically observable while a command is still running.
The Swift journal already records these states, but the runner currently serializes connection
handling, so a concurrent status request can be blocked behind the command it is querying.

Acceptance criteria:

- `status` can be answered while another runner command is waiting on main-thread XCTest work.
- The status path remains journal-only and does not touch app activation, XCTest dispatch, or
  command retry logic.
- Long-running command status can report `accepted` or `started` before the command reaches a
  terminal state.
- Existing command execution remains serial where mutation ordering matters.

iOS simulator validation:

- Run a deliberately long runner command in one request.
- While it is in flight, query `status(statusCommandId)` from another request.
- Verify status returns before the long command completes and reports `accepted` or `started`.
- Verify normal command ordering is unchanged for back-to-back mutating commands.

### 4. Session invalidation reduction

Goal: avoid tearing down otherwise healthy runner sessions when lifecycle status proves the command
completed or failed cleanly.

Acceptance criteria:

- Completed/failed lifecycle status suppresses invalidation for ambiguous post-send transport
  errors when the runner remains reachable.
- Unknown status states still invalidate to preserve current safety.
- Diagnostics record why invalidation was skipped or retained.
- No command is replayed after an observed mutating `accepted`, `started`, `completed`, or `failed`
  state.

iOS simulator validation:

- Inject or simulate a lost response after a command completes.
- Verify status recovery prevents runner restart.
- Run the next command in the same session and verify it succeeds without re-launching xcodebuild.

### 5. Response retention tuning

Goal: retain enough small command results for useful recovery without making the runner retain large
snapshots or binary-like payloads.

Acceptance criteria:

- Small scalar responses can be recovered from `lifecycleResponseJson`.
- Snapshot node trees and screenshots are not serialized or retained in the journal.
- The journal memory cap remains bounded by entry count and response JSON size.
- Retention policy is documented in tests or runner fixtures so future commands do not accidentally
  store large payloads.

iOS simulator validation:

- Run small-result commands and verify status can recover retained JSON.
- Run snapshot-heavy commands and verify status reports terminal state without retained response JSON.
- Confirm the runner remains responsive after repeated snapshots.

## Suggested ordering

1. Land status-before-invalidate recovery first. It is the safety net needed before reducing
   defensive preflights.
2. Add diagnostics/metrics for preflight use, skipped preflights, status recovery, and invalidation
   reason. This can happen alongside slice 1 or 2.
3. Reduce `uptime` for hot interaction loops with a conservative command allowlist.
4. Make the status transport path observable during long-running commands.
5. Broaden the preflight policy only after simulator measurements show stable behavior.

## Side-by-side work

- Status recovery and diagnostics can be developed together or separately.
- Transport status visibility can proceed independently once the protocol is on `main`.
- Adaptive `uptime` should wait for status recovery, because it relies on the same recovery path for
  ambiguous post-send failures.
- Response retention tuning can proceed independently as long as it preserves the current caps.
