fix(ios): regain healthy timer leeway after the fast foreground beat
Summary
Answers the one unresolved non-outdated thread on #21406 ("Restore healthy leeway after the fast foreground beat"): appBecameActive arms first: 1s, repeating: 4s, and a DispatchSourceTimer records a single leeway for the whole schedule — 50 ms for the fast first fire. Because progressIntervalArmed was recorded as 4 s, the tick handler's desired == armed check never retargeted, so every later healthy beat for the rest of the session kept the 50 ms coalescing tolerance the fast beat needed.
The pending fast first fire is now tracked explicitly (progressFastFirePending). After it emits, the timer retargets to the armed repeating cadence, regaining the 500 ms healthy leeway. If the capture state changed while the beat was pending, the retarget defers to the existing desired-cadence branch instead, preserving the fault-path behaviors pinned by the #21406 review (a preserved fast fire through recovery, a second fault before the fire).
cancelProgressTimer clears the flag with the rest of the timer state.
Product invariants
- None of the indexed invariants are triggered by this path (no uplink pause, no capture-policy change: the tick still fires at the same deadlines; only the kernel coalescing tolerance after the first fast beat changes). The batch liveness contract is unchanged: healthy 4 s beat under Dart's 10 s stall deadline, one fast beat on foreground return.
Failure class
Failure-Class: none
Test plan
- Behavior reasoning against
syncProgressTimer/applyProgressCadencestate machine; flag lifecycle covers arm/retarget/cancel - Pre-push gate (shared PR contract, hygiene, supplied PR body)
- Full CI green on this exact head (iOS build lanes), then independent review before merge