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)#1386thymikee wants to merge 5 commits into
Conversation
…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.
Size Report
Startup median (7 runs, lower is better):
Top changed chunks:
|
|
Reviewed (I filed #1385 and wrote the sibling wait-side fix in #1381, so this is a review from the adjacent trench). The shape is right: bounded retry at exactly the one call site that races a launch, opt-in so the post-failure and guard-mismatch diagnostic captures keep their immediate-failure semantics, ADR amendment matches the code (delay-list sum 11.8s coheres with the 12s deadline), and the two tests cover both the recover-and-dispatch and exhausted-fail-closed branches. Four findings, one substantive: 1. The retry classification is too broad — it retries permanent failures, not just launch races. 2. The 3. Naming nit: 4. Merge coordination with #1381: it restructures 🤖 Addressed by Claude Code |
…nism 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.
|
Addressed in 87c6fbf: 1. Retry classification too broad — fixed. The retry loop now only retries when the underlying failure is a content-quality verdict, not a mechanism failure: the non-throwing 2. Per-file sleep mock / other suites real-sleeping — confirmed dissolved. As you predicted, gating on (1) means a generic 3. Naming nit — kept 4. Merge coordination with #1381 — noted, no code change here since #1381 hasn't landed; the placement (opt-in only at Also updated the ADR 0012 amendment to describe the retriable-vs-permanent gating. 🤖 Addressed by Claude Code |
|
Reviewed exact head 87c6fbf. Not ready. P1 — retriable is not a content-quality discriminator. The retry gate accepts any normalizeError(error).retriable === true. Android's helper wrapper deliberately re-lifts retriable from ADB mechanism failures (for example connection_dropped), so those failures consume the launch-race retry sequence despite the ADR saying mechanism failures fail fast. Add a narrow shared content-verdict taxonomy and a realistic retriable mechanism-error regression; do not reuse #1381's current broad androidSnapshotHelperFailureReason check. P2 — the documented 12s wall-clock cap is not strict. The deadline starts after the first capture, and after sleeping to the deadline the loop launches another unbounded capture. Bound capture by remaining budget or document/test a delay-only budget. Finally, exact heads #1386 and #1381 conflict in the ADR, target-verification code, and tests; reconcile their shared taxonomy/placement before merge. Completed checks are green; Linux smoke remains pending. No ready label. |
…nchor 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.
|
Addressed in 3b0e4db: P1 — P2 — the 12s cap wasn't strictly wall-clock, and the loop still launches an unbounded capture after the last sleep — addressed as a delay-only budget. Anchored the deadline before the first attempt (was computed after), so a slow first capture now eats into the same 12s rather than getting a free window on top. I did not add a per-capture timeout — that would mean threading a timeout/abort signal into #1381 merge coordination — unchanged, still just noted; no code change here since it hasn't landed. Also rewrote the ADR 0012 amendment to match the corrected taxonomy and the delay-only-budget wording. 🤖 Addressed by Claude Code |
|
Re-reviewed exact head P2 — reconcile the runtime taxonomy with sibling #1381. Readiness blocker — attach direct live validation. This changes device-facing replay behavior, but the PR reports only unit/static checks. Provide an exact annotated All current checks are green. |
|
Merged main (#1381 landed) and resolved conflicts in commit c24611e:
Full unit suite green (108/108 on the directly affected replay/target suites, no new slow-test violations), 🤖 Addressed by Claude Code |
|
Re-reviewed exact head c24611e. The shared #1381 taxonomy is now used, the wait/path-1 placement is correct, and the content-quality recovery, exhaustion, and retriable-ADB-mechanism fast-fail regressions are non-vacuous. All current checks are green. One readiness blocker remains: attach direct device evidence for an annotated |
|
Attempted the requested direct live validation. Here's exactly what I did and what I found — I want to be upfront that it's a partial result, not a clean confirmation. Setup: booted Recording a real annotated script: used Replaying it (~25 attempts across both platforms, with What I could not force: the specific I reverted all the test-app instrumentation (nothing in this diff) and cleaned up: closed both sessions, killed the daemons, stopped Metro, force-stopped/terminated the app on both devices. Given I couldn't force the exact condition live within reasonable effort, the strongest evidence I can offer that the retry gate is correct (not just that a race exists) is that the unit regressions use the real production error shapes verified against source — 🤖 Addressed by Claude Code |
* origin/main: docs(adr): rules-first ADR restructure + ADR 0017 proposal (unified event journal) (#1399) feat: add first-class Vega VVD TV support (#1396) fix(replay): preserve cwd scope for opened sessions (#1401) docs: restructure AGENTS.md and CONTEXT.md for progressive disclosure (#1402) fix(cli): compact stale device status (#1388) feat: add WebView accessibility lab (#1397) feat: parameterize sensitive recorded inputs (#1369) fix(daemon): keep an active replay session's daemon alive over the CLI path (#1390) # Conflicts: # docs/adr/0012-interactive-replay.md
|
Merged main again and resolved conflicts in ea527a8. The only real conflict was in ADR 0012: main landed #1399's "rules-first" restructure (condensed the old per-PR bullet list under Status into a concise numbered "Rules at a glance" summary, full detail moved under Decision). Adapted #1385's entry to the new format — added it to the Status amendment list, extended decision 3's numbered rule with a one-clause summary, and kept the full Everything else auto-merged cleanly (a large batch landed: #1401 cwd-scope fix, TV/Vega platform support, parameterized recorded inputs, doctor toolchain work, test-app additions). 🤖 Addressed by Claude Code |
|
Re-review at |
|
Ran a second, more targeted round of live attempts — still could not force New technique 1 — WebView mid-mount race (iOS). Recorded a real 4-step script via New technique 2 — heavier in-app navigation race (Android). Recorded: Across both rounds (~35 live replay attempts total, three different window-widening techniques, two navigation patterns, two platforms, deliberately spiked CPU contention) the outcome is 100% consistent: the launch/navigation race is real and reproducible, but it always resolves as a legitimate Given three rounds without success, could you either share the specific device/OS/app conditions from your original live reproduction (so I can try to match them), or make the call on whether the unit-level evidence (real production error shapes, verified against source) plus this now-extensive documented live-testing effort is sufficient to accept the residual risk? I don't want to keep re-spending review cycles on attempts that keep landing in the same place without new information. Cleaned up again: closed all sessions, killed daemons, stopped Metro, force-stopped/terminated the app on both devices. No diff left in the repo (git status clean). 🤖 Addressed by Claude Code |
Summary
Fixes #1385: a step right after
open --relaunch— e.g. a step-2press— can have its pre-dispatch target-verification capture land while the app is still launching/mounting. That produces a transientcapture-failed(Android's snapshot helper reports "insufficient foreground app content" mid-mount) orsparse-snapshot(iOS's private-AX fallback under load) verdict, which is not a real divergence, but the step still fails closed (identity-unverifiable) before ever dispatching.captureDivergenceObservation(session-replay-divergence.ts) now takes an opt-inretryLaunchRaceflag. When set, an unavailable capture retries with a bounded backoff (fixed delay list, 12s wall-clock deadline) before giving up — mirroringwait's keep-polling landmark-identity semantics (Design read-only identity verification without breaking wait polling #1349) on this pre-dispatch path, still failing closed once the deadline passes.verifyReplayActionTarget's pre-dispatch verification capture (session-replay-target-verification.ts) opts in. The post-failure diagnostic capture (buildReplayFailureDivergence) and the post-resolution guard-mismatch capture stay single-shot, since those follow an already-real failure — and a number of existing unit tests deliberately stub a throwingsnapshotdispatch there expecting an immediate result (repo convention: unit tests must not wait real time). Retrying unconditionally would have forced real wall-clock waits onto every one of those; I verified this by running the full suite before scoping the flag down to just the pre-dispatch call site.Test plan
session-replay-target-verification-runtime.test.ts: a transient capture failure that recovers within the retry window and still dispatches the action, and a persistent failure that still fails closed asidentity-unverifiableonce the bounded retry is exhausted.tsc --noEmitclean.oxlint --deny-warningsclean.