Files
callstack__agent-device/docs/ios-runner-protocol-optimizations.md
2026-08-29 13:10:47 +02:00

160 lines
8.2 KiB
Markdown

# 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 packages/platform-apple/src/runner/__tests__/runner-command-retry.test.ts`.
- Unit bundle: `pnpm exec vitest run packages/platform-apple/src/runner/__tests__/runner-client.test.ts packages/platform-apple/src/runner/__tests__/runner-session.test.ts packages/platform-apple/src/runner/__tests__/runner-command-retry.test.ts packages/platform-apple/src/runner/__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.