…ization, and dead-pid recovery
Three live-reproduced defects on a loaded Pixel_7_CI emulator shared the
"pulled file is not a playable MP4" / "manifest could not be verified"
symptom family:
- The Swift video validator required duration > 0, permanently rejecting
valid single-frame recordings of fully static screens (AVFoundation
reports their duration as 0). Whether a run passed depended on whether
anything — even the status-bar clock — changed during the window.
- screenrecord finalizes by patching a front-reserved moov in place, so
the remote file size never changes; the copy path now re-pulls with
escalating delays (750/1500/3000ms) and detects finalization from the
pulled bytes via a mandatory ftyp+moov container sniff, which also
preserves the truncation detection the duration check provided by
accident. The local waitForStableFile call is gone: a pull is complete
when adb returns.
- toybox `ps -p <missing-pid>` exits 1 with empty output — the normal
pid-gone signature — but the recovery liveness probe read every
non-zero exit as an uncertain adb failure, making a finished recording
behind a live-status manifest unrecoverable forever. Empty-output
failures now corroborate via the full process list: healthy listing
with the pid absent recovers the finished recording; listing failure
stays conservatively uncertain (a stale verdict deletes the manifest,
so transport health is proven first). Transport failures keep stderr
and exec-layer timeouts throw, which is what makes the empty-output
signature safe to trust.
Provider-scenario coverage: in-place same-size finalization landing past
the first retry, and dead-pid recovery on a responsive device — both
fail on the previous implementation with the live-observed errors.
Symptom
record stopon Android fails withfailed to copy recording chunk 1: … pulled file is not a playable MP4(reproduced 2/2 on a loaded Pixel_7_CI emulator), and separately, once a daemon dies mid-recording and screenrecord exits, every retry fails forever withactive Android recording manifest could not be verified.Root causes (all live-verified on Pixel_7_CI, 2026-08-06)
1. The video validator rejected valid recordings (the dominant failure). An 8s recording of a fully static screen legitimately contains a single frame — screenrecord only encodes on screen updates — and AVFoundation reports its duration as 0. The Swift validator's
CMTimeGetSeconds(duration) > 0check rejected these complete, ffprobe-valid files every time, so no retry budget could help. Whether a run passed depended on whether anything (even the status-bar clock minute tick) changed during the window — which made it look like a load-dependent race.2. Finalization is invisible to size-based gates. The remote file layout is
[ftyp][free→moov][mdat]: screenrecord pre-reserves moov space and patches it in place on stop, so the file size never changes when the recording becomes playable. Early pulls (the genuine 1–3s race under load) have afreeplaceholder where the moov belongs. Theduration > 0check was also accidentally load-bearing here: AVFoundation reports a moov-first file with a truncated mdat as playable, so relaxing duration alone would have lost truncation detection.3. Recovery misread the pid-gone signature. toybox
ps -o pid=,args= -p <missing-pid>exits 1 with completely empty output (verified:-p 99999→ exit 1 empty;-p 1→ exit 0 with init line). The recovery liveness probe treated every non-zero exit as "uncertain adb failure", so a finished recording behind alive-status manifest — finalized MP4 and manifest sitting on /sdcard — was unrecoverable forever.Fixes
src/utils/video.ts: drop theduration > 0requirement (single-frame recordings are valid) and make the ftyp+moov container sniff a mandatory precondition inisPlayableVideo— it is the finalization oracle for in-place-patched captures and restores truncation detection honestly.src/daemon/handlers/record-trace-android-copy.ts: replace the single 750ms retry with escalating re-pull delays (750/1500/3000ms — outlasts the observed finalization window with margin); drop the localwaitForStableFilecall (a pull is complete when adb returns; this also removes ~500ms of dead wait from every Android stop). No remote stat polling: size cannot signal an in-place moov patch.src/daemon/handlers/record-trace-android-recovery.ts: a probe failure with empty stdout+stderr corroborates via the full process list (ps -A, reusing the pending-path helper): healthy listing with the pid absent → recover as finished and pull; listing failure → stay conservatively uncertain. The corroboration is a safety requirement, not politeness — astaleverdict deletes the manifest, so transport health must be proven first. Genuine transport failures keep their conservative path: they leave stderr, and exec-layer timeouts throw before this branch runs.Tests
Two new provider scenarios, both verified to fail on the previous implementation with the live-observed errors:
free→moovcontent flip landing only after the second pull (inside the observed window, past the old 2-attempt budget).ps -pexit-1-empty, responsive device, finalized remote file → stop recovers, pulls once, cleans the manifest.The existing guard tests are untouched and still pin the conservative behavior: the stale-manifest flow never enters the new branch (its probe exits 0) and still never calls
ps -A; the uncertain-probe fixture uses non-empty stderr and stays uncertain.Live validation
open → record start → 8s idle → record stopcycles under 5-core CPU load pass, including one producing a duration-0 file (ffprobeduration N/A) — the class that failed 100% before.kill -2screenrecord manually): the nextrecord stoprecovers the manifest on the first try and pulls a playable 1080x2400 MP4 with the expected "no longer running / may be truncated" warning.Full Android recording scenario suite 23/23, recording unit suites green, lint/typecheck/layering/format green.
Follow-up (not in this PR)
The 3232-byte "moov atom not found" manual pull from the original report is real but only reachable when the process-exit gate is bypassed — through the pipeline,
waitForAndroidStopExitruns first; the escalating retries cover the residual window wherepslies under load.