Files
Michał Pierzchała 5ef52ad901 fix(replay): bounded retry on pre-dispatch capture racing app launch (#1385) (#1386)
* fix(replay): bounded retry on pre-dispatch capture racing app launch (#1385)

A step right after `open --relaunch` (e.g. a step-2 press) can have its
pre-dispatch target-verification capture land while the app is still
launching/mounting, producing a transient capture-failed/sparse-snapshot
verdict that isn't a real divergence and fails the step closed before it
ever dispatches. captureDivergenceObservation now takes an opt-in
retryLaunchRace flag that bounds-retries that specific capture (fixed
backoff, 12s deadline), mirroring wait's keep-polling landmark semantics
(#1349) on this pre-dispatch path. Only verifyReplayActionTarget's gate
opts in; the post-failure diagnostic capture and post-resolution
guard-mismatch capture stay single-shot since they follow an
already-real failure.

* fix(replay): gate #1385 launch-race retry on content-quality vs mechanism failure

Address review feedback on #1386: the bounded retry was retrying every
thrown capture-failed, including permanent mechanism failures (e.g. a
missing helper artifact) that a retry can never fix, at the cost of the
full backoff budget before the identical divergence. Gate the retry on
the same signal Android's helper capture path already emits for this
exact distinction: retriable:true on a content-poor rejection
(rejectAndroidHelperContentUnavailable), unset on a permanent one
(androidSnapshotHelperUnavailableError). The non-throwing
sparse-snapshot verdict still always retries. Mirrors #1381's
isUnreadableCaptureContentError taxonomy for the wait keep-poll loop.

Also update the two #1385 tests to use a retriable-tagged error and add
a case proving a permanent failure fails on the first attempt.

* fix(replay): narrow #1385 retry to a real content-quality taxonomy, anchor the deadline

Address second review pass on #1386:

P1 - `retriable === true` was too broad: Android's adb layer (adb-executor.ts)
marks genuine transport mechanism failures retriable too (connection_dropped,
device_offline, server_version_mismatch - an unchanged retry of the SAME adb
command can succeed there), so those consumed the launch-race retry budget
despite being exactly the mechanism failures the ADR says must fail fast.
Replace the check with the narrow discriminator: the thrown error's
androidSnapshotHelperFailureReason must be one of the three literal content
verdicts rejectAndroidHelperContentUnavailable attaches (empty-helper-output,
system-window-only, content-poor-app-window) - never the free-form message
androidSnapshotHelperCaptureError sets for a crashed/timed-out helper, and
never present at all for a permanently missing helper artifact. Added a
regression test for an adb mechanism failure marked retriable at the
transport level, proving it still fails on the first attempt.

P2 - the 12s deadline started AFTER the first (unbounded) capture attempt,
so the effective wall-clock cost was "first capture + 12s of retries",
undocumented and untested as such. Anchor the deadline before the first
attempt instead, and rewrite the comments/ADR to state precisely what is
and is not bounded: the delay list caps attempt count, the deadline caps
sleep time from entry, neither caps an individual capture's own duration.
Added a fake-timer test proving the deadline (not just the delay array's
length) is what stops retries when captures themselves consume real time.

* test(replay): add targeted iOS sparse-snapshot retry regressions

Address the reviewer's second finding on #1386: the Android
capture-failed retry path had recover/exhaustion regressions, but the
iOS sparse-snapshot verdict branch (isSparseSnapshotQualityVerdict,
always-retryable, no thrown error involved) had no test of its own -
only incidental coverage by way of the Android-shaped tests. Add the
mirror pair: a sparse verdict that recovers within the bounded retry
and dispatches, and one that never recovers and fails closed as
identity-unverifiable once the retry is exhausted.

* fix(replay): re-consume shared #1381 predicate after rebase reintroduced local one

The branch's rebase onto main re-resolved the #1381 conflict without
carrying forward the earlier fix, reintroducing a local
ANDROID_CAPTURE_CONTENT_QUALITY_REASONS/isCaptureContentQualityFailure
duplicate of the shared isUnreadableCaptureContentError predicate
(src/snapshot/snapshot-quality.ts, landed by #1381). Two parallel
copies of the same taxonomy can silently drift. Drop the local
duplicate and its Android-platform-type import; consume the shared
predicate directly, matching what ADR 0012 already documents.
2026-07-27 13:34:33 +02:00
..

ADR index — read this when…

ADR Read when you touch…
0001 Provider-First Integration Scenarios integration testing strategy, provider transcripts, the scenario harness
0002 Persistent Platform Helper Sessions helper process lifecycle, keep-alive semantics
0003 Daemon Command Registry daemon routing, request-policy traits
0004 iOS Snapshot Backend Strategy snapshot capture plans, backend fallbacks, quality verdicts
0005 iOS Runner Interaction Lifecycle XCTest runner sessions, leases, adoption, idle-stop
0006 Daemon RPC Protocol Version remote daemon HTTP/JSON-RPC compatibility
0007 Remote Device Leases leases, tenancy, provider-owned devices
0008 Command Descriptor Registry adding/changing a command, any surface projection (CLI/MCP/client/batch), timeout policy
0009 Apple Platform Consolidation Apple platform family, apple/appleOs axes, the apple-leak guard
0010 Error system conventions error codes, hints, normalizeError, typed error signals
0011 Interaction Guarantee Contract interaction dispatch paths, fast paths, guards, the guarantee matrix, parity tables
0012 Interactive Replay replay healing/--update, diagnostic resolution disclosure, bounded .ad target-binding evidence, bounded divergence wire/error handling, plan-bound replay-only --from semantics, and agent-supervised re-record repair ("heal-by-doing")
0013 Unified Gesture Plans gesture API/routing, contact topology, multi-touch geometry, native pointer injection, two-finger pan
0014 Session Ref-Frame Lifetime ref authorization epochs, complete/partial issuance, pre-side-effect expiration, replay/batch compatibility, and cross-platform stale-mutation policy
0015 Direct Maestro Compatibility Engine Maestro YAML parsing/execution, compatibility observation policy, conformance, performance gates, gesture integration
0016 Active-Session Script Publication publishing an armed open-to-destination .ad script without closing its live session
0017 Parameterized Recorded Inputs safely authoring sensitive fill inputs as ${VAR} placeholders across recording, replay, and repair

ADRs record why; the registries and gates they describe are the living source of truth — when prose and a registry disagree, the registry wins and the ADR needs a follow-up.

Shape conventions, so consulting an ADR stays cheap:

  • Normative rules first, terse. Status, then a "Rules at a glance" summary a reader can stop after; full contracts and rationale below it.
  • Rationale and refuted alternatives stay in the ADR — they are what stops re-litigating settled ideas — but below the fold.
  • Process history is deleted once complete, not archived in-file. Migration plans, per-step landing tables, and point-in-time status change-logs go to git history; the Status section keeps one line saying so plus any accepted, still-relevant waiver or evidence gap.
  • Once a rule is gate-enforced, the ADR keeps the why and points at the gate rather than restating the rule's details.