Retires the upstream silent-skip defect (bisected to commit 555eb05 —
imported broken from before Phase 1, never on the original 22-defect
audit because it needed a real failure mode to surface). Per
.planning/debug/empty-archive-port-race.md Option C step 4: even with
the architectural port lifecycle now bullet-proof, a hard outer timeout
on the BUFFER fetch (10 s after every retry) must result in an
operator-VISIBLE failure — not a silent video-less archive.
1. **EmptyVideoBufferError** — typed error class with a stable `code`
field ('empty-video-buffer') matching the offscreen-side
CaptureErrorCode union vocabulary. Lets saveArchive's catch
distinguish "no segments" failure from JSZip/manifest failures.
2. **createArchive throws** — when videoBufferResponse.segments.length
=== 0 OR when the merged blob is zero bytes, throw the typed error
with a detail string for diagnostics. Replaces the silent-skip
branch that was the bisect-confirmed transport for the H2 class.
3. **saveArchive broadcast** — on EmptyVideoBufferError, emit
{type:'RECORDING_ERROR', error:'empty-video-buffer'} via
chrome.runtime.sendMessage. The popup's existing RECORDING_ERROR
handler surfaces the failure to the operator (same channel as
codec-unsupported, user-cancelled, etc.). saveArchive still returns
{success:false, error} so the popup's direct-response path also
sees the failure (defense-in-depth via two channels).
Status: 52 GREEN / 52 tests passing. All 12 RED tests from the Option C
gate (3 in port-reconnect-race + 4 in port-health-probe + 5 in
request-id-protocol) are now GREEN. Build clean (npm run build exit 0).
Pinning contracts intact:
- D-12 port-serialization (base64 wire format): GREEN
- D-13 segment-rotation (3 x 10 s restart-segments ring): GREEN
- A3 webm-playback (ffmpeg dry-run on fixture): GREEN
tsc --noEmit exit 0; type-safety grep clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>