forked from unom/punktfunk
A Windows exclusive-topology eviction makes the host rebuild its capture ring and encoder in place. It takes a few hundred milliseconds and is entirely host-local: no packet is lost, the link never changes. But the client's bitrate controller decides on 750 ms report windows, and a window straddling that rebuild sees almost no stream. The 0.29 field log: 401 ms of rebuild produced a window reporting actual_kbps=390 against a 20 000 target with loss_ppm=0, which the controller read as congestion — x0.7 and slow start retired, three minutes at ~15 Mbps on a link that never dropped a packet. The client already knows how to throw a window away. `discard_abr_window` feeds the controller nothing, sends no LossReport (so a bogus window cannot spike the host's adaptive FEC) and closes the standing-latency detector as not-loss-free. It had exactly one cause: the tail of the client's own speed test. This adds the second, and it is the one party that actually knows — the host. `PipelineGap` (0x0A, host->client, 9 bytes) carries the rebuild's measured span. A DURATION, never an instant: host and client clocks are not in the same domain — 14.7 s apart in that same log — so an instant would need skew-correcting before it meant anything. The client anchors the gap to its own receive time and gap_ms is evidence for the log rather than an input to arithmetic. The span is read from the transition trace's own total, so the number the client logs is the total_ms on the host's trace line: one number, checkable from either end of a field report. Announced on both in-place rebuild paths, including the mode switch. The mode switch is NOT already covered by the client's `on_mode_switch` reset: that clears the learned caps, the three latency baselines and the proven mark, which does mute OWD/decode/encode for a few windows — but it touches neither `current_kbps`, `probing` nor `bad_windows`, and every signal that needs no baseline (an unrecoverable frame, a flush, heavy loss over a near-empty denominator, a keyframe-ask storm) still scores the straddling window, where one severe verdict costs the same x0.7 plus slow start. Deliberate limitation: only the window in flight is discarded. A rebuild long enough to straddle a window boundary damaged the previous window too, and that one is already decided. Retracting it would mean holding every window back by a window in case a gap follows — trading a rare over-reaction for a permanent one. Additive: no ABI bump, no wire break. 0x0A extends the contiguous 0x01-0x09 rate-control block its only consumer already lives in, and is not in the 0x30 clock block precisely because no clock domain is involved. A client that predates it hits its "unknown control message" arm and keeps today's behaviour; a host that predates it never sends one. Tests: wire round trip including cross-decode against the three identically-shaped rate-control messages either side of it (the type byte is the only thing keeping a gap from re-decoding as a SetBitrate), and an end-to-end pump test driving a real ControlTask over a real QUIC control stream into a real DataPump, asserting the straddling window produces no request and the next one reports normally. 464 core tests green, fmt and clippy clean. Not verified here: the Windows-gated eviction call site compiles on no platform available locally (aws-lc-sys needs windows.h to cross-compile). Type-checked by reading; owed a Windows runner build. Known gaps, stated rather than papered over: a mode-switch rebuild that FAILS keeps streaming the old mode and leaves its stall unannounced, and three other in-place rebuild sites are still silent — the Gaming/Desktop session switch, the ABR re-target's fallback open_video rebuild (~0.6 s, and self-inflicted: the controller causing the stall its next window reads as congestion), and reset_stalled_encoder.