mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
3a02e514e1
* refactor(ios): consolidate series batching onto the sequence runner command Closes #767 Routes every Apple multi-press variant (plain, double-tap, hold, jitter) and swipe series through budget-chunked sequence requests, retiring the daemon-side tapSeries and dragSeries senders: - Add a doubleTap step kind to the sequence allowlist on both ends, mirroring the retired tapSeries doubleTapAt branch. - The single doubleTap interactor sends a one-step sequence and parses the result, surfacing step failures as errors. - Swipe series unroll ping-pong daemon-side into per-step endpoints; the runner's coordinate-drag path ignores durationMs exactly as the daemon-sent (non-synthesized) dragSeries did. - Extract runIosSequenceChunks so press and swipe share the chunking, aggregation, and global step-index rebasing. - Keep tapSeries/dragSeries runner handlers for wire compatibility with older daemons, annotated like interactionFrame; remove both from the preflight-skip allowlist (daemon never sends them) and update ADR 0005 / protocol-optimizations docs. This also closes the latent watchdog exposure where press --count N --interval-ms M routed to tapSeries and executed all pauses inside one 30s-watchdog main-thread block with no chunking. Behavior note: plain tap series now use the synthesized HID tap path on iOS non-tv (with runner-side tapAt fallback), matching the individual tap command instead of the retired tapSeries' XCUICoordinate taps. https://claude.ai/code/session_01VokBZWESTDgcnbYwS4DkJo * refactor(ios): drop dead series wire surface from the daemon - Remove chunkRunnerSequenceSteps: superseded by the budget-aware chunker; no production callers remained. - Remove tapSeries/dragSeries from the RunnerCommand union along with their orphaned fields (count, intervalMs, doubleTap, pauseMs, pattern) and protocol fixtures: this type is the send surface of the current daemon, which no longer sends either command. The Swift runner keeps serving both for wire compatibility with older daemons. - Retarget the ready-mutation preflight test from tapSeries to sequence. https://claude.ai/code/session_01VokBZWESTDgcnbYwS4DkJo * refactor(ios): remove retired series and frame wire commands entirely Drops the runner-side wire compatibility for tapSeries, dragSeries, and interactionFrame now that no daemon path sends them (series fuse into sequence since this branch; interactionFrame was fused into scroll in #760): - Swift: delete the three handler cases, performDragSeries, runSeries (no remaining callers), the CommandType enum cases, journal-retention and traits entries, and the Command fields (count, intervalMs, doubleTap, pauseMs, pattern) that existed only for them. The never-sent synthesized dragSeries branch goes with it. - TS: drop interactionFrame from the RunnerCommand union and isReadOnlyRunnerCommand, and its protocol fixture. - Update stale perf scenario labels referencing the retired commands. Verified dead before removal: no dynamic command construction anywhere (runner-command-recovery only echoes in-flight command ids), no raw-string references in Swift, no docs references. Helpers shared with live paths (synthesizedDragAt, doubleTapAt, keyboardAvoidingDragPoints, sleepFor) all retain callers. Compat: an old daemon paired with a runner built from these sources gets a CommandType decode rejection; the source-fingerprint check rebuilds a matching runner on the next session. https://claude.ai/code/session_01VokBZWESTDgcnbYwS4DkJo --------- Co-authored-by: Claude <noreply@anthropic.com>
160 lines
8.1 KiB
Markdown
160 lines
8.1 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 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.
|