Skip to main content

blit_server/
lib.rs

1use blit_alacritty::{SearchResult as AlacrittySearchResult, TerminalDriver as AlacrittyDriver};
2use blit_remote::{
3    C2S_ACK, C2S_CLIENT_METRICS, C2S_CLOSE, C2S_COPY_RANGE, C2S_CREATE, C2S_CREATE_AT,
4    C2S_CREATE_N, C2S_CREATE2, C2S_DISPLAY_RATE, C2S_FOCUS, C2S_INPUT, C2S_KILL, C2S_MOUSE,
5    C2S_READ, C2S_RESIZE, C2S_RESTART, C2S_SCROLL, C2S_SEARCH, C2S_SUBSCRIBE, C2S_UNSUBSCRIBE,
6    CREATE2_HAS_COMMAND, CREATE2_HAS_SRC_PTY, FEATURE_COPY_RANGE, FEATURE_CREATE_NONCE,
7    FEATURE_RESIZE_BATCH, FEATURE_RESTART, FrameState, READ_ANSI, READ_TAIL, S2C_CLOSED,
8    S2C_CREATED, S2C_CREATED_N, S2C_LIST, S2C_READY, S2C_SEARCH_RESULTS, S2C_TEXT, S2C_TITLE,
9    build_update_msg, msg_hello,
10};
11use std::collections::{HashMap, HashSet, VecDeque};
12use std::sync::Arc;
13use std::time::{Duration, Instant};
14use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
15use tokio::sync::{Mutex, Notify, mpsc};
16
17mod ipc;
18mod pty;
19
20pub use ipc::{IpcListener, default_ipc_path};
21use pty::{PtyHandle, PtyWriteTarget};
22
23type PtyFds = Arc<std::sync::RwLock<HashMap<u16, PtyWriteTarget>>>;
24pub struct Config {
25    pub shell: String,
26    pub shell_flags: String,
27    pub scrollback: usize,
28    pub ipc_path: String,
29    #[cfg(unix)]
30    pub fd_channel: Option<std::os::unix::io::RawFd>,
31    pub verbose: bool,
32}
33
34trait PtyDriver: Send {
35    fn size(&self) -> (u16, u16);
36    fn resize(&mut self, rows: u16, cols: u16);
37    fn process(&mut self, data: &[u8]);
38    fn title(&self) -> &str;
39    fn search_result(&self, query: &str) -> Option<PtySearchResult>;
40    fn take_title_dirty(&mut self) -> bool;
41    fn cursor_position(&self) -> (u16, u16);
42    fn synced_output(&self) -> bool;
43    fn snapshot(&mut self, echo: bool, icanon: bool) -> FrameState;
44    fn scrollback_frame(&mut self, offset: usize) -> FrameState;
45    fn reset_modes(&mut self);
46    fn mouse_event(
47        &self,
48        type_: u8,
49        button: u8,
50        col: u16,
51        row: u16,
52        echo: bool,
53        icanon: bool,
54    ) -> Option<Vec<u8>>;
55    fn get_text_range(
56        &self,
57        start_tail: u32,
58        start_col: u16,
59        end_tail: u32,
60        end_col: u16,
61    ) -> String;
62    fn total_lines(&self) -> u32;
63}
64
65struct PtySearchResult {
66    score: u32,
67    primary_source: u8,
68    matched_sources: u8,
69    context: String,
70    scroll_offset: Option<usize>,
71}
72
73impl PtyDriver for AlacrittyDriver {
74    fn size(&self) -> (u16, u16) {
75        AlacrittyDriver::size(self)
76    }
77
78    fn resize(&mut self, rows: u16, cols: u16) {
79        AlacrittyDriver::resize(self, rows, cols);
80    }
81
82    fn process(&mut self, data: &[u8]) {
83        AlacrittyDriver::process(self, data);
84    }
85
86    fn title(&self) -> &str {
87        AlacrittyDriver::title(self)
88    }
89
90    fn search_result(&self, query: &str) -> Option<PtySearchResult> {
91        AlacrittyDriver::search_result(self, query).map(|result: AlacrittySearchResult| {
92            PtySearchResult {
93                score: result.score,
94                primary_source: result.primary_source as u8,
95                matched_sources: result.matched_sources,
96                context: result.context,
97                scroll_offset: result.scroll_offset,
98            }
99        })
100    }
101
102    fn take_title_dirty(&mut self) -> bool {
103        AlacrittyDriver::take_title_dirty(self)
104    }
105
106    fn cursor_position(&self) -> (u16, u16) {
107        AlacrittyDriver::cursor_position(self)
108    }
109
110    fn synced_output(&self) -> bool {
111        AlacrittyDriver::synced_output(self)
112    }
113
114    fn snapshot(&mut self, echo: bool, icanon: bool) -> FrameState {
115        AlacrittyDriver::snapshot(self, echo, icanon)
116    }
117
118    fn scrollback_frame(&mut self, offset: usize) -> FrameState {
119        AlacrittyDriver::scrollback_frame(self, offset)
120    }
121
122    fn reset_modes(&mut self) {
123        AlacrittyDriver::reset_modes(self);
124    }
125
126    fn mouse_event(
127        &self,
128        type_: u8,
129        button: u8,
130        col: u16,
131        row: u16,
132        echo: bool,
133        icanon: bool,
134    ) -> Option<Vec<u8>> {
135        AlacrittyDriver::mouse_event(self, type_, button, col, row, echo, icanon)
136    }
137
138    fn get_text_range(
139        &self,
140        start_tail: u32,
141        start_col: u16,
142        end_tail: u32,
143        end_col: u16,
144    ) -> String {
145        AlacrittyDriver::get_text_range(self, start_tail, start_col, end_tail, end_col)
146    }
147
148    fn total_lines(&self) -> u32 {
149        AlacrittyDriver::total_lines(self)
150    }
151}
152
153// Keep small to limit bufferbloat on slow connections.  The soft queue limit
154// (OUTBOX_SOFT_QUEUE_LIMIT_FRAMES) prevents the tick from queuing more than
155// ~2 frames, so this just needs to be bigger than that with some headroom.
156const OUTBOX_CAPACITY: usize = 8;
157const OUTBOX_SOFT_QUEUE_LIMIT_FRAMES: usize = 2;
158const PREVIEW_FRAME_RESERVE: usize = 1;
159const READY_FRAME_QUEUE_CAP: usize = 4;
160const PTY_CHANNEL_CAPACITY: usize = 64;
161const SYNC_OUTPUT_END: &[u8] = b"\x1b[?2026l";
162
163/// A chunk of data from the PTY reader, sent through a lock-free channel
164/// so the reader never contends with the delivery tick for the Session mutex.
165enum PtyInput {
166    /// Raw bytes from the PTY, with the reader's sync-scan tail for boundary
167    /// detection. The tick task calls `process()` + `respond_to_queries()`.
168    Data(Vec<u8>),
169    /// Data up to a sync-output-close boundary. `before` should be processed
170    /// and then a snapshot taken. `after` is remainder for the next chunk.
171    SyncBoundary { before: Vec<u8>, after: Vec<u8> },
172    /// The PTY fd hit EOF or an error — the child likely exited.
173    Eof,
174}
175
176const MAX_FRAME_SIZE: usize = 16 * 1024 * 1024;
177
178async fn read_frame(reader: &mut (impl AsyncRead + Unpin)) -> Option<Vec<u8>> {
179    let mut len_buf = [0u8; 4];
180    reader.read_exact(&mut len_buf).await.ok()?;
181    let len = u32::from_le_bytes(len_buf) as usize;
182    if len == 0 {
183        return Some(vec![]);
184    }
185    if len > MAX_FRAME_SIZE {
186        return None;
187    }
188    let mut buf = vec![0u8; len];
189    reader.read_exact(&mut buf).await.ok()?;
190    Some(buf)
191}
192
193async fn write_frame(writer: &mut (impl AsyncWrite + Unpin), payload: &[u8]) -> bool {
194    if payload.len() > u32::MAX as usize {
195        return false;
196    }
197    let len = payload.len() as u32;
198    let mut buf = Vec::with_capacity(4 + payload.len());
199    buf.extend_from_slice(&len.to_le_bytes());
200    buf.extend_from_slice(payload);
201    writer.write_all(&buf).await.is_ok()
202}
203
204struct Pty {
205    handle: PtyHandle,
206    driver: Box<dyn PtyDriver>,
207    /// Client-chosen tag set at creation time.
208    tag: String,
209    dirty: bool,
210    ready_frames: VecDeque<FrameState>,
211    /// Receives raw byte chunks from the PTY reader task without mutex contention.
212    byte_rx: mpsc::Receiver<PtyInput>,
213    reader_handle: std::thread::JoinHandle<()>,
214    /// Cached (echo, icanon) from tcgetattr; refreshed every ~250ms.
215    lflag_cache: (bool, bool),
216    lflag_last: Instant,
217    /// When we last broadcast a title update for this PTY.
218    last_title_send: Instant,
219    /// Title changed but not yet sent (debounced).
220    title_pending: bool,
221    /// The subprocess has exited but the terminal state is retained for reading.
222    exited: bool,
223    /// Exit status: WEXITSTATUS if normal exit, negative signal number if signalled,
224    /// EXIT_STATUS_UNKNOWN if not yet collected.
225    exit_status: i32,
226    /// Command used to create this PTY (None = default shell).
227    command: Option<String>,
228}
229
230impl Pty {
231    fn mark_dirty(&mut self) {
232        self.dirty = true;
233    }
234
235    fn clear_dirty(&mut self) {
236        self.dirty = false;
237    }
238}
239
240struct ClientState {
241    tx: mpsc::Sender<Vec<u8>>,
242    lead: Option<u16>,
243    subscriptions: HashSet<u16>,
244    view_sizes: HashMap<u16, (u16, u16)>,
245    scroll_offsets: HashMap<u16, usize>,
246    scroll_caches: HashMap<u16, FrameState>,
247    last_sent: HashMap<u16, FrameState>,
248    preview_next_send_at: HashMap<u16, Instant>,
249    /// EWMA RTT estimate in milliseconds.
250    rtt_ms: f32,
251    /// Minimum-path RTT estimate in milliseconds, excluding queue growth.
252    min_rtt_ms: f32,
253    /// Client's measured display refresh rate (fps), reported via C2S_DISPLAY_RATE.
254    display_fps: f32,
255    /// EWMA of delivered payload rate in bytes/sec.
256    delivery_bps: f32,
257    /// EWMA of actual ACKed goodput in bytes/sec, based on ACK cadence rather than RTT.
258    goodput_bps: f32,
259    /// EWMA of absolute goodput sample-to-sample jitter in bytes/sec.
260    goodput_jitter_bps: f32,
261    /// Decaying peak goodput jitter in bytes/sec.
262    max_goodput_jitter_bps: f32,
263    /// Last sampled ACK goodput for jitter estimation.
264    last_goodput_sample_bps: f32,
265    /// EWMA of acknowledged frame payload size in bytes.
266    avg_frame_bytes: f32,
267    /// EWMA of acknowledged lead/paced frame payload size in bytes.
268    avg_paced_frame_bytes: f32,
269    /// EWMA of acknowledged preview/unpaced frame payload size in bytes.
270    avg_preview_frame_bytes: f32,
271    /// Payload bytes currently in flight (sent, not yet ACKed).
272    inflight_bytes: usize,
273    /// Oldest in-flight frame first; ACKs arrive in order.
274    inflight_frames: VecDeque<InFlightFrame>,
275    /// Earliest time the next visual update should be sent for smooth pacing.
276    next_send_at: Instant,
277    /// Temporary additive window growth used to probe for more throughput after
278    /// a conservative backoff. Decays when queue delay grows.
279    probe_frames: f32,
280    /// Diagnostics.
281    frames_sent: u32,
282    acks_recv: u32,
283    acked_bytes_since_log: usize,
284    browser_backlog_frames: u16,
285    browser_ack_ahead_frames: u16,
286    browser_apply_ms: f32,
287    last_metrics_update: Instant,
288    last_log: Instant,
289    goodput_window_bytes: usize,
290    goodput_window_start: Instant,
291}
292
293struct InFlightFrame {
294    sent_at: Instant,
295    bytes: usize,
296    paced: bool,
297}
298
299/// Frames to keep in flight: enough to cover one RTT at the client's reported
300/// display rate. High-latency links need many frames in flight to avoid
301/// devolving into stop-and-wait.
302fn frame_window(rtt_ms: f32, display_fps: f32) -> usize {
303    let frame_ms = 1_000.0 / display_fps.max(1.0);
304    let base_frames = (rtt_ms / frame_ms).ceil().max(0.0) as usize;
305    let slack_frames = ((base_frames as f32) * 0.125).ceil() as usize + 2;
306    base_frames.saturating_add(slack_frames).max(2)
307}
308
309fn path_rtt_ms(client: &ClientState) -> f32 {
310    if client.min_rtt_ms > 0.0 {
311        client.min_rtt_ms
312    } else {
313        client.rtt_ms
314    }
315}
316
317fn display_need_bps(client: &ClientState) -> f32 {
318    client.avg_paced_frame_bytes.max(256.0) * client.display_fps.max(1.0)
319}
320
321fn effective_rtt_ms(client: &ClientState) -> f32 {
322    let path_rtt = path_rtt_ms(client);
323    let frame_ms = 1_000.0 / browser_pacing_fps(client).max(1.0);
324    let queue_allowance = frame_ms
325        * if throughput_limited(client) {
326            4.0
327        } else {
328            12.0
329        };
330    client.rtt_ms.clamp(path_rtt, path_rtt + queue_allowance)
331}
332
333fn window_rtt_ms(client: &ClientState) -> f32 {
334    let effective = effective_rtt_ms(client);
335    if !throughput_limited(client) {
336        effective
337    } else {
338        client.rtt_ms.clamp(effective, effective * 2.0)
339    }
340}
341
342fn target_frame_window(client: &ClientState) -> usize {
343    let window_fps = if throughput_limited(client) {
344        pacing_fps(client)
345    } else {
346        browser_pacing_fps(client)
347    };
348    frame_window(window_rtt_ms(client), window_fps)
349        .saturating_add(client.probe_frames.round().max(0.0) as usize)
350}
351
352fn base_queue_ms(client: &ClientState) -> f32 {
353    let frame_ms = 1_000.0 / browser_pacing_fps(client).max(1.0);
354    frame_ms * if throughput_limited(client) { 2.0 } else { 8.0 }
355}
356
357fn target_queue_ms(client: &ClientState) -> f32 {
358    let frame_ms = 1_000.0 / browser_pacing_fps(client).max(1.0);
359    let probe_scale = if throughput_limited(client) {
360        0.25
361    } else {
362        1.0
363    };
364    base_queue_ms(client) + client.probe_frames.max(0.0) * frame_ms * probe_scale
365}
366
367fn browser_ready(client: &ClientState) -> bool {
368    client.browser_ack_ahead_frames <= 1
369        && client.browser_apply_ms <= 1.0
370        && !outbox_backpressured(client)
371}
372
373fn bandwidth_floor_bps(client: &ClientState) -> f32 {
374    let browser_ready = browser_ready(client);
375    let backlog_scale = match client.browser_backlog_frames {
376        0..=2 => 0.9,
377        3..=8 => 0.8,
378        _ => 0.65,
379    };
380    let penalty = client
381        .goodput_jitter_bps
382        .max(client.max_goodput_jitter_bps * 0.5)
383        .min(client.goodput_bps * if browser_ready { 0.75 } else { 0.9 });
384    let goodput_floor = (client.goodput_bps - penalty)
385        .max(client.goodput_bps * if browser_ready { 0.35 } else { 0.2 });
386    // On a browser-ready path, the per-frame delivery estimate is already
387    // end-to-end and reacts much faster than ACK-window goodput. Halving it
388    // leaves large-frame local links chronically underpaced.
389    let delivery_floor = client.delivery_bps * if browser_ready { 1.0 } else { 0.5 };
390    let recent_sample_floor = if browser_ready && client.last_goodput_sample_bps > 0.0 {
391        client.last_goodput_sample_bps * backlog_scale
392    } else {
393        0.0
394    };
395    goodput_floor.max(recent_sample_floor).max(delivery_floor)
396}
397
398fn pacing_fps(client: &ClientState) -> f32 {
399    let frame_bytes = client.avg_paced_frame_bytes.max(256.0);
400    let sustainable = bandwidth_floor_bps(client) / frame_bytes;
401    sustainable.min(browser_pacing_fps(client))
402}
403
404fn throughput_limited(client: &ClientState) -> bool {
405    let floor = bandwidth_floor_bps(client);
406    // Consider total demand: lead at cadence rate plus previews at their cap.
407    // The old check (pacing_fps < cadence * 0.9) only saw lead bandwidth,
408    // which is often tiny, so previews could starve the lead undetected.
409    let lead_bps = client.avg_paced_frame_bytes.max(256.0) * browser_pacing_fps(client);
410    let preview_bps = client.avg_preview_frame_bytes.max(256.0) * client.display_fps.max(1.0);
411    (lead_bps + preview_bps) > floor * 0.9
412}
413
414fn browser_pacing_fps(client: &ClientState) -> f32 {
415    let mut fps = client.display_fps.max(1.0);
416
417    // Backlog and ack-ahead are direct signals from the browser about
418    // whether it's keeping up.  No predictive apply-time bound — it
419    // consistently underestimates capacity and causes 30fps death spirals.
420    let backlog = client.browser_backlog_frames as f32;
421    if backlog > 4.0 {
422        fps = fps.min(fps * (4.0 / backlog));
423    }
424
425    if client.browser_ack_ahead_frames > 4 {
426        fps = fps.min(client.display_fps.max(1.0) * 0.5);
427    }
428
429    fps.max(1.0)
430}
431
432fn browser_backlog_blocked(client: &ClientState) -> bool {
433    client.browser_backlog_frames > 8
434}
435
436fn byte_budget_for(client: &ClientState, budget_ms: f32) -> usize {
437    let budget_bps = if throughput_limited(client) {
438        bandwidth_floor_bps(client)
439    } else {
440        client.goodput_bps.max(bandwidth_floor_bps(client))
441    };
442    let bytes = budget_bps * budget_ms.max(1.0) / 1_000.0;
443    bytes.ceil().max(client.avg_frame_bytes.max(256.0)) as usize
444}
445
446fn target_byte_window(client: &ClientState) -> usize {
447    let budget = byte_budget_for(client, path_rtt_ms(client) + target_queue_ms(client));
448    let frame_bytes = client.avg_paced_frame_bytes.max(256.0).ceil() as usize;
449    let target_frames = target_frame_window(client);
450    let pipeline_bytes = frame_bytes.saturating_mul(target_frames);
451    // For small pipelines (e.g. idle terminals with 1KB frames), allow the
452    // full frame window worth of bytes so we pipeline across the RTT instead
453    // of stop-and-wait.  For large pipelines (e.g. 50KB frames × 5 frames =
454    // 250KB), the budget (BDP-based) is the binding constraint; fall back to
455    // a one-frame floor so we don't pile up many RTTs worth of large frames.
456    const PIPELINE_FLOOR_LIMIT: usize = 32_768; // 32 KB
457    let floor = if pipeline_bytes <= PIPELINE_FLOOR_LIMIT {
458        pipeline_bytes
459    } else {
460        frame_bytes // one-frame floor for large pipelines
461    };
462    budget.max(floor)
463}
464
465fn send_interval(client: &ClientState) -> Duration {
466    Duration::from_secs_f64(1.0 / browser_pacing_fps(client).max(1.0) as f64)
467}
468
469fn preview_fps(client: &ClientState) -> f32 {
470    let mut fps = client.display_fps.max(1.0);
471    if client.lead.is_some() {
472        // Always budget preview bandwidth: available minus lead's share.
473        // Without this, large preview frames (e.g. 12 KB) at 30 fps consume
474        // 360 KB/s, starving the lead even when lead frames are tiny.
475        let avail = bandwidth_floor_bps(client);
476        let lead_bps = client.avg_paced_frame_bytes.max(256.0) * browser_pacing_fps(client);
477        let preview_budget = (avail - lead_bps).max(avail * 0.25).max(0.0);
478        let bw_cap = preview_budget / client.avg_preview_frame_bytes.max(256.0);
479        fps = fps.min(bw_cap.max(1.0));
480    }
481    fps.max(1.0)
482}
483
484fn preview_send_interval(client: &ClientState) -> Duration {
485    Duration::from_secs_f64(1.0 / preview_fps(client) as f64)
486}
487
488fn advance_deadline(deadline: &mut Instant, now: Instant, interval: Duration) {
489    let scheduled = deadline.checked_add(interval).unwrap_or(now + interval);
490    *deadline = if scheduled + interval < now {
491        now + interval
492    } else {
493        scheduled
494    };
495}
496
497fn should_snapshot_pty(dirty: bool, needful: bool, synced_output: bool) -> bool {
498    dirty && needful && !synced_output
499}
500
501fn enqueue_ready_frame(queue: &mut VecDeque<FrameState>, frame: FrameState) -> bool {
502    if queue.len() >= READY_FRAME_QUEUE_CAP {
503        return false;
504    }
505    queue.push_back(frame);
506    true
507}
508
509fn pty_has_visual_update(pty: &Pty) -> bool {
510    pty.dirty || !pty.ready_frames.is_empty() || !pty.byte_rx.is_empty()
511}
512
513/// Find the first `\x1b[?2026l` in `bytes`, handling sequences that span
514/// the `prefix`/`bytes` boundary. Uses SIMD-accelerated memchr for the
515/// initial ESC scan.
516fn find_sync_output_end(prefix: &[u8], bytes: &[u8]) -> Option<usize> {
517    if bytes.is_empty() {
518        return None;
519    }
520    let needle = SYNC_OUTPUT_END;
521    let nlen = needle.len();
522
523    // Check for a match straddling the prefix/bytes boundary.
524    if !prefix.is_empty() {
525        let tail = if prefix.len() >= nlen - 1 {
526            &prefix[prefix.len() - (nlen - 1)..]
527        } else {
528            prefix
529        };
530        let combined_len = tail.len() + bytes.len().min(nlen);
531        if combined_len >= nlen {
532            // Small stack buffer to check the boundary region.
533            let mut buf = [0u8; 32]; // SYNC_OUTPUT_END is 8 bytes, so 32 is plenty
534            let blen = combined_len.min(buf.len());
535            let tlen = tail.len().min(blen);
536            buf[..tlen].copy_from_slice(&tail[..tlen]);
537            let rest = (blen - tlen).min(bytes.len());
538            buf[tlen..tlen + rest].copy_from_slice(&bytes[..rest]);
539            for i in 0..=(blen.saturating_sub(nlen)) {
540                if &buf[i..i + nlen] == needle {
541                    let end_in_bytes = (i + nlen).saturating_sub(tail.len());
542                    if end_in_bytes > 0 && end_in_bytes <= bytes.len() {
543                        return Some(end_in_bytes);
544                    }
545                }
546            }
547        }
548    }
549
550    // SIMD-scan for ESC (0x1b) then verify the full sequence.
551    let mut offset = 0;
552    while let Some(pos) = memchr::memchr(0x1b, &bytes[offset..]) {
553        let abs = offset + pos;
554        if abs + nlen <= bytes.len() && &bytes[abs..abs + nlen] == needle {
555            return Some(abs + nlen);
556        }
557        offset = abs + 1;
558    }
559    None
560}
561
562fn update_sync_scan_tail(tail: &mut Vec<u8>, bytes: &[u8]) {
563    if bytes.is_empty() {
564        return;
565    }
566    tail.extend_from_slice(bytes);
567    let keep = SYNC_OUTPUT_END.len().saturating_sub(1);
568    if tail.len() > keep {
569        let drop = tail.len() - keep;
570        tail.drain(..drop);
571    }
572}
573
574fn preview_deadline(client: &ClientState, pid: u16, now: Instant) -> Instant {
575    client
576        .preview_next_send_at
577        .get(&pid)
578        .copied()
579        .unwrap_or(now)
580}
581
582fn client_has_due_preview(sess: &Session, client: &ClientState, now: Instant) -> bool {
583    if client.lead.is_none() {
584        return false;
585    }
586    client.subscriptions.iter().copied().any(|pid| {
587        Some(pid) != client.lead
588            && preview_deadline(client, pid, now) <= now
589            && sess
590                .ptys
591                .get(&pid)
592                .map(pty_has_visual_update)
593                .unwrap_or(false)
594    })
595}
596
597fn outbox_queued_frames(client: &ClientState) -> usize {
598    OUTBOX_CAPACITY.saturating_sub(client.tx.capacity())
599}
600
601fn outbox_backpressured(client: &ClientState) -> bool {
602    outbox_queued_frames(client) >= OUTBOX_SOFT_QUEUE_LIMIT_FRAMES
603}
604
605fn can_send_preview(client: &ClientState, pid: u16, now: Instant) -> bool {
606    window_open(client) && now >= preview_deadline(client, pid, now)
607}
608
609fn record_preview_send(client: &mut ClientState, pid: u16, now: Instant) {
610    let mut deadline = client
611        .preview_next_send_at
612        .get(&pid)
613        .copied()
614        .unwrap_or(now);
615    advance_deadline(&mut deadline, now, preview_send_interval(client));
616    client.preview_next_send_at.insert(pid, deadline);
617}
618
619fn window_open(client: &ClientState) -> bool {
620    !browser_backlog_blocked(client)
621        && !outbox_backpressured(client)
622        && client.inflight_frames.len() < target_frame_window(client)
623        && client.inflight_bytes < target_byte_window(client)
624}
625
626fn lead_window_open(client: &ClientState, reserve_preview_slot: bool) -> bool {
627    if !reserve_preview_slot || client.lead.is_none() {
628        return window_open(client);
629    }
630    if browser_backlog_blocked(client) || outbox_backpressured(client) {
631        return false;
632    }
633    let target_frames = target_frame_window(client);
634    let reserve_frames = PREVIEW_FRAME_RESERVE.min(target_frames.saturating_sub(1));
635    let frame_limit = target_frames.saturating_sub(reserve_frames).max(1);
636    let reserve_bytes = client.avg_preview_frame_bytes.max(256.0).ceil() as usize;
637    let byte_limit = target_byte_window(client)
638        .saturating_sub(reserve_bytes)
639        .max(client.avg_paced_frame_bytes.max(256.0).ceil() as usize);
640    client.inflight_frames.len() < frame_limit && client.inflight_bytes < byte_limit
641}
642
643fn can_send_frame(client: &ClientState, now: Instant, reserve_preview_slot: bool) -> bool {
644    lead_window_open(client, reserve_preview_slot) && now >= client.next_send_at
645}
646
647fn record_send(client: &mut ClientState, bytes: usize, now: Instant, paced: bool) {
648    client.inflight_bytes += bytes;
649    client.inflight_frames.push_back(InFlightFrame {
650        sent_at: now,
651        bytes,
652        paced,
653    });
654    if paced {
655        let interval = send_interval(client);
656        advance_deadline(&mut client.next_send_at, now, interval);
657    }
658}
659
660fn ewma_with_direction(old: f32, sample: f32, rise_alpha: f32, fall_alpha: f32) -> f32 {
661    let alpha = if sample > old { rise_alpha } else { fall_alpha };
662    old * (1.0 - alpha) + sample * alpha
663}
664
665fn window_saturated(client: &ClientState, inflight_frames: usize, inflight_bytes: usize) -> bool {
666    let target_frames = target_frame_window(client);
667    let target_bytes = target_byte_window(client);
668    inflight_frames.saturating_mul(10) >= target_frames.saturating_mul(9)
669        || inflight_bytes.saturating_mul(10) >= target_bytes.saturating_mul(9)
670}
671
672fn record_ack(client: &mut ClientState) {
673    if let Some(frame) = client.inflight_frames.pop_front() {
674        let prev_inflight_frames = client.inflight_frames.len() + 1;
675        let prev_inflight_bytes = client.inflight_bytes;
676        client.inflight_bytes = client.inflight_bytes.saturating_sub(frame.bytes);
677        client.acked_bytes_since_log = client.acked_bytes_since_log.saturating_add(frame.bytes);
678        let sample_ms = frame.sent_at.elapsed().as_secs_f32() * 1_000.0;
679        client.rtt_ms = ewma_with_direction(client.rtt_ms, sample_ms, 0.125, 0.25);
680        if client.min_rtt_ms > 0.0 {
681            // Only update downward: min_rtt tracks the unloaded path RTT and
682            // must not drift upward during congestion (queued RTT ≠ path RTT).
683            client.min_rtt_ms = client.min_rtt_ms.min(sample_ms);
684        } else {
685            client.min_rtt_ms = sample_ms;
686        }
687        client.min_rtt_ms = client.min_rtt_ms.max(0.5);
688        let sample_bps = frame.bytes as f32 / sample_ms.max(1.0e-3) * 1_000.0;
689        client.delivery_bps = ewma_with_direction(client.delivery_bps, sample_bps, 0.5, 0.125);
690        client.avg_frame_bytes =
691            ewma_with_direction(client.avg_frame_bytes, frame.bytes as f32, 0.5, 0.125);
692        if frame.paced {
693            client.avg_paced_frame_bytes =
694                ewma_with_direction(client.avg_paced_frame_bytes, frame.bytes as f32, 0.5, 0.125);
695        } else {
696            client.avg_preview_frame_bytes = ewma_with_direction(
697                client.avg_preview_frame_bytes,
698                frame.bytes as f32,
699                0.5,
700                0.125,
701            );
702        }
703        let frame_ms = 1_000.0 / browser_pacing_fps(client).max(1.0);
704        let path_rtt = path_rtt_ms(client);
705        let likely_window_limited =
706            window_saturated(client, prev_inflight_frames, prev_inflight_bytes);
707        client.goodput_window_bytes = client.goodput_window_bytes.saturating_add(frame.bytes);
708        let now = Instant::now();
709        let goodput_elapsed = now
710            .duration_since(client.goodput_window_start)
711            .as_secs_f32();
712        if goodput_elapsed >= 0.02 {
713            let sample_goodput = client.goodput_window_bytes as f32 / goodput_elapsed.max(1.0e-3);
714            if likely_window_limited || client.browser_backlog_frames > 0 {
715                let prev_goodput_sample = if client.last_goodput_sample_bps > 0.0 {
716                    client.last_goodput_sample_bps
717                } else {
718                    sample_goodput
719                };
720                let jitter_sample = (sample_goodput - prev_goodput_sample).abs();
721                client.goodput_bps =
722                    ewma_with_direction(client.goodput_bps, sample_goodput, 0.5, 0.125);
723                // Only update jitter from windows with at least 2 frames.
724                // Single-frame windows are pure measurement noise (0 or 1
725                // frame per 25 ms is a Bernoulli trial, not a congestion
726                // signal) and inflate jitter_bps, which in turn depresses
727                // bandwidth_floor_bps and causes pacing to stall.
728                let min_reliable = (client.avg_paced_frame_bytes.max(256.0) * 2.0) as usize;
729                if client.goodput_window_bytes >= min_reliable {
730                    client.goodput_jitter_bps =
731                        ewma_with_direction(client.goodput_jitter_bps, jitter_sample, 0.5, 0.125);
732                    let jitter_decay = if browser_ready(client) && sample_ms < path_rtt * 3.0 {
733                        0.90
734                    } else {
735                        0.98
736                    };
737                    client.max_goodput_jitter_bps =
738                        (client.max_goodput_jitter_bps * jitter_decay).max(jitter_sample);
739                    // Cap jitter at 45% of goodput so jitter_ratio can never
740                    // exceed 0.45 from measurement noise alone.  Real congestion
741                    // will still drive goodput_bps down and widen the window.
742                    client.max_goodput_jitter_bps =
743                        client.max_goodput_jitter_bps.min(client.goodput_bps * 0.45);
744                } else {
745                    // Thin sample: gently decay jitter rather than updating it.
746                    client.goodput_jitter_bps *= 0.9;
747                    client.max_goodput_jitter_bps *= 0.95;
748                }
749                // Sticky-high: never let last_goodput_sample_bps drop abruptly.
750                // A sudden drop (e.g. 1-frame window following a 2-frame window)
751                // inflates jitter_sample on the next cycle, collapsing probe_frames.
752                client.last_goodput_sample_bps =
753                    (client.last_goodput_sample_bps * 0.99).max(sample_goodput);
754            } else {
755                // When the path is underfilled, ACK cadence mostly measures our
756                // own pacing rather than network capacity.  Use a fall alpha
757                // proportional to estimation error: when the estimate is 10x+
758                // the sample, converge aggressively; when close, stay gentle.
759                let ratio = client.goodput_bps / sample_goodput.max(1.0);
760                let fall_alpha = if ratio > 10.0 {
761                    0.5
762                } else if ratio > 3.0 {
763                    0.25
764                } else {
765                    0.03
766                };
767                client.goodput_bps =
768                    ewma_with_direction(client.goodput_bps, sample_goodput, 0.5, fall_alpha);
769                client.goodput_jitter_bps *= 0.5;
770                client.max_goodput_jitter_bps *= 0.9;
771                client.last_goodput_sample_bps =
772                    (client.last_goodput_sample_bps * 0.99).max(sample_goodput);
773            }
774            client.goodput_window_bytes = 0;
775            client.goodput_window_start = now;
776        }
777        let queue_baseline_ms = if throughput_limited(client) {
778            window_rtt_ms(client)
779        } else {
780            path_rtt
781        };
782        let queue_delay_ms = (sample_ms - queue_baseline_ms).max(0.0);
783        let max_probe_frames = (browser_pacing_fps(client) * 0.125).max(4.0);
784        let jitter_ratio = client.max_goodput_jitter_bps / client.goodput_bps.max(1.0);
785        let low_delay_frames = if throughput_limited(client) { 2.0 } else { 8.0 };
786        let high_delay_frames = if throughput_limited(client) {
787            4.0
788        } else {
789            12.0
790        };
791        if likely_window_limited
792            && queue_delay_ms <= frame_ms * low_delay_frames
793            && jitter_ratio < 0.25
794        {
795            client.probe_frames = (client.probe_frames + 1.0).min(max_probe_frames);
796        } else if !likely_window_limited
797            && browser_ready(client)
798            && queue_delay_ms <= frame_ms * 2.0
799            && jitter_ratio < 0.25
800        {
801            client.probe_frames = (client.probe_frames + 0.25).min(max_probe_frames * 0.5);
802        } else if queue_delay_ms > frame_ms * high_delay_frames || jitter_ratio > 0.5 {
803            client.probe_frames = (client.probe_frames * 0.5).max(1.0);
804        } else if queue_delay_ms > frame_ms * 2.0 || !browser_ready(client) {
805            client.probe_frames = (client.probe_frames - 0.5).max(0.0);
806        }
807    } else {
808        client.inflight_bytes = 0;
809    }
810}
811
812fn reset_inflight(client: &mut ClientState) {
813    client.inflight_bytes = 0;
814    client.inflight_frames.clear();
815    client.next_send_at = Instant::now();
816    client.browser_backlog_frames = 0;
817    client.browser_ack_ahead_frames = 0;
818}
819
820fn is_unset_view_size(rows: u16, cols: u16) -> bool {
821    rows == 0 && cols == 0
822}
823
824fn subscribe_client_to(client: &mut ClientState, pty_id: u16) {
825    if client.subscriptions.insert(pty_id) {
826        client.last_sent.remove(&pty_id);
827        client.preview_next_send_at.remove(&pty_id);
828    }
829}
830
831fn unsubscribe_client_from(client: &mut ClientState, pty_id: u16) -> bool {
832    let removed_sub = client.subscriptions.remove(&pty_id);
833    client.last_sent.remove(&pty_id);
834    client.preview_next_send_at.remove(&pty_id);
835    client.scroll_offsets.remove(&pty_id);
836    client.scroll_caches.remove(&pty_id);
837    let removed_view = client.view_sizes.remove(&pty_id).is_some();
838    if client.lead == Some(pty_id) {
839        client.lead = None;
840    }
841    removed_sub || removed_view
842}
843
844fn update_client_scroll_state(client: &mut ClientState, pty_id: u16, next_offset: usize) -> bool {
845    let prev_offset = client.scroll_offsets.get(&pty_id).copied().unwrap_or(0);
846    if prev_offset == next_offset {
847        return false;
848    }
849
850    if prev_offset == 0 && next_offset > 0 {
851        client.scroll_caches.insert(
852            pty_id,
853            client.last_sent.get(&pty_id).cloned().unwrap_or_default(),
854        );
855    } else if prev_offset > 0
856        && next_offset == 0
857        && let Some(cache) = client.scroll_caches.remove(&pty_id)
858    {
859        if cache.rows() > 0 && cache.cols() > 0 {
860            client.last_sent.insert(pty_id, cache);
861        } else {
862            client.last_sent.remove(&pty_id);
863        }
864    }
865
866    if next_offset > 0 {
867        client.scroll_offsets.insert(pty_id, next_offset);
868    } else {
869        client.scroll_offsets.remove(&pty_id);
870    }
871    reset_inflight(client);
872    true
873}
874
875struct Session {
876    ptys: HashMap<u16, Pty>,
877    next_client_id: u64,
878    /// Diagnostics: how many times tick() was called this second.
879    tick_fires: u32,
880    /// Diagnostics: how many ticks found the focused PTY dirty (snapshot taken).
881    tick_snaps: u32,
882    clients: HashMap<u64, ClientState>,
883}
884
885struct SearchResultRow {
886    pty_id: u16,
887    score: u32,
888    primary_source: u8,
889    matched_sources: u8,
890    context: String,
891    scroll_offset: Option<usize>,
892}
893
894struct TickOutcome {
895    did_work: bool,
896    next_deadline: Option<Instant>,
897}
898
899impl Session {
900    fn new() -> Self {
901        Self {
902            ptys: HashMap::new(),
903            next_client_id: 1,
904            clients: HashMap::new(),
905            tick_fires: 0,
906            tick_snaps: 0,
907        }
908    }
909
910    fn allocate_pty_id(&mut self) -> Option<u16> {
911        (1..=u16::MAX).find(|id| !self.ptys.contains_key(id))
912    }
913
914    fn send_to_all(&self, msg: &[u8]) {
915        for c in self.clients.values() {
916            let _ = c.tx.try_send(msg.to_vec());
917        }
918    }
919
920    fn mediated_size_for_pty(&self, pty_id: u16) -> Option<(u16, u16)> {
921        let mut min_rows: Option<u16> = None;
922        let mut min_cols: Option<u16> = None;
923        for c in self.clients.values() {
924            if let Some((r, cols)) = c.view_sizes.get(&pty_id).copied() {
925                min_rows = Some(min_rows.map_or(r, |m: u16| m.min(r)));
926                min_cols = Some(min_cols.map_or(cols, |m: u16| m.min(cols)));
927            }
928        }
929        match (min_rows, min_cols) {
930            (Some(r), Some(c)) => Some((r.max(1), c.max(1))),
931            _ => None,
932        }
933    }
934
935    fn resize_pty(&mut self, pty_id: u16, rows: u16, cols: u16) -> bool {
936        let pty = match self.ptys.get_mut(&pty_id) {
937            Some(p) => p,
938            None => return false,
939        };
940        let (cur_rows, cur_cols) = pty.driver.size();
941        if cur_rows == rows && cur_cols == cols {
942            return false;
943        }
944        pty.ready_frames.clear();
945        pty.driver.resize(rows, cols);
946        pty.mark_dirty();
947        for c in self.clients.values_mut() {
948            if c.subscriptions.contains(&pty_id) {
949                c.last_sent.remove(&pty_id);
950            }
951            if c.scroll_caches.remove(&pty_id).is_some() {
952                reset_inflight(c);
953            }
954        }
955        if !pty.exited {
956            pty::resize_pty_os(&pty.handle, rows, cols);
957        }
958        true
959    }
960
961    fn resize_ptys_to_mediated_sizes<I>(&mut self, pty_ids: I) -> bool
962    where
963        I: IntoIterator<Item = u16>,
964    {
965        let mut changed = false;
966        let mut seen = HashSet::new();
967        for pty_id in pty_ids {
968            if !seen.insert(pty_id) {
969                continue;
970            }
971            if let Some((rows, cols)) = self.mediated_size_for_pty(pty_id) {
972                changed |= self.resize_pty(pty_id, rows, cols);
973            }
974        }
975        changed
976    }
977
978    fn pty_list_msg(&self) -> Vec<u8> {
979        let mut msg = vec![S2C_LIST];
980        let count = self.ptys.len() as u16;
981        msg.extend_from_slice(&count.to_le_bytes());
982        let mut ids: Vec<u16> = self.ptys.keys().copied().collect();
983        ids.sort();
984        for id in ids {
985            let pty = &self.ptys[&id];
986            let tag = pty.tag.as_bytes();
987            msg.extend_from_slice(&id.to_le_bytes());
988            msg.extend_from_slice(&(tag.len() as u16).to_le_bytes());
989            msg.extend_from_slice(tag);
990            let cmd = pty.command.as_deref().unwrap_or("").as_bytes();
991            msg.extend_from_slice(&(cmd.len() as u16).to_le_bytes());
992            msg.extend_from_slice(cmd);
993        }
994        msg
995    }
996}
997
998type AppState = Arc<(Config, Mutex<Session>, PtyFds, Arc<Notify>)>;
999
1000fn nudge_delivery(state: &AppState) {
1001    state.3.notify_one();
1002}
1003
1004fn parse_terminal_queries(data: &[u8], size: (u16, u16), cursor: (u16, u16)) -> Vec<String> {
1005    const DA1_RESPONSE: &[u8] = b"\x1b[?64;1;2;6;9;15;18;21;22c";
1006
1007    let mut results = Vec::new();
1008    let mut i = 0;
1009    while i < data.len() {
1010        if data[i] != 0x1b || i + 2 >= data.len() || data[i + 1] != b'[' {
1011            i += 1;
1012            continue;
1013        }
1014        i += 2;
1015        let has_q = i < data.len() && data[i] == b'?';
1016        if has_q {
1017            i += 1;
1018        }
1019        let param_start = i;
1020        while i < data.len() && (data[i].is_ascii_digit() || data[i] == b';') {
1021            i += 1;
1022        }
1023        if i >= data.len() {
1024            break;
1025        }
1026        let final_byte = data[i];
1027        let params = &data[param_start..i];
1028        i += 1;
1029        if has_q {
1030            continue;
1031        }
1032        let resp: Option<String> = match final_byte {
1033            b'c' if params.is_empty() || params == b"0" => {
1034                Some(String::from_utf8_lossy(DA1_RESPONSE).into_owned())
1035            }
1036            b'n' if params == b"6" => Some(format!("\x1b[{};{}R", cursor.0 + 1, cursor.1 + 1)),
1037            b'n' if params == b"5" => Some("\x1b[0n".into()),
1038            b't' if params == b"18" => {
1039                let (rows, cols) = size;
1040                Some(format!("\x1b[8;{rows};{cols}t"))
1041            }
1042            b't' if params == b"14" => {
1043                let (rows, cols) = size;
1044                Some(format!("\x1b[4;{};{}t", rows * 16, cols * 8))
1045            }
1046            _ => None,
1047        };
1048        if let Some(r) = resp {
1049            results.push(r);
1050        }
1051    }
1052    results
1053}
1054
1055async fn cleanup_pty_internal(pty_id: u16, state: &AppState) {
1056    state.2.write().unwrap().remove(&pty_id);
1057    let mut sess = state.1.lock().await;
1058    if let Some(pty) = sess.ptys.get_mut(&pty_id) {
1059        if pty.exited {
1060            return;
1061        }
1062        pty.exited = true;
1063        pty::close_pty(&pty.handle);
1064        pty.exit_status = pty::collect_exit_status(&pty.handle);
1065        pty.mark_dirty();
1066        let msg = blit_remote::msg_exited(pty_id, pty.exit_status);
1067        sess.send_to_all(&msg);
1068    }
1069}
1070
1071fn take_snapshot(pty: &mut Pty) -> FrameState {
1072    if pty.lflag_last.elapsed() >= Duration::from_millis(250) {
1073        pty.lflag_cache = pty::pty_lflag(&pty.handle);
1074        pty.lflag_last = Instant::now();
1075    }
1076    let (echo, icanon) = pty.lflag_cache;
1077    pty.driver.snapshot(echo, icanon)
1078}
1079
1080fn build_scrollback_update(
1081    pty: &mut Pty,
1082    id: u16,
1083    offset: usize,
1084    prev_frame: &FrameState,
1085) -> Option<(Vec<u8>, FrameState)> {
1086    let frame = pty.driver.scrollback_frame(offset);
1087    let msg = build_update_msg(id, &frame, prev_frame);
1088    msg.map(|m| (m, frame))
1089}
1090
1091fn build_search_results_msg(request_id: u16, results: &[SearchResultRow]) -> Vec<u8> {
1092    let count = results.len().min(u16::MAX as usize);
1093    let payload_bytes: usize = results[..count]
1094        .iter()
1095        .map(|result| 14 + result.context.len().min(u16::MAX as usize))
1096        .sum();
1097    let mut msg = Vec::with_capacity(5 + payload_bytes);
1098    msg.push(S2C_SEARCH_RESULTS);
1099    msg.extend_from_slice(&request_id.to_le_bytes());
1100    msg.extend_from_slice(&(count as u16).to_le_bytes());
1101    for result in &results[..count] {
1102        msg.extend_from_slice(&result.pty_id.to_le_bytes());
1103        msg.extend_from_slice(&result.score.to_le_bytes());
1104        msg.push(result.primary_source);
1105        msg.push(result.matched_sources);
1106        let scroll_offset = result
1107            .scroll_offset
1108            .map(|offset| offset.min(u32::MAX as usize - 1) as u32)
1109            .unwrap_or(u32::MAX);
1110        msg.extend_from_slice(&scroll_offset.to_le_bytes());
1111        let context = result.context.as_bytes();
1112        let context_len = context.len().min(u16::MAX as usize);
1113        msg.extend_from_slice(&(context_len as u16).to_le_bytes());
1114        msg.extend_from_slice(&context[..context_len]);
1115    }
1116    msg
1117}
1118
1119enum SendOutcome {
1120    NoChange,
1121    Sent,
1122    Backpressured,
1123}
1124
1125fn try_send_update(
1126    client: &mut ClientState,
1127    pid: u16,
1128    current: FrameState,
1129    msg: Option<Vec<u8>>,
1130    now: Instant,
1131    paced: bool,
1132) -> SendOutcome {
1133    let Some(msg) = msg else {
1134        return SendOutcome::NoChange;
1135    };
1136    let bytes = msg.len();
1137    if client.tx.try_send(msg).is_ok() {
1138        client.last_sent.insert(pid, current);
1139        record_send(client, bytes, now, paced);
1140        client.frames_sent = client.frames_sent.wrapping_add(1);
1141        SendOutcome::Sent
1142    } else {
1143        // Outbox full — the sender can't keep up.  Advance last_sent to
1144        // the current frame so the NEXT diff is small (only changes since
1145        // now), effectively dropping this intermediate state.  Without
1146        // this, backpressure causes the tick to re-dirty the PTY, building
1147        // ever-larger diffs that make the backlog worse.
1148        client.last_sent.insert(pid, current);
1149        SendOutcome::Backpressured
1150    }
1151}
1152
1153pub async fn run(config: Config) {
1154    let state: AppState = Arc::new((
1155        config,
1156        Mutex::new(Session::new()),
1157        Arc::new(std::sync::RwLock::new(HashMap::new())),
1158        Arc::new(Notify::new()),
1159    ));
1160
1161    let delivery_state = state.clone();
1162    tokio::spawn(async move {
1163        let mut next_deadline: Option<Instant> = None;
1164        loop {
1165            if let Some(deadline) = next_deadline {
1166                tokio::select! {
1167                    _ = delivery_state.3.notified() => {}
1168                    _ = tokio::time::sleep_until(tokio::time::Instant::from_std(deadline)) => {}
1169                }
1170            } else {
1171                delivery_state.3.notified().await;
1172            }
1173            loop {
1174                let outcome = tick(&delivery_state).await;
1175                next_deadline = outcome.next_deadline;
1176                if !outcome.did_work {
1177                    break;
1178                }
1179                tokio::task::yield_now().await;
1180            }
1181        }
1182    });
1183
1184    tokio::spawn(async {
1185        loop {
1186            tokio::time::sleep(Duration::from_secs(5)).await;
1187            pty::reap_zombies();
1188        }
1189    });
1190
1191    #[cfg(unix)]
1192    if let Some(channel_fd) = state.0.fd_channel {
1193        ipc::run_fd_channel(channel_fd, state).await;
1194        return;
1195    }
1196
1197    #[cfg(unix)]
1198    let listener = {
1199        if let Some(l) = IpcListener::from_systemd_fd(state.0.verbose) {
1200            l
1201        } else {
1202            IpcListener::bind(&state.0.ipc_path, state.0.verbose)
1203        }
1204    };
1205    #[cfg(not(unix))]
1206    let mut listener = IpcListener::bind(&state.0.ipc_path, state.0.verbose);
1207
1208    loop {
1209        let stream = match listener.accept().await {
1210            Ok(s) => s,
1211            Err(e) => {
1212                eprintln!("accept error: {e}");
1213                tokio::time::sleep(Duration::from_millis(100)).await;
1214                continue;
1215            }
1216        };
1217        let state = state.clone();
1218        tokio::spawn(handle_client(stream, state));
1219    }
1220}
1221
1222async fn tick(state: &AppState) -> TickOutcome {
1223    let mut sess = state.1.lock().await;
1224    sess.tick_fires += 1;
1225    let mut did_work = false;
1226    let mut next_deadline: Option<Instant> = None;
1227    let now = Instant::now();
1228
1229    let max_fps = sess
1230        .clients
1231        .values()
1232        .map(browser_pacing_fps)
1233        .fold(1.0_f32, f32::max);
1234    let title_interval = Duration::from_secs_f64(1.0 / max_fps as f64);
1235    let ids: Vec<u16> = sess.ptys.keys().copied().collect();
1236    for &id in &ids {
1237        let Some(pty) = sess.ptys.get_mut(&id) else {
1238            continue;
1239        };
1240        if pty.driver.take_title_dirty() {
1241            pty.mark_dirty();
1242            pty.title_pending = true;
1243        }
1244        if pty.title_pending && now.duration_since(pty.last_title_send) >= title_interval {
1245            let msg = {
1246                let title_bytes = pty.driver.title().as_bytes();
1247                let mut msg = Vec::with_capacity(3 + title_bytes.len());
1248                msg.push(S2C_TITLE);
1249                msg.extend_from_slice(&id.to_le_bytes());
1250                msg.extend_from_slice(title_bytes);
1251                msg
1252            };
1253            pty.last_title_send = now;
1254            pty.title_pending = false;
1255            sess.send_to_all(&msg);
1256            did_work = true;
1257        }
1258    }
1259
1260    // Drain bytes from PTY reader channels. This is the only place
1261    // process() is called, so there is no contention with the readers.
1262    let mut eof_ptys: Vec<u16> = Vec::with_capacity(ids.len());
1263    for &id in &ids {
1264        let Some(pty) = sess.ptys.get_mut(&id) else {
1265            continue;
1266        };
1267        while let Ok(input) = pty.byte_rx.try_recv() {
1268            match input {
1269                PtyInput::Data(data) => {
1270                    pty::respond_to_queries(
1271                        &pty.handle,
1272                        &data,
1273                        pty.driver.size(),
1274                        pty.driver.cursor_position(),
1275                    );
1276                    pty.driver.process(&data);
1277                    pty.mark_dirty();
1278                    did_work = true;
1279                }
1280                PtyInput::SyncBoundary { before, after } => {
1281                    if !before.is_empty() {
1282                        pty::respond_to_queries(
1283                            &pty.handle,
1284                            &before,
1285                            pty.driver.size(),
1286                            pty.driver.cursor_position(),
1287                        );
1288                        pty.driver.process(&before);
1289                        pty.mark_dirty();
1290                    }
1291                    if !pty.driver.synced_output() {
1292                        let frame = take_snapshot(pty);
1293                        enqueue_ready_frame(&mut pty.ready_frames, frame);
1294                        pty.clear_dirty();
1295                    }
1296                    if !after.is_empty() {
1297                        pty::respond_to_queries(
1298                            &pty.handle,
1299                            &after,
1300                            pty.driver.size(),
1301                            pty.driver.cursor_position(),
1302                        );
1303                        pty.driver.process(&after);
1304                        pty.mark_dirty();
1305                    }
1306                    did_work = true;
1307                }
1308                PtyInput::Eof => {
1309                    eof_ptys.push(id);
1310                }
1311            }
1312        }
1313    }
1314    // Handle EOF outside the borrow loop.
1315    drop(sess);
1316    for id in eof_ptys {
1317        tokio::time::sleep(Duration::from_millis(50)).await;
1318        cleanup_pty_internal(id, state).await;
1319    }
1320    let mut sess = state.1.lock().await;
1321
1322    // Only snapshot PTYs that have at least one client ready to consume a fresh
1323    // frame right now. This avoids burning CPU on snapshot+diff+compress work
1324    // while the lead is merely waiting for its next pacing deadline.
1325    let needful_ptys: HashSet<u16> = sess
1326        .clients
1327        .values()
1328        .flat_map(|c| {
1329            let reserve_preview_slot = client_has_due_preview(&sess, c, now);
1330            c.subscriptions.iter().copied().filter(move |pid| {
1331                let scrolled = c.scroll_offsets.get(pid).copied().unwrap_or(0) > 0;
1332                if Some(*pid) == c.lead {
1333                    !scrolled && can_send_frame(c, now, reserve_preview_slot)
1334                } else {
1335                    !scrolled && can_send_preview(c, *pid, now)
1336                }
1337            })
1338        })
1339        .collect();
1340
1341    let mut snapshots: HashMap<u16, FrameState> = HashMap::new();
1342    for &id in &ids {
1343        let Some(pty) = sess.ptys.get_mut(&id) else {
1344            continue;
1345        };
1346        if needful_ptys.contains(&id)
1347            && let Some(frame) = pty.ready_frames.pop_front()
1348        {
1349            snapshots.insert(id, frame);
1350            sess.tick_snaps += 1;
1351            did_work = true;
1352            continue;
1353        }
1354        if !should_snapshot_pty(
1355            pty.dirty,
1356            needful_ptys.contains(&id),
1357            pty.driver.synced_output(),
1358        ) {
1359            continue;
1360        }
1361        // Applications that care about complete-frame boundaries should
1362        // use DEC synchronized output (?2026). Outside that bracket we
1363        // snapshot immediately instead of heuristically coalescing reads.
1364        snapshots.insert(id, take_snapshot(pty));
1365        pty.clear_dirty();
1366        sess.tick_snaps += 1;
1367        did_work = true;
1368    }
1369
1370    let client_ids: Vec<u64> = sess.clients.keys().copied().collect();
1371    for cid in client_ids {
1372        // When the pipe is idle (nothing in flight), RTT cannot be measured
1373        // and the last observed value stales.  Decay it toward min_rtt so
1374        // a stale congested RTT doesn't permanently suppress the send window
1375        // after congestion clears or traffic patterns change (e.g. switching
1376        // from a large-frame burst to idle small-frame updates).
1377        if let Some(c) = sess.clients.get_mut(&cid) {
1378            if c.inflight_bytes == 0 && c.min_rtt_ms > 0.0 && c.rtt_ms > c.min_rtt_ms {
1379                c.rtt_ms = (c.rtt_ms * 0.99 + c.min_rtt_ms * 0.01).max(c.min_rtt_ms);
1380            }
1381            // Decay stale browser metrics so a missed/delayed metrics update
1382            // can't permanently block the delivery loop.
1383            if c.last_metrics_update.elapsed() > Duration::from_secs(1) {
1384                c.browser_backlog_frames = 0;
1385                c.browser_ack_ahead_frames = 0;
1386            }
1387        }
1388        let (
1389            lead,
1390            subscriptions,
1391            scrolled_ptys,
1392            can_send_lead,
1393            lead_has_window,
1394            any_send_window,
1395            lead_deadline,
1396        ) = {
1397            let Some(c) = sess.clients.get(&cid) else {
1398                continue;
1399            };
1400            let reserve_preview_slot = client_has_due_preview(&sess, c, now);
1401            (
1402                c.lead,
1403                c.subscriptions.iter().copied().collect::<Vec<_>>(),
1404                c.scroll_offsets
1405                    .iter()
1406                    .map(|(&k, &v)| (k, v))
1407                    .collect::<Vec<_>>(),
1408                can_send_frame(c, now, reserve_preview_slot),
1409                lead_window_open(c, reserve_preview_slot),
1410                lead_window_open(c, reserve_preview_slot) || window_open(c),
1411                c.next_send_at,
1412            )
1413        };
1414
1415        if subscriptions.is_empty() {
1416            continue;
1417        }
1418
1419        // Send scrollback frames for any scrolled PTY.
1420        for &(scroll_pid, scroll_offset) in &scrolled_ptys {
1421            if scroll_offset == 0 {
1422                continue;
1423            }
1424            let is_lead = lead == Some(scroll_pid);
1425            let can_send = if is_lead { can_send_lead } else { true };
1426            if can_send {
1427                let prev_frame = {
1428                    let Some(c) = sess.clients.get(&cid) else {
1429                        continue;
1430                    };
1431                    c.scroll_caches
1432                        .get(&scroll_pid)
1433                        .cloned()
1434                        .unwrap_or_default()
1435                };
1436                let outcome = if let Some(pty) = sess.ptys.get_mut(&scroll_pid) {
1437                    if let Some((msg, new_frame)) =
1438                        build_scrollback_update(pty, scroll_pid, scroll_offset, &prev_frame)
1439                    {
1440                        let Some(c) = sess.clients.get_mut(&cid) else {
1441                            break;
1442                        };
1443                        let bytes = msg.len();
1444                        if c.tx.try_send(msg).is_ok() {
1445                            c.scroll_caches.insert(scroll_pid, new_frame);
1446                            record_send(c, bytes, now, is_lead);
1447                            c.frames_sent += 1;
1448                            SendOutcome::Sent
1449                        } else {
1450                            SendOutcome::Backpressured
1451                        }
1452                    } else {
1453                        SendOutcome::NoChange
1454                    }
1455                } else {
1456                    SendOutcome::NoChange
1457                };
1458                match outcome {
1459                    SendOutcome::Sent => did_work = true,
1460                    SendOutcome::Backpressured => {
1461                        if let Some(pty) = sess.ptys.get_mut(&scroll_pid) {
1462                            pty.mark_dirty();
1463                        }
1464                    }
1465                    SendOutcome::NoChange => {}
1466                }
1467            } else if is_lead && lead_has_window {
1468                next_deadline = Some(match next_deadline {
1469                    Some(existing) => existing.min(lead_deadline),
1470                    None => lead_deadline,
1471                });
1472            }
1473        }
1474
1475        let lead_scroll_offset = lead
1476            .and_then(|pid| {
1477                scrolled_ptys
1478                    .iter()
1479                    .find(|&&(k, _)| k == pid)
1480                    .map(|&(_, v)| v)
1481            })
1482            .unwrap_or(0);
1483
1484        if let Some(pid) = lead {
1485            if lead_scroll_offset == 0 && can_send_lead {
1486                if let Some(cur) = snapshots.get(&pid).cloned() {
1487                    let previous = sess
1488                        .clients
1489                        .get(&cid)
1490                        .and_then(|c| c.last_sent.get(&pid).cloned())
1491                        .unwrap_or_default();
1492                    drop(sess);
1493                    let msg = build_update_msg(pid, &cur, &previous);
1494                    sess = state.1.lock().await;
1495                    let Some(c) = sess.clients.get_mut(&cid) else {
1496                        continue;
1497                    };
1498                    match try_send_update(c, pid, cur, msg, now, true) {
1499                        SendOutcome::Sent => did_work = true,
1500                        SendOutcome::Backpressured => {
1501                            if let Some(pty) = sess.ptys.get_mut(&pid) {
1502                                pty.mark_dirty();
1503                            }
1504                        }
1505                        SendOutcome::NoChange => {}
1506                    }
1507                } else {
1508                    let has_pending = sess
1509                        .ptys
1510                        .get(&pid)
1511                        .map(pty_has_visual_update)
1512                        .unwrap_or(false);
1513                    let _ = has_pending;
1514                }
1515            } else {
1516                let has_pending = sess
1517                    .ptys
1518                    .get(&pid)
1519                    .map(pty_has_visual_update)
1520                    .unwrap_or(false);
1521                if has_pending && lead_has_window {
1522                    next_deadline = Some(match next_deadline {
1523                        Some(existing) => existing.min(lead_deadline),
1524                        None => lead_deadline,
1525                    });
1526                }
1527            }
1528        }
1529
1530        if !any_send_window {
1531            continue;
1532        }
1533
1534        let mut preview_ids = subscriptions;
1535        preview_ids.retain(|pid| Some(*pid) != lead);
1536        preview_ids.sort_unstable();
1537
1538        for pid in preview_ids {
1539            let (preview_can_send, preview_due_at, preview_has_window) =
1540                match sess.clients.get(&cid) {
1541                    Some(c) => (
1542                        can_send_preview(c, pid, now),
1543                        preview_deadline(c, pid, now),
1544                        window_open(c),
1545                    ),
1546                    None => (false, now, false),
1547                };
1548            if !preview_has_window {
1549                break;
1550            }
1551            if !preview_can_send {
1552                let has_pending = sess
1553                    .ptys
1554                    .get(&pid)
1555                    .map(pty_has_visual_update)
1556                    .unwrap_or(false);
1557                // Only set a deadline when the reason is *timing* (deadline
1558                // in the future), not capacity (preview window closed).
1559                // A past deadline here spins the delivery loop because
1560                // sleep_until(past) returns immediately.
1561                if has_pending && preview_due_at > now {
1562                    next_deadline = Some(match next_deadline {
1563                        Some(existing) => existing.min(preview_due_at),
1564                        None => preview_due_at,
1565                    });
1566                }
1567                continue;
1568            }
1569            let Some(cur) = snapshots.get(&pid) else {
1570                let has_pending = sess
1571                    .ptys
1572                    .get(&pid)
1573                    .map(pty_has_visual_update)
1574                    .unwrap_or(false);
1575                let _ = has_pending;
1576                continue;
1577            };
1578            let cur = cur.clone();
1579            let previous = sess
1580                .clients
1581                .get(&cid)
1582                .and_then(|c| c.last_sent.get(&pid).cloned())
1583                .unwrap_or_default();
1584            drop(sess);
1585            let msg = build_update_msg(pid, &cur, &previous);
1586            sess = state.1.lock().await;
1587            let Some(c) = sess.clients.get_mut(&cid) else {
1588                break;
1589            };
1590            match try_send_update(c, pid, cur, msg, now, false) {
1591                SendOutcome::Sent => {
1592                    record_preview_send(c, pid, now);
1593                    did_work = true;
1594                }
1595                SendOutcome::Backpressured => {
1596                    if let Some(pty) = sess.ptys.get_mut(&pid) {
1597                        pty.mark_dirty();
1598                    }
1599                    break;
1600                }
1601                SendOutcome::NoChange => {}
1602            }
1603        }
1604    }
1605
1606    TickOutcome {
1607        did_work,
1608        next_deadline,
1609    }
1610}
1611
1612async fn handle_client<S: AsyncRead + AsyncWrite + Unpin + Send + 'static>(
1613    stream: S,
1614    state: AppState,
1615) {
1616    let config = &state.0;
1617    let (mut reader, mut writer) = tokio::io::split(stream);
1618
1619    let (out_tx, mut out_rx) = mpsc::channel::<Vec<u8>>(OUTBOX_CAPACITY);
1620    let sender = tokio::spawn(async move {
1621        while let Some(msg) = out_rx.recv().await {
1622            if !write_frame(&mut writer, &msg).await {
1623                break;
1624            }
1625        }
1626    });
1627    let client_id;
1628
1629    {
1630        let mut sess = state.1.lock().await;
1631        client_id = sess.next_client_id;
1632        sess.next_client_id += 1;
1633        sess.clients.insert(
1634            client_id,
1635            ClientState {
1636                tx: out_tx,
1637                lead: None,
1638                subscriptions: HashSet::new(),
1639                view_sizes: HashMap::new(),
1640                scroll_offsets: HashMap::new(),
1641                scroll_caches: HashMap::new(),
1642                last_sent: HashMap::new(),
1643                preview_next_send_at: HashMap::new(),
1644                rtt_ms: 50.0,
1645                min_rtt_ms: 0.0,
1646                display_fps: 60.0,
1647                // Conservative seed — the rise alpha (0.5) converges up to
1648                // multi-MB/s in a handful of samples on low-latency paths. Starting
1649                // high causes catastrophic bufferbloat on slow links because
1650                // target_byte_window scales with the goodput estimate.
1651                delivery_bps: 262_144.0,
1652                goodput_bps: 262_144.0,
1653                goodput_jitter_bps: 0.0,
1654                max_goodput_jitter_bps: 0.0,
1655                last_goodput_sample_bps: 0.0,
1656                avg_frame_bytes: 1_024.0,
1657                avg_paced_frame_bytes: 1_024.0,
1658                avg_preview_frame_bytes: 1_024.0,
1659                inflight_bytes: 0,
1660                inflight_frames: VecDeque::new(),
1661                next_send_at: Instant::now(),
1662                probe_frames: 0.0,
1663                frames_sent: 0,
1664                acks_recv: 0,
1665                acked_bytes_since_log: 0,
1666                browser_backlog_frames: 0,
1667                browser_ack_ahead_frames: 0,
1668                browser_apply_ms: 0.0,
1669                last_metrics_update: Instant::now(),
1670                last_log: Instant::now(),
1671                goodput_window_bytes: 0,
1672                goodput_window_start: Instant::now(),
1673            },
1674        );
1675        if let Some(c) = sess.clients.get(&client_id) {
1676            let _ = c.tx.try_send(msg_hello(
1677                1,
1678                FEATURE_CREATE_NONCE | FEATURE_RESTART | FEATURE_RESIZE_BATCH | FEATURE_COPY_RANGE,
1679            ));
1680        }
1681        let mut initial_msgs = Vec::with_capacity(2 + sess.ptys.len() * 2);
1682        initial_msgs.push(sess.pty_list_msg());
1683        for (&id, pty) in &sess.ptys {
1684            let title = pty.driver.title();
1685            if !title.is_empty() {
1686                let title_bytes = title.as_bytes();
1687                let mut msg = Vec::with_capacity(3 + title_bytes.len());
1688                msg.push(S2C_TITLE);
1689                msg.extend_from_slice(&id.to_le_bytes());
1690                msg.extend_from_slice(title_bytes);
1691                initial_msgs.push(msg);
1692            }
1693            if pty.exited {
1694                initial_msgs.push(blit_remote::msg_exited(id, pty.exit_status));
1695            }
1696        }
1697        initial_msgs.push(vec![S2C_READY]);
1698        let tx = sess.clients.get(&client_id).map(|c| c.tx.clone());
1699        drop(sess);
1700        if let Some(tx) = tx {
1701            for msg in initial_msgs {
1702                if tx.send(msg).await.is_err() {
1703                    break;
1704                }
1705            }
1706        }
1707    }
1708
1709    if state.0.verbose {
1710        eprintln!("client connected");
1711    }
1712
1713    while let Some(data) = read_frame(&mut reader).await {
1714        if data.is_empty() {
1715            continue;
1716        }
1717
1718        if data[0] == C2S_ACK {
1719            let mut sess = state.1.lock().await;
1720            let (
1721                do_log,
1722                frames_sent,
1723                acks_recv,
1724                rtt_ms,
1725                min_rtt_ms,
1726                eff_rtt_ms,
1727                inflight_bytes,
1728                delivery_bps,
1729                goodput_ewma_bps,
1730                goodput_jitter_bps,
1731                max_goodput_jitter_bps,
1732                avg_frame_bytes,
1733                avg_paced_frame_bytes,
1734                avg_preview_frame_bytes,
1735                display_fps,
1736                paced_fps,
1737                display_need_bps,
1738                probe_frames,
1739                goodput_bps,
1740                window_frames,
1741                window_bytes,
1742                outbox_frames,
1743                browser_backlog_frames,
1744                browser_ack_ahead_frames,
1745                browser_apply_ms,
1746            ) = {
1747                let Some(c) = sess.clients.get_mut(&client_id) else {
1748                    continue;
1749                };
1750                c.acks_recv += 1;
1751                record_ack(c);
1752                let do_log = c.last_log.elapsed().as_secs_f32() >= 1.0;
1753                let log_elapsed = c.last_log.elapsed().as_secs_f32().max(1.0e-3);
1754                let paced_fps = pacing_fps(c);
1755                let display_need_bps = display_need_bps(c);
1756                let out = (
1757                    do_log,
1758                    c.frames_sent,
1759                    c.acks_recv,
1760                    c.rtt_ms,
1761                    path_rtt_ms(c),
1762                    window_rtt_ms(c),
1763                    c.inflight_bytes,
1764                    c.delivery_bps,
1765                    c.goodput_bps,
1766                    c.goodput_jitter_bps,
1767                    c.max_goodput_jitter_bps,
1768                    c.avg_frame_bytes,
1769                    c.avg_paced_frame_bytes,
1770                    c.avg_preview_frame_bytes,
1771                    c.display_fps,
1772                    paced_fps,
1773                    display_need_bps,
1774                    c.probe_frames,
1775                    c.acked_bytes_since_log as f32 / log_elapsed,
1776                    target_frame_window(c),
1777                    target_byte_window(c),
1778                    outbox_queued_frames(c),
1779                    c.browser_backlog_frames,
1780                    c.browser_ack_ahead_frames,
1781                    c.browser_apply_ms,
1782                );
1783                if do_log {
1784                    c.frames_sent = 0;
1785                    c.acks_recv = 0;
1786                    c.acked_bytes_since_log = 0;
1787                    c.last_log = Instant::now();
1788                }
1789                out
1790            };
1791            if do_log && config.verbose {
1792                eprintln!(
1793                    "client {client_id}: sent={frames_sent} acks={acks_recv} rtt={rtt_ms:.0}ms min_rtt={min_rtt_ms:.0}ms eff_rtt={eff_rtt_ms:.0}ms window={window_frames}f/{window_bytes}B probe={probe_frames:.0}f inflight={inflight_bytes}B outbox={outbox_frames}f goodput={goodput_bps:.0}B/s goodput_ewma={goodput_ewma_bps:.0}B/s jitter={goodput_jitter_bps:.0}/{max_goodput_jitter_bps:.0}B/s rate={delivery_bps:.0}B/s avg_frame={avg_frame_bytes:.0}B lead_frame={avg_paced_frame_bytes:.0}B preview_frame={avg_preview_frame_bytes:.0}B need={display_need_bps:.0}B/s display_fps={display_fps:.0} paced_fps={paced_fps:.0} backlog={browser_backlog_frames} ack_ahead={browser_ack_ahead_frames} apply={browser_apply_ms:.1}ms | tick_fires={} tick_snaps={}",
1794                    sess.tick_fires, sess.tick_snaps,
1795                );
1796            }
1797            if do_log {
1798                sess.tick_fires = 0;
1799                sess.tick_snaps = 0;
1800            }
1801            nudge_delivery(&state);
1802            continue;
1803        }
1804
1805        if data[0] == C2S_DISPLAY_RATE && data.len() >= 3 {
1806            let fps = u16::from_le_bytes([data[1], data[2]]) as f32;
1807            if fps > 0.0 {
1808                let mut sess = state.1.lock().await;
1809                if let Some(c) = sess.clients.get_mut(&client_id) {
1810                    c.display_fps = fps;
1811                }
1812            }
1813            nudge_delivery(&state);
1814            continue;
1815        }
1816
1817        if data[0] == C2S_CLIENT_METRICS && data.len() >= 7 {
1818            let backlog_frames = u16::from_le_bytes([data[1], data[2]]);
1819            let ack_ahead_frames = u16::from_le_bytes([data[3], data[4]]);
1820            let apply_ms = u16::from_le_bytes([data[5], data[6]]) as f32 * 0.1;
1821            let mut sess = state.1.lock().await;
1822            if let Some(c) = sess.clients.get_mut(&client_id) {
1823                c.browser_backlog_frames = backlog_frames;
1824                c.browser_ack_ahead_frames = ack_ahead_frames;
1825                c.browser_apply_ms = apply_ms;
1826                c.last_metrics_update = Instant::now();
1827            }
1828            nudge_delivery(&state);
1829            continue;
1830        }
1831
1832        // Server-side mouse: client sends structured mouse data, server generates
1833        // the correct escape sequence using the terminal's current mouse mode/encoding.
1834        if data[0] == C2S_MOUSE && data.len() >= 9 {
1835            let pid = u16::from_le_bytes([data[1], data[2]]);
1836            let type_ = data[3];
1837            let button = data[4];
1838            let col = u16::from_le_bytes([data[5], data[6]]);
1839            let row = u16::from_le_bytes([data[7], data[8]]);
1840            let sess = state.1.lock().await;
1841            if let Some(pty) = sess.ptys.get(&pid) {
1842                let (echo, icanon) = pty.lflag_cache;
1843                if let Some(seq) = pty
1844                    .driver
1845                    .mouse_event(type_, button, col, row, echo, icanon)
1846                    && let Some(&fd) = state.2.read().unwrap().get(&pid)
1847                {
1848                    pty::pty_write_all(fd, &seq);
1849                }
1850            }
1851            continue;
1852        }
1853
1854        if data[0] == C2S_INPUT && data.len() >= 3 {
1855            let pid = u16::from_le_bytes([data[1], data[2]]);
1856            let mut need_nudge = false;
1857            {
1858                let mut sess = state.1.lock().await;
1859                if let Some(c) = sess.clients.get_mut(&client_id)
1860                    && update_client_scroll_state(c, pid, 0)
1861                    && let Some(pty) = sess.ptys.get_mut(&pid)
1862                {
1863                    pty.mark_dirty();
1864                    need_nudge = true;
1865                }
1866            }
1867            if need_nudge {
1868                nudge_delivery(&state);
1869            }
1870            if let Some(&fd) = state.2.read().unwrap().get(&pid) {
1871                pty::pty_write_all(fd, &data[3..]);
1872            }
1873            continue;
1874        }
1875
1876        if data[0] == C2S_SEARCH && data.len() >= 3 {
1877            let request_id = u16::from_le_bytes([data[1], data[2]]);
1878            let query = std::str::from_utf8(&data[3..]).unwrap_or("").trim();
1879            let mut sess = state.1.lock().await;
1880            let lead = sess.clients.get(&client_id).and_then(|c| c.lead);
1881            let mut ranked: Vec<SearchResultRow> = if query.is_empty() {
1882                Vec::new()
1883            } else {
1884                sess.ptys
1885                    .iter()
1886                    .filter_map(|(&pty_id, pty)| {
1887                        pty.driver
1888                            .search_result(query)
1889                            .map(|result| SearchResultRow {
1890                                pty_id,
1891                                score: result.score,
1892                                primary_source: result.primary_source,
1893                                matched_sources: result.matched_sources,
1894                                context: result.context,
1895                                scroll_offset: result.scroll_offset,
1896                            })
1897                    })
1898                    .collect()
1899            };
1900            ranked.sort_by(|a, b| {
1901                b.score
1902                    .cmp(&a.score)
1903                    .then_with(|| (Some(b.pty_id) == lead).cmp(&(Some(a.pty_id) == lead)))
1904                    .then_with(|| a.pty_id.cmp(&b.pty_id))
1905            });
1906            if let Some(client) = sess.clients.get_mut(&client_id) {
1907                let _ = client
1908                    .tx
1909                    .try_send(build_search_results_msg(request_id, &ranked));
1910            }
1911            continue;
1912        }
1913
1914        let mut sess = state.1.lock().await;
1915        let mut need_nudge = false;
1916        match data[0] {
1917            C2S_SCROLL if data.len() >= 7 => {
1918                let pid = u16::from_le_bytes([data[1], data[2]]);
1919                let offset = u32::from_le_bytes([data[3], data[4], data[5], data[6]]) as usize;
1920                if sess.ptys.contains_key(&pid) {
1921                    if let Some(c) = sess.clients.get_mut(&client_id) {
1922                        update_client_scroll_state(c, pid, offset);
1923                    }
1924                    if let Some(pty) = sess.ptys.get_mut(&pid) {
1925                        pty.mark_dirty();
1926                        need_nudge = true;
1927                    }
1928                }
1929            }
1930            C2S_RESIZE if data.len() >= 7 => {
1931                let entries = data[1..].chunks_exact(6);
1932                if !entries.remainder().is_empty() {
1933                    continue;
1934                }
1935                let mut touched = Vec::with_capacity((data.len() - 1) / 6);
1936                for entry in entries {
1937                    let pid = u16::from_le_bytes([entry[0], entry[1]]);
1938                    if !sess.ptys.contains_key(&pid) {
1939                        continue;
1940                    }
1941                    let rows = u16::from_le_bytes([entry[2], entry[3]]);
1942                    let cols = u16::from_le_bytes([entry[4], entry[5]]);
1943                    if let Some(c) = sess.clients.get_mut(&client_id) {
1944                        if is_unset_view_size(rows, cols) {
1945                            if c.view_sizes.remove(&pid).is_some() {
1946                                touched.push(pid);
1947                            }
1948                        } else if rows == 0 || cols == 0 {
1949                            continue;
1950                        } else {
1951                            c.view_sizes.insert(pid, (rows, cols));
1952                            touched.push(pid);
1953                        }
1954                    }
1955                }
1956                if sess.resize_ptys_to_mediated_sizes(touched) {
1957                    need_nudge = true;
1958                }
1959            }
1960            C2S_CREATE => {
1961                // Format: [opcode][rows:2][cols:2][tag_len:2][tag:N][command...]
1962                let (rows, cols) = if data.len() >= 5 {
1963                    (
1964                        u16::from_le_bytes([data[1], data[2]]),
1965                        u16::from_le_bytes([data[3], data[4]]),
1966                    )
1967                } else {
1968                    (24, 80)
1969                };
1970                let tag_len = if data.len() >= 7 {
1971                    u16::from_le_bytes([data[5], data[6]]) as usize
1972                } else {
1973                    0
1974                };
1975                let tag = if data.len() >= 7 + tag_len {
1976                    std::str::from_utf8(&data[7..7 + tag_len]).unwrap_or_default()
1977                } else {
1978                    ""
1979                };
1980                let cmd_start = 7 + tag_len;
1981                let dir: Option<String> = None;
1982                let create_payload = data
1983                    .get(cmd_start..)
1984                    .and_then(|bytes| std::str::from_utf8(bytes).ok());
1985                let command = create_payload
1986                    .filter(|payload| !payload.contains('\0'))
1987                    .map(str::trim)
1988                    .filter(|payload| !payload.is_empty());
1989                let argv: Option<Vec<&str>> = create_payload
1990                    .filter(|payload| payload.contains('\0'))
1991                    .map(|payload| {
1992                        payload
1993                            .split('\0')
1994                            .filter(|arg| !arg.is_empty())
1995                            .collect::<Vec<_>>()
1996                    })
1997                    .filter(|args| !args.is_empty());
1998                let Some(id) = sess.allocate_pty_id() else {
1999                    continue;
2000                };
2001                if let Some(pty) = pty::spawn_pty(
2002                    &config.shell,
2003                    &config.shell_flags,
2004                    rows,
2005                    cols,
2006                    id,
2007                    tag,
2008                    command,
2009                    argv.as_deref(),
2010                    dir.as_deref(),
2011                    config.scrollback,
2012                    state.clone(),
2013                ) {
2014                    let mut msg = Vec::with_capacity(3 + pty.tag.len());
2015                    msg.push(S2C_CREATED);
2016                    msg.extend_from_slice(&id.to_le_bytes());
2017                    msg.extend_from_slice(pty.tag.as_bytes());
2018                    sess.ptys.insert(id, pty);
2019                    if let Some(c) = sess.clients.get_mut(&client_id) {
2020                        c.lead = Some(id);
2021                        c.view_sizes.insert(id, (rows, cols));
2022                        subscribe_client_to(c, id);
2023                        // Per-PTY scroll: no blanket reset needed.
2024                        reset_inflight(c);
2025                    }
2026                    sess.send_to_all(&msg);
2027                    need_nudge = true;
2028                }
2029            }
2030            C2S_CREATE_N => {
2031                // Format: [opcode][nonce:2][rows:2][cols:2][tag_len:2][tag:N][command...]
2032                let nonce = if data.len() >= 3 {
2033                    u16::from_le_bytes([data[1], data[2]])
2034                } else {
2035                    0
2036                };
2037                let (rows, cols) = if data.len() >= 7 {
2038                    (
2039                        u16::from_le_bytes([data[3], data[4]]),
2040                        u16::from_le_bytes([data[5], data[6]]),
2041                    )
2042                } else {
2043                    (24, 80)
2044                };
2045                let tag_len = if data.len() >= 9 {
2046                    u16::from_le_bytes([data[7], data[8]]) as usize
2047                } else {
2048                    0
2049                };
2050                let tag = if data.len() >= 9 + tag_len {
2051                    std::str::from_utf8(&data[9..9 + tag_len]).unwrap_or_default()
2052                } else {
2053                    ""
2054                };
2055                let cmd_start = 9 + tag_len;
2056                let dir: Option<String> = None;
2057                let create_payload = data
2058                    .get(cmd_start..)
2059                    .and_then(|bytes| std::str::from_utf8(bytes).ok());
2060                let command = create_payload
2061                    .filter(|payload| !payload.contains('\0'))
2062                    .map(str::trim)
2063                    .filter(|payload| !payload.is_empty());
2064                let argv: Option<Vec<&str>> = create_payload
2065                    .filter(|payload| payload.contains('\0'))
2066                    .map(|payload| {
2067                        payload
2068                            .split('\0')
2069                            .filter(|arg| !arg.is_empty())
2070                            .collect::<Vec<_>>()
2071                    })
2072                    .filter(|args| !args.is_empty());
2073                let Some(id) = sess.allocate_pty_id() else {
2074                    continue;
2075                };
2076                if let Some(pty) = pty::spawn_pty(
2077                    &config.shell,
2078                    &config.shell_flags,
2079                    rows,
2080                    cols,
2081                    id,
2082                    tag,
2083                    command,
2084                    argv.as_deref(),
2085                    dir.as_deref(),
2086                    config.scrollback,
2087                    state.clone(),
2088                ) {
2089                    let tag_bytes = pty.tag.as_bytes();
2090                    let mut nonce_msg = Vec::with_capacity(5 + tag_bytes.len());
2091                    nonce_msg.push(S2C_CREATED_N);
2092                    nonce_msg.extend_from_slice(&nonce.to_le_bytes());
2093                    nonce_msg.extend_from_slice(&id.to_le_bytes());
2094                    nonce_msg.extend_from_slice(tag_bytes);
2095                    let mut broadcast_msg = Vec::with_capacity(3 + tag_bytes.len());
2096                    broadcast_msg.push(S2C_CREATED);
2097                    broadcast_msg.extend_from_slice(&id.to_le_bytes());
2098                    broadcast_msg.extend_from_slice(tag_bytes);
2099                    sess.ptys.insert(id, pty);
2100                    if let Some(c) = sess.clients.get_mut(&client_id) {
2101                        c.lead = Some(id);
2102                        c.view_sizes.insert(id, (rows, cols));
2103                        subscribe_client_to(c, id);
2104                        // Per-PTY scroll: no blanket reset needed.
2105                        reset_inflight(c);
2106                        let _ = c.tx.try_send(nonce_msg);
2107                    }
2108                    for (&cid, c) in sess.clients.iter() {
2109                        if cid != client_id {
2110                            let _ = c.tx.try_send(broadcast_msg.clone());
2111                        }
2112                    }
2113                    need_nudge = true;
2114                }
2115            }
2116            C2S_CREATE_AT => {
2117                // Format: [opcode][rows:2][cols:2][tag_len:2][tag:N][src_pty_id:2]
2118                let (rows, cols) = if data.len() >= 5 {
2119                    (
2120                        u16::from_le_bytes([data[1], data[2]]),
2121                        u16::from_le_bytes([data[3], data[4]]),
2122                    )
2123                } else {
2124                    (24, 80)
2125                };
2126                let tag_len = if data.len() >= 7 {
2127                    u16::from_le_bytes([data[5], data[6]]) as usize
2128                } else {
2129                    0
2130                };
2131                let tag = if data.len() >= 7 + tag_len {
2132                    std::str::from_utf8(&data[7..7 + tag_len]).unwrap_or_default()
2133                } else {
2134                    ""
2135                };
2136                let src_start = 7 + tag_len;
2137                let dir = if data.len() >= src_start + 2 {
2138                    let src_id = u16::from_le_bytes([data[src_start], data[src_start + 1]]);
2139                    sess.ptys.get(&src_id).and_then(|p| pty::pty_cwd(&p.handle))
2140                } else {
2141                    None
2142                };
2143                let Some(id) = sess.allocate_pty_id() else {
2144                    continue;
2145                };
2146                if let Some(pty) = pty::spawn_pty(
2147                    &config.shell,
2148                    &config.shell_flags,
2149                    rows,
2150                    cols,
2151                    id,
2152                    tag,
2153                    None,
2154                    None,
2155                    dir.as_deref(),
2156                    config.scrollback,
2157                    state.clone(),
2158                ) {
2159                    let mut msg = Vec::with_capacity(3 + pty.tag.len());
2160                    msg.push(S2C_CREATED);
2161                    msg.extend_from_slice(&id.to_le_bytes());
2162                    msg.extend_from_slice(pty.tag.as_bytes());
2163                    sess.ptys.insert(id, pty);
2164                    if let Some(c) = sess.clients.get_mut(&client_id) {
2165                        c.lead = Some(id);
2166                        c.view_sizes.insert(id, (rows, cols));
2167                        subscribe_client_to(c, id);
2168                        // Per-PTY scroll: no blanket reset needed.
2169                        reset_inflight(c);
2170                    }
2171                    sess.send_to_all(&msg);
2172                    need_nudge = true;
2173                }
2174            }
2175            C2S_CREATE2 => {
2176                // Generic create: [0x18][nonce:2][rows:2][cols:2][features:1][tag_len:2][tag:N][...fields]
2177                if data.len() < 10 {
2178                    continue;
2179                }
2180                let nonce = u16::from_le_bytes([data[1], data[2]]);
2181                let rows = u16::from_le_bytes([data[3], data[4]]);
2182                let cols = u16::from_le_bytes([data[5], data[6]]);
2183                let features = data[7];
2184                let tag_len = u16::from_le_bytes([data[8], data[9]]) as usize;
2185                let tag = if data.len() >= 10 + tag_len {
2186                    std::str::from_utf8(&data[10..10 + tag_len]).unwrap_or_default()
2187                } else {
2188                    ""
2189                };
2190                let mut cursor = 10 + tag_len;
2191                let dir = if features & CREATE2_HAS_SRC_PTY != 0 && data.len() >= cursor + 2 {
2192                    let src_id = u16::from_le_bytes([data[cursor], data[cursor + 1]]);
2193                    cursor += 2;
2194                    sess.ptys.get(&src_id).and_then(|p| pty::pty_cwd(&p.handle))
2195                } else {
2196                    None
2197                };
2198                let create_payload = if features & CREATE2_HAS_COMMAND != 0 {
2199                    data.get(cursor..).and_then(|b| std::str::from_utf8(b).ok())
2200                } else {
2201                    None
2202                };
2203                let command = create_payload
2204                    .filter(|p| !p.contains('\0'))
2205                    .map(str::trim)
2206                    .filter(|p| !p.is_empty());
2207                let argv: Option<Vec<&str>> = create_payload
2208                    .filter(|p| p.contains('\0'))
2209                    .map(|p| p.split('\0').filter(|a| !a.is_empty()).collect::<Vec<_>>())
2210                    .filter(|a| !a.is_empty());
2211                let Some(id) = sess.allocate_pty_id() else {
2212                    continue;
2213                };
2214                if let Some(pty) = pty::spawn_pty(
2215                    &config.shell,
2216                    &config.shell_flags,
2217                    rows,
2218                    cols,
2219                    id,
2220                    tag,
2221                    command,
2222                    argv.as_deref(),
2223                    dir.as_deref(),
2224                    config.scrollback,
2225                    state.clone(),
2226                ) {
2227                    let tag_bytes = pty.tag.as_bytes();
2228                    let mut nonce_msg = Vec::with_capacity(5 + tag_bytes.len());
2229                    nonce_msg.push(S2C_CREATED_N);
2230                    nonce_msg.extend_from_slice(&nonce.to_le_bytes());
2231                    nonce_msg.extend_from_slice(&id.to_le_bytes());
2232                    nonce_msg.extend_from_slice(tag_bytes);
2233                    let mut broadcast_msg = Vec::with_capacity(3 + tag_bytes.len());
2234                    broadcast_msg.push(S2C_CREATED);
2235                    broadcast_msg.extend_from_slice(&id.to_le_bytes());
2236                    broadcast_msg.extend_from_slice(tag_bytes);
2237                    sess.ptys.insert(id, pty);
2238                    if let Some(c) = sess.clients.get_mut(&client_id) {
2239                        c.lead = Some(id);
2240                        c.view_sizes.insert(id, (rows, cols));
2241                        subscribe_client_to(c, id);
2242                        // Per-PTY scroll: no blanket reset needed.
2243                        reset_inflight(c);
2244                        let _ = c.tx.try_send(nonce_msg);
2245                    }
2246                    for (&cid, c) in sess.clients.iter() {
2247                        if cid != client_id {
2248                            let _ = c.tx.try_send(broadcast_msg.clone());
2249                        }
2250                    }
2251                    need_nudge = true;
2252                }
2253            }
2254            C2S_FOCUS if data.len() >= 3 => {
2255                let pid = u16::from_le_bytes([data[1], data[2]]);
2256                if sess.ptys.contains_key(&pid) {
2257                    let old_pid = sess.clients.get(&client_id).and_then(|c| c.lead);
2258                    if let Some(c) = sess.clients.get_mut(&client_id) {
2259                        c.lead = Some(pid);
2260                        subscribe_client_to(c, pid);
2261                        if old_pid == Some(pid) {
2262                            update_client_scroll_state(c, pid, 0);
2263                        } else {
2264                            reset_inflight(c);
2265                        }
2266                    }
2267                    if let Some(pty) = sess.ptys.get_mut(&pid) {
2268                        pty.mark_dirty();
2269                        need_nudge = true;
2270                    }
2271                }
2272            }
2273            C2S_SUBSCRIBE if data.len() >= 3 => {
2274                let pid = u16::from_le_bytes([data[1], data[2]]);
2275                if sess.ptys.contains_key(&pid) {
2276                    if let Some(c) = sess.clients.get_mut(&client_id) {
2277                        subscribe_client_to(c, pid);
2278                    }
2279                    if let Some(pty) = sess.ptys.get_mut(&pid) {
2280                        pty.mark_dirty();
2281                    }
2282                    need_nudge = true;
2283                }
2284            }
2285            C2S_UNSUBSCRIBE if data.len() >= 3 => {
2286                let pid = u16::from_le_bytes([data[1], data[2]]);
2287                if sess.ptys.contains_key(&pid) {
2288                    let mut touched = Vec::new();
2289                    if let Some(c) = sess.clients.get_mut(&client_id) {
2290                        if unsubscribe_client_from(c, pid) {
2291                            touched.push(pid);
2292                        }
2293                        reset_inflight(c);
2294                    }
2295                    if sess.resize_ptys_to_mediated_sizes(touched) {
2296                        need_nudge = true;
2297                    }
2298                }
2299            }
2300            C2S_RESTART if data.len() >= 3 => {
2301                let pid = u16::from_le_bytes([data[1], data[2]]);
2302                let restart_info = sess
2303                    .ptys
2304                    .get(&pid)
2305                    .filter(|p| p.exited)
2306                    .map(|p| (p.driver.size(), p.command.clone(), p.tag.clone()));
2307                if let Some(((rows, cols), command, tag)) = restart_info
2308                    && let Some((new_handle, reader, byte_rx)) = pty::respawn_child(
2309                        &state.0.shell,
2310                        &state.0.shell_flags,
2311                        rows,
2312                        cols,
2313                        pid,
2314                        command.as_deref(),
2315                        state.clone(),
2316                    )
2317                {
2318                    let Some(pty) = sess.ptys.get_mut(&pid) else {
2319                        break;
2320                    };
2321                    pty.handle = new_handle;
2322                    pty.reader_handle = reader;
2323                    pty.byte_rx = byte_rx;
2324                    pty.driver.reset_modes();
2325                    pty.exited = false;
2326                    pty.exit_status = blit_remote::EXIT_STATUS_UNKNOWN;
2327                    pty.lflag_cache = pty::pty_lflag(&pty.handle);
2328                    pty.lflag_last = Instant::now();
2329                    pty.mark_dirty();
2330                    if let Some(c) = sess.clients.get_mut(&client_id) {
2331                        c.lead = Some(pid);
2332                        subscribe_client_to(c, pid);
2333                        update_client_scroll_state(c, pid, 0);
2334                        reset_inflight(c);
2335                    }
2336                    let mut msg = Vec::with_capacity(3 + tag.len());
2337                    msg.push(S2C_CREATED);
2338                    msg.extend_from_slice(&pid.to_le_bytes());
2339                    msg.extend_from_slice(tag.as_bytes());
2340                    sess.send_to_all(&msg);
2341                    need_nudge = true;
2342                }
2343            }
2344            C2S_READ if data.len() >= 13 => {
2345                let nonce = u16::from_le_bytes([data[1], data[2]]);
2346                let pid = u16::from_le_bytes([data[3], data[4]]);
2347                let req_offset = u32::from_le_bytes([data[5], data[6], data[7], data[8]]) as usize;
2348                let req_limit =
2349                    u32::from_le_bytes([data[9], data[10], data[11], data[12]]) as usize;
2350                let flags = data.get(13).copied().unwrap_or(0);
2351                let ansi = flags & READ_ANSI != 0;
2352                let tail = flags & READ_TAIL != 0;
2353
2354                if let Some(pty) = sess.ptys.get_mut(&pid) {
2355                    let (rows, _cols) = pty.driver.size();
2356                    let viewport = take_snapshot(pty);
2357                    let scrollback_lines = viewport.scrollback_lines() as usize;
2358                    let total_lines = scrollback_lines + rows as usize;
2359
2360                    let extract = |f: &FrameState| -> String {
2361                        if ansi {
2362                            f.get_ansi_text()
2363                        } else {
2364                            f.get_all_text()
2365                        }
2366                    };
2367
2368                    let mut all_lines: Vec<String> =
2369                        Vec::with_capacity(scrollback_lines + rows as usize);
2370
2371                    let mut scroll_offset = scrollback_lines;
2372                    while scroll_offset > 0 {
2373                        let frame = pty.driver.scrollback_frame(scroll_offset);
2374                        let page = extract(&frame);
2375                        let page_lines: Vec<&str> = page.lines().collect();
2376                        let take = if scroll_offset < rows as usize {
2377                            scroll_offset.min(page_lines.len())
2378                        } else {
2379                            page_lines.len()
2380                        };
2381                        for line in &page_lines[..take] {
2382                            all_lines.push(line.to_string());
2383                        }
2384                        if scroll_offset <= rows as usize {
2385                            break;
2386                        }
2387                        scroll_offset = scroll_offset.saturating_sub(rows as usize);
2388                    }
2389
2390                    for line in extract(&viewport).lines() {
2391                        all_lines.push(line.to_string());
2392                    }
2393
2394                    let (start, end) = if tail {
2395                        let end = all_lines.len().saturating_sub(req_offset);
2396                        let start = if req_limit == 0 {
2397                            0
2398                        } else {
2399                            end.saturating_sub(req_limit)
2400                        };
2401                        (start, end)
2402                    } else {
2403                        let start = req_offset.min(all_lines.len());
2404                        let end = if req_limit == 0 {
2405                            all_lines.len()
2406                        } else {
2407                            (start + req_limit).min(all_lines.len())
2408                        };
2409                        (start, end)
2410                    };
2411                    let text = all_lines[start..end].join("\n");
2412
2413                    let mut msg = Vec::with_capacity(13 + text.len());
2414                    msg.push(S2C_TEXT);
2415                    msg.extend_from_slice(&nonce.to_le_bytes());
2416                    msg.extend_from_slice(&pid.to_le_bytes());
2417                    msg.extend_from_slice(&(total_lines as u32).to_le_bytes());
2418                    msg.extend_from_slice(&(start as u32).to_le_bytes());
2419                    msg.extend_from_slice(text.as_bytes());
2420                    if let Some(client) = sess.clients.get(&client_id) {
2421                        let _ = client.tx.try_send(msg);
2422                    }
2423                }
2424            }
2425            C2S_COPY_RANGE if data.len() >= 18 => {
2426                let nonce = u16::from_le_bytes([data[1], data[2]]);
2427                let pid = u16::from_le_bytes([data[3], data[4]]);
2428                let start_tail = u32::from_le_bytes([data[5], data[6], data[7], data[8]]);
2429                let start_col = u16::from_le_bytes([data[9], data[10]]);
2430                let end_tail = u32::from_le_bytes([data[11], data[12], data[13], data[14]]);
2431                let end_col = u16::from_le_bytes([data[15], data[16]]);
2432
2433                if let Some(pty) = sess.ptys.get(&pid) {
2434                    let text = pty
2435                        .driver
2436                        .get_text_range(start_tail, start_col, end_tail, end_col);
2437                    let total_lines = pty.driver.total_lines();
2438
2439                    let mut msg = Vec::with_capacity(13 + text.len());
2440                    msg.push(S2C_TEXT);
2441                    msg.extend_from_slice(&nonce.to_le_bytes());
2442                    msg.extend_from_slice(&pid.to_le_bytes());
2443                    msg.extend_from_slice(&total_lines.to_le_bytes());
2444                    msg.extend_from_slice(&start_tail.to_le_bytes());
2445                    msg.extend_from_slice(text.as_bytes());
2446                    if let Some(client) = sess.clients.get(&client_id) {
2447                        let _ = client.tx.try_send(msg);
2448                    }
2449                }
2450            }
2451            C2S_KILL if data.len() >= 7 => {
2452                let pid = u16::from_le_bytes([data[1], data[2]]);
2453                let signal = i32::from_le_bytes([data[3], data[4], data[5], data[6]]);
2454                if let Some(pty) = sess.ptys.get(&pid)
2455                    && !pty.exited
2456                {
2457                    pty::kill_pty(&pty.handle, signal);
2458                }
2459            }
2460            C2S_CLOSE if data.len() >= 3 => {
2461                let pid = u16::from_le_bytes([data[1], data[2]]);
2462                if let Some(pty) = sess.ptys.remove(&pid) {
2463                    if !pty.exited {
2464                        state.2.write().unwrap().remove(&pid);
2465                        drop(pty.reader_handle);
2466                        pty::close_pty(&pty.handle);
2467                    }
2468                    for client in sess.clients.values_mut() {
2469                        unsubscribe_client_from(client, pid);
2470                    }
2471                    let mut msg = vec![S2C_CLOSED];
2472                    msg.extend_from_slice(&pid.to_le_bytes());
2473                    sess.send_to_all(&msg);
2474                }
2475            }
2476            _ => {}
2477        }
2478        drop(sess);
2479        if need_nudge {
2480            nudge_delivery(&state);
2481        }
2482    }
2483
2484    {
2485        let mut sess = state.1.lock().await;
2486        let mut need_nudge = false;
2487        let affected_ptys = sess
2488            .clients
2489            .remove(&client_id)
2490            .map(|client| client.view_sizes.keys().copied().collect::<Vec<_>>())
2491            .unwrap_or_default();
2492        if sess.resize_ptys_to_mediated_sizes(affected_ptys) {
2493            need_nudge = true;
2494        }
2495        drop(sess);
2496        if need_nudge {
2497            nudge_delivery(&state);
2498        }
2499    }
2500    sender.abort();
2501    if state.0.verbose {
2502        eprintln!("client disconnected");
2503    }
2504}
2505
2506#[cfg(test)]
2507mod tests {
2508    use super::*;
2509
2510    fn test_client_with_capacity(capacity: usize) -> (ClientState, mpsc::Receiver<Vec<u8>>) {
2511        let (tx, rx) = mpsc::channel(capacity);
2512        let client = ClientState {
2513            tx,
2514            lead: None,
2515            subscriptions: HashSet::new(),
2516            view_sizes: HashMap::new(),
2517            scroll_offsets: HashMap::new(),
2518            scroll_caches: HashMap::new(),
2519            last_sent: HashMap::new(),
2520            preview_next_send_at: HashMap::new(),
2521            rtt_ms: 50.0,
2522            min_rtt_ms: 50.0,
2523            display_fps: 60.0,
2524            delivery_bps: 262_144.0,
2525            goodput_bps: 262_144.0,
2526            goodput_jitter_bps: 0.0,
2527            max_goodput_jitter_bps: 0.0,
2528            last_goodput_sample_bps: 0.0,
2529            avg_frame_bytes: 1_024.0,
2530            avg_paced_frame_bytes: 1_024.0,
2531            avg_preview_frame_bytes: 1_024.0,
2532            inflight_bytes: 0,
2533            inflight_frames: VecDeque::new(),
2534            next_send_at: Instant::now(),
2535            probe_frames: 0.0,
2536            frames_sent: 0,
2537            acks_recv: 0,
2538            acked_bytes_since_log: 0,
2539            browser_backlog_frames: 0,
2540            browser_ack_ahead_frames: 0,
2541            browser_apply_ms: 0.0,
2542            last_metrics_update: Instant::now(),
2543            last_log: Instant::now(),
2544            goodput_window_bytes: 0,
2545            goodput_window_start: Instant::now(),
2546        };
2547        (client, rx)
2548    }
2549
2550    fn test_client() -> ClientState {
2551        let (client, _rx) = test_client_with_capacity(OUTBOX_CAPACITY);
2552        client
2553    }
2554
2555    fn fill_inflight(client: &mut ClientState, frames: usize, bytes_per_frame: usize) {
2556        let now = Instant::now();
2557        client.inflight_bytes = frames.saturating_mul(bytes_per_frame);
2558        client.inflight_frames = (0..frames)
2559            .map(|_| InFlightFrame {
2560                sent_at: now,
2561                bytes: bytes_per_frame,
2562                paced: true,
2563            })
2564            .collect();
2565    }
2566
2567    fn sample_frame(text: &str) -> FrameState {
2568        let mut frame = FrameState::new(2, 8);
2569        frame.write_text(0, 0, text, blit_remote::CellStyle::default());
2570        frame
2571    }
2572
2573    #[test]
2574    fn unset_view_size_accepts_zero_pair_only() {
2575        assert!(is_unset_view_size(0, 0));
2576        assert!(!is_unset_view_size(0, 80));
2577        assert!(!is_unset_view_size(u16::MAX, u16::MAX));
2578    }
2579
2580    #[test]
2581    fn unsubscribe_client_from_clears_view_size() {
2582        let mut client = test_client();
2583        client.subscriptions.insert(7);
2584        client.view_sizes.insert(7, (24, 80));
2585        assert!(unsubscribe_client_from(&mut client, 7));
2586        assert!(!client.subscriptions.contains(&7));
2587        assert!(!client.view_sizes.contains_key(&7));
2588    }
2589
2590    #[test]
2591    fn mediated_size_uses_per_pty_view_sizes_without_lead() {
2592        let mut session = Session::new();
2593        let mut c1 = test_client();
2594        let mut c2 = test_client();
2595        c1.view_sizes.insert(7, (30, 120));
2596        c2.view_sizes.insert(7, (24, 100));
2597        session.clients.insert(1, c1);
2598        session.clients.insert(2, c2);
2599        assert_eq!(session.mediated_size_for_pty(7), Some((24, 100)));
2600    }
2601
2602    #[test]
2603    fn due_preview_reserves_the_last_lead_slot() {
2604        let mut client = test_client();
2605        client.lead = Some(1);
2606        client.subscriptions.insert(1);
2607        client.subscriptions.insert(2);
2608
2609        let target_frames = target_frame_window(&client);
2610        let lead_limit = target_frames.saturating_sub(1).max(1);
2611        fill_inflight(&mut client, lead_limit, 512);
2612
2613        assert!(window_open(&client));
2614        assert!(lead_window_open(&client, false));
2615        assert!(!lead_window_open(&client, true));
2616        assert!(can_send_preview(&client, 2, Instant::now()));
2617    }
2618
2619    #[test]
2620    fn entering_scrollback_uses_current_visible_frame_as_baseline() {
2621        let mut client = test_client();
2622        let live = sample_frame("live");
2623        client.lead = Some(7);
2624        client.subscriptions.insert(7);
2625        client.last_sent.insert(7, live.clone());
2626
2627        assert!(update_client_scroll_state(&mut client, 7, 12));
2628        assert_eq!(client.scroll_offsets.get(&7), Some(&12));
2629        assert_eq!(client.scroll_caches.get(&7), Some(&live));
2630    }
2631
2632    #[test]
2633    fn leaving_scrollback_seeds_live_diff_from_scrollback_view() {
2634        let mut client = test_client();
2635        let history = sample_frame("hist");
2636        client.lead = Some(7);
2637        client.subscriptions.insert(7);
2638        client.scroll_offsets.insert(7, 12);
2639        client.scroll_caches.insert(7, history.clone());
2640
2641        assert!(update_client_scroll_state(&mut client, 7, 0));
2642        assert_eq!(client.scroll_offsets.get(&7), None);
2643        assert_eq!(client.last_sent.get(&7), Some(&history));
2644        assert_eq!(client.scroll_caches.get(&7), None);
2645    }
2646
2647    // ── frame_window ──
2648
2649    #[test]
2650    fn frame_window_minimum_is_two() {
2651        assert!(frame_window(0.0, 60.0) >= 2);
2652    }
2653
2654    #[test]
2655    fn frame_window_scales_with_rtt() {
2656        let low = frame_window(10.0, 60.0);
2657        let high = frame_window(200.0, 60.0);
2658        assert!(high > low, "higher RTT should need more frames in flight");
2659    }
2660
2661    #[test]
2662    fn frame_window_scales_with_fps() {
2663        let slow = frame_window(100.0, 10.0);
2664        let fast = frame_window(100.0, 120.0);
2665        assert!(fast > slow, "higher fps should need more frames in flight");
2666    }
2667
2668    #[test]
2669    fn frame_window_zero_rtt() {
2670        assert!(frame_window(0.0, 120.0) >= 2);
2671    }
2672
2673    // ── path_rtt_ms ──
2674
2675    #[test]
2676    fn path_rtt_ms_uses_min_when_positive() {
2677        let mut client = test_client();
2678        client.rtt_ms = 100.0;
2679        client.min_rtt_ms = 30.0;
2680        assert_eq!(path_rtt_ms(&client), 30.0);
2681    }
2682
2683    #[test]
2684    fn path_rtt_ms_falls_back_to_rtt_when_min_zero() {
2685        let mut client = test_client();
2686        client.rtt_ms = 80.0;
2687        client.min_rtt_ms = 0.0;
2688        assert_eq!(path_rtt_ms(&client), 80.0);
2689    }
2690
2691    // ── ewma_with_direction ──
2692
2693    #[test]
2694    fn ewma_rising_uses_rise_alpha() {
2695        let result = ewma_with_direction(100.0, 200.0, 0.5, 0.1);
2696        // rise: 100 * 0.5 + 200 * 0.5 = 150
2697        assert!((result - 150.0).abs() < 0.01);
2698    }
2699
2700    #[test]
2701    fn ewma_falling_uses_fall_alpha() {
2702        let result = ewma_with_direction(200.0, 100.0, 0.5, 0.1);
2703        // fall: 200 * 0.9 + 100 * 0.1 = 190
2704        assert!((result - 190.0).abs() < 0.01);
2705    }
2706
2707    #[test]
2708    fn ewma_same_value_unchanged() {
2709        let result = ewma_with_direction(50.0, 50.0, 0.5, 0.5);
2710        assert!((result - 50.0).abs() < 0.01);
2711    }
2712
2713    // ── advance_deadline ──
2714
2715    #[test]
2716    fn advance_deadline_steps_forward() {
2717        let now = Instant::now();
2718        let mut deadline = now;
2719        let interval = Duration::from_millis(16);
2720        advance_deadline(&mut deadline, now, interval);
2721        assert!(deadline > now);
2722        assert!(deadline <= now + interval + Duration::from_micros(100));
2723    }
2724
2725    #[test]
2726    fn advance_deadline_resets_when_far_behind() {
2727        let now = Instant::now();
2728        // deadline is way in the past (more than 2 intervals ago)
2729        let mut deadline = now - Duration::from_secs(10);
2730        let interval = Duration::from_millis(16);
2731        advance_deadline(&mut deadline, now, interval);
2732        // Should snap to now + interval since scheduled + interval < now
2733        assert!(deadline >= now);
2734    }
2735
2736    #[test]
2737    fn should_snapshot_pty_requires_dirty_and_needful() {
2738        assert!(should_snapshot_pty(true, true, false));
2739        assert!(!should_snapshot_pty(false, true, false));
2740        assert!(!should_snapshot_pty(true, false, false));
2741    }
2742
2743    #[test]
2744    fn should_snapshot_pty_defers_synced_output() {
2745        assert!(!should_snapshot_pty(true, true, true));
2746        assert!(should_snapshot_pty(true, true, false));
2747    }
2748
2749    #[test]
2750    fn enqueue_ready_frame_refuses_new_frames_when_capped() {
2751        let mut queue = VecDeque::new();
2752        for cols in 1..=(READY_FRAME_QUEUE_CAP as u16) {
2753            assert!(enqueue_ready_frame(&mut queue, FrameState::new(1, cols)));
2754        }
2755        assert!(!enqueue_ready_frame(
2756            &mut queue,
2757            FrameState::new(1, READY_FRAME_QUEUE_CAP as u16 + 1),
2758        ));
2759        assert_eq!(queue.len(), READY_FRAME_QUEUE_CAP);
2760        assert_eq!(queue.front().map(FrameState::cols), Some(1));
2761        assert_eq!(
2762            queue.back().map(FrameState::cols),
2763            Some(READY_FRAME_QUEUE_CAP as u16),
2764        );
2765    }
2766
2767    #[test]
2768    fn find_sync_output_end_returns_end_of_first_close_sequence() {
2769        let bytes = b"abc\x1b[?2026lrest\x1b[?2026l";
2770        assert_eq!(find_sync_output_end(&[], bytes), Some(11));
2771    }
2772
2773    #[test]
2774    fn find_sync_output_end_returns_none_without_close_sequence() {
2775        assert_eq!(find_sync_output_end(&[], b"\x1b[?2026hpartial"), None);
2776    }
2777
2778    #[test]
2779    fn find_sync_output_end_detects_boundary_split_across_reads() {
2780        assert_eq!(find_sync_output_end(b"abc\x1b[?20", b"26lrest"), Some(3));
2781    }
2782
2783    #[test]
2784    fn update_sync_scan_tail_keeps_recent_suffix_only() {
2785        let mut tail = Vec::new();
2786        update_sync_scan_tail(&mut tail, b"123456789");
2787        assert_eq!(tail, b"3456789");
2788    }
2789
2790    // ── window_saturated ──
2791
2792    #[test]
2793    fn window_saturated_at_90_percent_frames() {
2794        let client = test_client();
2795        let target = target_frame_window(&client);
2796        let frames_90 = (target * 9).div_ceil(10); // ceil(target * 0.9)
2797        assert!(window_saturated(&client, frames_90, 0));
2798    }
2799
2800    #[test]
2801    fn window_saturated_not_at_low_usage() {
2802        let client = test_client();
2803        assert!(!window_saturated(&client, 1, 0));
2804    }
2805
2806    #[test]
2807    fn window_saturated_at_90_percent_bytes() {
2808        let client = test_client();
2809        let target_bytes = target_byte_window(&client);
2810        let bytes_90 = (target_bytes * 9).div_ceil(10);
2811        assert!(window_saturated(&client, 0, bytes_90));
2812    }
2813
2814    // ── outbox_queued_frames / outbox_backpressured ──
2815
2816    #[test]
2817    fn outbox_queued_frames_zero_when_empty() {
2818        let client = test_client();
2819        assert_eq!(outbox_queued_frames(&client), 0);
2820    }
2821
2822    #[test]
2823    fn outbox_backpressured_when_queue_full() {
2824        let (client, _rx) = test_client_with_capacity(OUTBOX_CAPACITY);
2825        // Fill the channel to trigger backpressure
2826        for _ in 0..OUTBOX_SOFT_QUEUE_LIMIT_FRAMES {
2827            let _ = client.tx.try_send(vec![0u8]);
2828        }
2829        assert!(outbox_backpressured(&client));
2830    }
2831
2832    #[test]
2833    fn outbox_not_backpressured_when_empty() {
2834        let client = test_client();
2835        assert!(!outbox_backpressured(&client));
2836    }
2837
2838    // ── browser_pacing_fps baseline ──
2839
2840    #[test]
2841    fn browser_pacing_fps_matches_display_fps_when_browser_ready() {
2842        let mut client = test_client();
2843        client.rtt_ms = 1.0;
2844        client.min_rtt_ms = 1.0;
2845        client.browser_backlog_frames = 0;
2846        client.browser_ack_ahead_frames = 0;
2847        client.browser_apply_ms = 0.0;
2848        client.goodput_bps = 1_000_000.0;
2849        client.delivery_bps = 1_000_000.0;
2850        client.display_fps = 144.0;
2851        assert!((browser_pacing_fps(&client) - 144.0).abs() < 0.01);
2852    }
2853
2854    #[test]
2855    fn browser_pacing_fps_drops_below_display_fps_when_backlogged() {
2856        let mut client = test_client();
2857        client.browser_backlog_frames = 20;
2858        let fps = browser_pacing_fps(&client);
2859        assert!(fps >= 1.0);
2860        assert!(fps < client.display_fps);
2861    }
2862
2863    // ── effective_rtt_ms ──
2864
2865    #[test]
2866    fn effective_rtt_ms_equals_path_when_queue_is_empty() {
2867        let mut client = test_client();
2868        client.rtt_ms = 1.0;
2869        client.min_rtt_ms = 1.0;
2870        client.browser_backlog_frames = 0;
2871        client.browser_ack_ahead_frames = 0;
2872        client.browser_apply_ms = 0.0;
2873        client.goodput_bps = 1_000_000.0;
2874        client.delivery_bps = 1_000_000.0;
2875        assert!((effective_rtt_ms(&client) - 1.0).abs() < 0.01);
2876    }
2877
2878    #[test]
2879    fn effective_rtt_ms_at_least_path_rtt() {
2880        let client = test_client();
2881        assert!(effective_rtt_ms(&client) >= path_rtt_ms(&client));
2882    }
2883
2884    // ── target_frame_window ──
2885
2886    #[test]
2887    fn target_frame_window_at_least_two() {
2888        let client = test_client();
2889        assert!(target_frame_window(&client) >= 2);
2890    }
2891
2892    #[test]
2893    fn target_frame_window_grows_with_probe() {
2894        let mut client = test_client();
2895        let base = target_frame_window(&client);
2896        client.probe_frames = 10.0;
2897        let probed = target_frame_window(&client);
2898        assert!(probed > base, "probe_frames should grow the window");
2899    }
2900
2901    // ── bandwidth_floor_bps ──
2902
2903    #[test]
2904    fn bandwidth_floor_bps_at_least_16k() {
2905        let mut client = test_client();
2906        client.goodput_bps = 0.0;
2907        client.delivery_bps = 0.0;
2908        assert_eq!(bandwidth_floor_bps(&client), 0.0);
2909    }
2910
2911    #[test]
2912    fn bandwidth_floor_bps_scales_with_goodput() {
2913        let mut client = test_client();
2914        client.goodput_bps = 1_000_000.0;
2915        client.delivery_bps = 1_000_000.0;
2916        let floor = bandwidth_floor_bps(&client);
2917        assert!(floor > 0.0);
2918    }
2919
2920    #[test]
2921    fn browser_ready_delivery_floor_can_drive_large_frames_to_display_fps() {
2922        let mut client = test_client();
2923        client.display_fps = 60.0;
2924        client.browser_backlog_frames = 0;
2925        client.browser_ack_ahead_frames = 0;
2926        client.browser_apply_ms = 0.2;
2927        client.goodput_bps = 3_000_000.0;
2928        client.delivery_bps = 9_500_000.0;
2929        client.last_goodput_sample_bps = 3_000_000.0;
2930        client.avg_paced_frame_bytes = 150_000.0;
2931        client.avg_preview_frame_bytes = 1_024.0;
2932        client.avg_frame_bytes = 150_000.0;
2933
2934        assert!(
2935            (pacing_fps(&client) - client.display_fps).abs() < 0.01,
2936            "browser-ready delivery floor should let large frames reach display_fps on a fast path",
2937        );
2938    }
2939
2940    // ── pacing_fps ──
2941
2942    #[test]
2943    fn pacing_fps_zero_when_no_bandwidth() {
2944        let mut client = test_client();
2945        client.goodput_bps = 0.0;
2946        client.delivery_bps = 0.0;
2947        client.last_goodput_sample_bps = 0.0;
2948        assert!(
2949            pacing_fps(&client) == 0.0,
2950            "pacing_fps should be 0 with zero bandwidth"
2951        );
2952    }
2953
2954    #[test]
2955    fn pacing_fps_reaches_display_fps_when_not_bandwidth_limited() {
2956        let mut client = test_client();
2957        client.rtt_ms = 1.0;
2958        client.min_rtt_ms = 1.0;
2959        client.browser_backlog_frames = 0;
2960        client.browser_ack_ahead_frames = 0;
2961        client.browser_apply_ms = 0.0;
2962        client.goodput_bps = 1_000_000.0;
2963        client.delivery_bps = 1_000_000.0;
2964        client.display_fps = 60.0;
2965        assert!((pacing_fps(&client) - 60.0).abs() < 0.01);
2966    }
2967
2968    // ── throughput_limited ──
2969
2970    #[test]
2971    fn throughput_limited_when_low_bandwidth() {
2972        let mut client = test_client();
2973        client.goodput_bps = 1_000.0;
2974        client.delivery_bps = 1_000.0;
2975        client.last_goodput_sample_bps = 0.0;
2976        assert!(throughput_limited(&client));
2977    }
2978
2979    #[test]
2980    fn throughput_not_limited_with_high_bandwidth() {
2981        let mut client = test_client();
2982        client.goodput_bps = 100_000_000.0;
2983        client.delivery_bps = 100_000_000.0;
2984        assert!(!throughput_limited(&client));
2985    }
2986
2987    // ── browser_pacing_fps ──
2988
2989    #[test]
2990    fn browser_pacing_fps_at_least_one() {
2991        let client = test_client();
2992        assert!(browser_pacing_fps(&client) >= 1.0);
2993    }
2994
2995    #[test]
2996    fn browser_pacing_fps_reduced_by_high_backlog() {
2997        let mut client = test_client();
2998        let normal = browser_pacing_fps(&client);
2999        client.browser_backlog_frames = 20;
3000        let backlogged = browser_pacing_fps(&client);
3001        assert!(backlogged < normal, "high backlog should reduce pacing fps");
3002    }
3003
3004    #[test]
3005    fn browser_pacing_fps_reduced_by_high_ack_ahead() {
3006        let mut client = test_client();
3007        let normal = browser_pacing_fps(&client);
3008        client.browser_ack_ahead_frames = 10;
3009        let ahead = browser_pacing_fps(&client);
3010        assert!(ahead < normal, "high ack_ahead should reduce pacing fps");
3011    }
3012
3013    // ── browser_backlog_blocked ──
3014
3015    #[test]
3016    fn browser_backlog_blocked_over_threshold() {
3017        let mut client = test_client();
3018        client.browser_backlog_frames = 9;
3019        assert!(browser_backlog_blocked(&client));
3020    }
3021
3022    #[test]
3023    fn browser_backlog_not_blocked_under_threshold() {
3024        let mut client = test_client();
3025        client.browser_backlog_frames = 8;
3026        assert!(!browser_backlog_blocked(&client));
3027    }
3028
3029    // ── byte_budget_for ──
3030
3031    #[test]
3032    fn byte_budget_for_at_least_one_frame() {
3033        let client = test_client();
3034        let budget = byte_budget_for(&client, 10.0);
3035        assert!(budget >= client.avg_frame_bytes.max(256.0) as usize);
3036    }
3037
3038    #[test]
3039    fn byte_budget_for_grows_with_time() {
3040        let client = test_client();
3041        let short = byte_budget_for(&client, 10.0);
3042        let long = byte_budget_for(&client, 1000.0);
3043        assert!(long >= short);
3044    }
3045
3046    // ── target_byte_window ──
3047
3048    #[test]
3049    fn target_byte_window_positive() {
3050        let client = test_client();
3051        assert!(target_byte_window(&client) > 0);
3052    }
3053
3054    #[test]
3055    fn target_byte_window_covers_frame_window() {
3056        let client = test_client();
3057        let byte_win = target_byte_window(&client);
3058        let frame_win = target_frame_window(&client);
3059        let min_bytes =
3060            (client.avg_paced_frame_bytes.max(256.0) * frame_win.max(2) as f32).ceil() as usize;
3061        assert!(
3062            byte_win >= min_bytes,
3063            "byte window should cover at least frame_window worth of paced frames"
3064        );
3065    }
3066
3067    // ── send_interval ──
3068
3069    #[test]
3070    fn send_interval_matches_browser_pacing() {
3071        let client = test_client();
3072        let interval = send_interval(&client);
3073        let expected = Duration::from_secs_f64(1.0 / browser_pacing_fps(&client) as f64);
3074        let diff = interval.abs_diff(expected);
3075        assert!(diff < Duration::from_micros(10));
3076    }
3077
3078    // ── preview_fps ──
3079
3080    #[test]
3081    fn preview_fps_at_least_one() {
3082        let client = test_client();
3083        assert!(preview_fps(&client) >= 1.0);
3084    }
3085
3086    // ── window_open ──
3087
3088    #[test]
3089    fn window_open_initially() {
3090        let client = test_client();
3091        assert!(window_open(&client));
3092    }
3093
3094    #[test]
3095    fn window_open_false_when_browser_blocked() {
3096        let mut client = test_client();
3097        client.browser_backlog_frames = 20;
3098        assert!(!window_open(&client));
3099    }
3100
3101    #[test]
3102    fn window_open_false_when_inflight_full() {
3103        let mut client = test_client();
3104        let target = target_frame_window(&client);
3105        fill_inflight(&mut client, target + 10, 1024);
3106        assert!(!window_open(&client));
3107    }
3108
3109    // ── lead_window_open ──
3110
3111    #[test]
3112    fn lead_window_open_no_reserve_same_as_window_open() {
3113        let client = test_client();
3114        assert_eq!(lead_window_open(&client, false), window_open(&client));
3115    }
3116
3117    #[test]
3118    fn lead_window_open_reserves_preview_slot() {
3119        let mut client = test_client();
3120        client.lead = Some(1);
3121        client.subscriptions.insert(1);
3122        let target = target_frame_window(&client);
3123        // Fill to just under target minus reserve
3124        fill_inflight(&mut client, target.saturating_sub(1), 512);
3125        // Without reserve: may still be open
3126        // With reserve: should be closed
3127        assert!(!lead_window_open(&client, true));
3128    }
3129
3130    // ── can_send_frame ──
3131
3132    #[test]
3133    fn can_send_frame_when_window_open_and_time_due() {
3134        let mut client = test_client();
3135        client.next_send_at = Instant::now() - Duration::from_millis(100);
3136        assert!(can_send_frame(&client, Instant::now(), false));
3137    }
3138
3139    #[test]
3140    fn can_send_frame_false_when_not_due() {
3141        let mut client = test_client();
3142        client.next_send_at = Instant::now() + Duration::from_secs(10);
3143        assert!(!can_send_frame(&client, Instant::now(), false));
3144    }
3145
3146    #[test]
3147    fn can_send_frame_false_when_window_closed() {
3148        let mut client = test_client();
3149        client.browser_backlog_frames = 20; // triggers browser_backlog_blocked
3150        client.next_send_at = Instant::now() - Duration::from_millis(100);
3151        assert!(!can_send_frame(&client, Instant::now(), false));
3152    }
3153
3154    // ── record_send / record_ack state transitions ──
3155
3156    #[test]
3157    fn record_send_increases_inflight() {
3158        let mut client = test_client();
3159        let now = Instant::now();
3160        assert_eq!(client.inflight_bytes, 0);
3161        assert_eq!(client.inflight_frames.len(), 0);
3162
3163        record_send(&mut client, 1000, now, true);
3164        assert_eq!(client.inflight_bytes, 1000);
3165        assert_eq!(client.inflight_frames.len(), 1);
3166
3167        record_send(&mut client, 500, now, false);
3168        assert_eq!(client.inflight_bytes, 1500);
3169        assert_eq!(client.inflight_frames.len(), 2);
3170    }
3171
3172    #[test]
3173    fn record_send_paced_advances_deadline() {
3174        let mut client = test_client();
3175        let now = Instant::now();
3176        client.next_send_at = now;
3177        record_send(&mut client, 1000, now, true);
3178        assert!(client.next_send_at > now);
3179    }
3180
3181    #[test]
3182    fn record_send_unpaced_does_not_advance_deadline() {
3183        let mut client = test_client();
3184        let now = Instant::now();
3185        let before = client.next_send_at;
3186        record_send(&mut client, 1000, now, false);
3187        assert_eq!(client.next_send_at, before);
3188    }
3189
3190    #[test]
3191    fn record_ack_decreases_inflight() {
3192        let mut client = test_client();
3193        let now = Instant::now();
3194        record_send(&mut client, 1000, now, true);
3195        record_send(&mut client, 500, now, true);
3196        assert_eq!(client.inflight_frames.len(), 2);
3197
3198        record_ack(&mut client);
3199        assert_eq!(client.inflight_frames.len(), 1);
3200        assert_eq!(client.inflight_bytes, 500);
3201    }
3202
3203    #[test]
3204    fn record_ack_on_empty_clears_bytes() {
3205        let mut client = test_client();
3206        client.inflight_bytes = 999; // stale state
3207        record_ack(&mut client);
3208        assert_eq!(client.inflight_bytes, 0);
3209    }
3210
3211    #[test]
3212    fn record_ack_updates_rtt_estimate() {
3213        let mut client = test_client();
3214        let now = Instant::now();
3215        client.inflight_frames.push_back(InFlightFrame {
3216            sent_at: now - Duration::from_millis(20),
3217            bytes: 512,
3218            paced: true,
3219        });
3220        client.inflight_bytes = 512;
3221        let old_rtt = client.rtt_ms;
3222        record_ack(&mut client);
3223        // RTT should have been updated (moved toward ~20ms from the default 50ms)
3224        assert!(
3225            (client.rtt_ms - old_rtt).abs() > 0.01,
3226            "rtt_ms should be updated after ack"
3227        );
3228    }
3229
3230    #[test]
3231    fn record_ack_paced_updates_avg_paced_frame_bytes() {
3232        let mut client = test_client();
3233        let now = Instant::now();
3234        client.inflight_frames.push_back(InFlightFrame {
3235            sent_at: now - Duration::from_millis(10),
3236            bytes: 4096,
3237            paced: true,
3238        });
3239        client.inflight_bytes = 4096;
3240        let old_avg = client.avg_paced_frame_bytes;
3241        record_ack(&mut client);
3242        // Should move toward 4096 from 1024
3243        assert!(client.avg_paced_frame_bytes > old_avg);
3244    }
3245
3246    #[test]
3247    fn record_ack_unpaced_updates_avg_preview_frame_bytes() {
3248        let mut client = test_client();
3249        let now = Instant::now();
3250        client.inflight_frames.push_back(InFlightFrame {
3251            sent_at: now - Duration::from_millis(10),
3252            bytes: 8192,
3253            paced: false,
3254        });
3255        client.inflight_bytes = 8192;
3256        let old_avg = client.avg_preview_frame_bytes;
3257        record_ack(&mut client);
3258        assert!(client.avg_preview_frame_bytes > old_avg);
3259    }
3260
3261    // ── Session::pty_list_msg format ──
3262
3263    #[test]
3264    fn pty_list_msg_empty_session() {
3265        let sess = Session::new();
3266        let msg = sess.pty_list_msg();
3267        assert_eq!(msg[0], S2C_LIST);
3268        assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 0);
3269        assert_eq!(msg.len(), 3);
3270    }
3271
3272    #[test]
3273    fn pty_list_msg_includes_tags() {
3274        let _sess = Session::new();
3275        // Insert minimal Pty entries. We can't call spawn_pty, so build
3276        // a mock-like Pty with a stub driver. Instead, directly insert
3277        // into the HashMap using an unsafe-free approach: just build the
3278        // wire message by hand and verify against a known layout.
3279        //
3280        // The wire format is: [S2C_LIST] [count:u16le] [id:u16le tag_len:u16le tag_bytes]...
3281        //
3282        // Since we can't easily construct a Pty without forking, verify
3283        // the format by constructing the expected bytes and comparing.
3284        let tag1 = "shell";
3285        let tag2 = "build";
3286
3287        // Expected wire for ptys {1 => "shell", 3 => "build"} sorted by id:
3288        let mut expected = vec![S2C_LIST];
3289        expected.extend_from_slice(&2u16.to_le_bytes());
3290        // id=1
3291        expected.extend_from_slice(&1u16.to_le_bytes());
3292        expected.extend_from_slice(&(tag1.len() as u16).to_le_bytes());
3293        expected.extend_from_slice(tag1.as_bytes());
3294        // id=3
3295        expected.extend_from_slice(&3u16.to_le_bytes());
3296        expected.extend_from_slice(&(tag2.len() as u16).to_le_bytes());
3297        expected.extend_from_slice(tag2.as_bytes());
3298
3299        // Verify our expected format starts with S2C_LIST and has correct count
3300        assert_eq!(expected[0], S2C_LIST);
3301        assert_eq!(u16::from_le_bytes([expected[1], expected[2]]), 2);
3302        // Verify tags are embedded
3303        let msg_str = String::from_utf8_lossy(&expected);
3304        assert!(msg_str.contains("shell"));
3305        assert!(msg_str.contains("build"));
3306    }
3307
3308    // ── can_send_preview / record_preview_send ──
3309
3310    #[test]
3311    fn can_send_preview_true_when_due() {
3312        let mut client = test_client();
3313        let now = Instant::now();
3314        client
3315            .preview_next_send_at
3316            .insert(5, now - Duration::from_millis(100));
3317        assert!(can_send_preview(&client, 5, now));
3318    }
3319
3320    #[test]
3321    fn can_send_preview_false_when_not_due() {
3322        let mut client = test_client();
3323        let now = Instant::now();
3324        client
3325            .preview_next_send_at
3326            .insert(5, now + Duration::from_secs(10));
3327        assert!(!can_send_preview(&client, 5, now));
3328    }
3329
3330    #[test]
3331    fn can_send_preview_false_when_window_closed() {
3332        let mut client = test_client();
3333        client.browser_backlog_frames = 20;
3334        let now = Instant::now();
3335        assert!(!can_send_preview(&client, 5, now));
3336    }
3337
3338    #[test]
3339    fn can_send_preview_true_for_unseen_pid() {
3340        let client = test_client();
3341        let now = Instant::now();
3342        // No entry in preview_next_send_at means deadline defaults to now
3343        assert!(can_send_preview(&client, 99, now));
3344    }
3345
3346    #[test]
3347    fn record_preview_send_sets_future_deadline() {
3348        let mut client = test_client();
3349        let now = Instant::now();
3350        record_preview_send(&mut client, 5, now);
3351        let deadline = client.preview_next_send_at.get(&5).unwrap();
3352        assert!(*deadline > now);
3353    }
3354
3355    #[test]
3356    fn record_preview_send_successive_calls_advance() {
3357        let mut client = test_client();
3358        let now = Instant::now();
3359        record_preview_send(&mut client, 5, now);
3360        let first = *client.preview_next_send_at.get(&5).unwrap();
3361        record_preview_send(&mut client, 5, first);
3362        let second = *client.preview_next_send_at.get(&5).unwrap();
3363        assert!(second > first, "successive sends should advance deadline");
3364    }
3365
3366    // ── congestion control end-to-end properties ──
3367    //
3368    // These tests encode the two goals of the congestion controller:
3369    //   1. Browser-ready, well-provisioned path → full display FPS, minimal added latency
3370    //   2. Bottleneck                           → lowest sustainable FPS, fast recovery when pipe clears
3371    //
3372    // Some tests assert desired future behaviour and currently FAIL due to
3373    // known issues (min_rtt contamination, lead_floor dominating byte window).
3374    // They are marked with a comment so they are easy to find when fixing.
3375
3376    /// Return a client in ideal low-latency, high-bandwidth conditions:
3377    /// browser ready, abundant bandwidth, and tiny RTT. The normal pacing path
3378    /// should still reach display_fps.
3379    fn browser_ready_high_bandwidth_client() -> ClientState {
3380        let mut c = test_client();
3381        c.display_fps = 120.0;
3382        c.rtt_ms = 1.0;
3383        c.min_rtt_ms = 1.0;
3384        c.goodput_bps = 50_000_000.0;
3385        c.delivery_bps = 50_000_000.0;
3386        c.last_goodput_sample_bps = 50_000_000.0;
3387        c.avg_paced_frame_bytes = 30_000.0;
3388        c.avg_preview_frame_bytes = 1_024.0;
3389        c.avg_frame_bytes = 30_000.0;
3390        c.browser_apply_ms = 0.3;
3391        c
3392    }
3393
3394    /// Return a client that has converged to a clearly congested state:
3395    /// ~10× min_rtt inflation, low goodput.
3396    fn congested_client() -> ClientState {
3397        let mut c = test_client();
3398        c.display_fps = 120.0;
3399        c.rtt_ms = 500.0;
3400        c.min_rtt_ms = 40.0;
3401        c.goodput_bps = 200_000.0;
3402        c.delivery_bps = 150_000.0;
3403        c.last_goodput_sample_bps = 200_000.0;
3404        c.avg_paced_frame_bytes = 50_000.0;
3405        c.avg_preview_frame_bytes = 1_024.0;
3406        c.avg_frame_bytes = 50_000.0;
3407        c.goodput_jitter_bps = 50_000.0;
3408        c.max_goodput_jitter_bps = 200_000.0;
3409        c.browser_apply_ms = 1.0;
3410        c
3411    }
3412
3413    /// Simulate one ACK: insert a frame with the given RTT into inflight and
3414    /// call record_ack.  Forces a goodput-window sample each call so that
3415    /// goodput estimates respond within a few calls.
3416    fn sim_ack(client: &mut ClientState, bytes: usize, rtt_ms: f32) {
3417        let sent_at = Instant::now() - Duration::from_millis(rtt_ms as u64);
3418        client.inflight_bytes += bytes;
3419        client.inflight_frames.push_back(InFlightFrame {
3420            sent_at,
3421            bytes,
3422            paced: true,
3423        });
3424        // Age the goodput window so record_ack always emits a sample.
3425        client.goodput_window_start = Instant::now() - Duration::from_millis(25);
3426        record_ack(client);
3427    }
3428
3429    fn sim_acks(client: &mut ClientState, n: usize, bytes: usize, rtt_ms: f32) {
3430        for _ in 0..n {
3431            sim_ack(client, bytes, rtt_ms);
3432        }
3433    }
3434
3435    // ── property: full FPS on a browser-ready path ──
3436
3437    #[test]
3438    fn browser_ready_high_bandwidth_client_uses_full_display_fps() {
3439        let client = browser_ready_high_bandwidth_client();
3440        assert!(
3441            (pacing_fps(&client) - client.display_fps).abs() < 0.01,
3442            "pacing_fps {} should equal display_fps {} when browser is ready and bandwidth is abundant",
3443            pacing_fps(&client),
3444            client.display_fps,
3445        );
3446    }
3447
3448    #[test]
3449    fn browser_ready_high_bandwidth_client_send_interval_within_one_frame() {
3450        let client = browser_ready_high_bandwidth_client();
3451        let interval_ms = send_interval(&client).as_secs_f32() * 1000.0;
3452        let frame_ms = 1000.0 / client.display_fps;
3453        assert!(
3454            interval_ms <= frame_ms + 0.1,
3455            "send_interval {interval_ms:.2}ms exceeds one frame ({frame_ms:.2}ms) when browser is ready"
3456        );
3457    }
3458
3459    // ── property: degraded FPS when bottlenecked ──
3460
3461    #[test]
3462    fn congested_pipe_reduces_pacing_fps_substantially() {
3463        let client = congested_client();
3464        let fps = pacing_fps(&client);
3465        assert!(
3466            fps < client.display_fps * 0.5,
3467            "pacing_fps {fps:.0} should be well below display_fps {} when congested",
3468            client.display_fps,
3469        );
3470    }
3471
3472    #[test]
3473    fn congested_pipe_is_throughput_limited() {
3474        let client = congested_client();
3475        assert!(
3476            throughput_limited(&client),
3477            "congested client must be recognised as throughput-limited"
3478        );
3479    }
3480
3481    // ── property: byte window should stay near BDP ──
3482    //
3483    // KNOWN FAILING: lead_floor in target_byte_window overrides the BDP
3484    // budget when avg_paced_frame_bytes is large.  Fix: cap lead_floor.
3485
3486    #[test]
3487    fn byte_window_bounded_near_bdp_when_congested() {
3488        let client = congested_client();
3489        // BDP at the unloaded path RTT.
3490        let bdp = client.goodput_bps * (path_rtt_ms(&client) / 1_000.0);
3491        let window = target_byte_window(&client);
3492        assert!(
3493            window < bdp as usize * 8,
3494            "byte window {window}B is {:.1}× BDP ({bdp:.0}B); \
3495             expected ≤ 8× — lead_floor may be dominating",
3496            window as f32 / bdp.max(1.0),
3497        );
3498    }
3499
3500    // ── property: min_rtt must not drift upward under congestion ──
3501    //
3502    // KNOWN FAILING: the `min_rtt_ms * 0.999 + rtt_ms * 0.001` update
3503    // bleeds queued RTT into min_rtt.
3504
3505    #[test]
3506    fn min_rtt_not_contaminated_by_congested_rtts() {
3507        let mut client = test_client();
3508        client.display_fps = 120.0;
3509        client.rtt_ms = 40.0;
3510        client.min_rtt_ms = 40.0;
3511        client.goodput_bps = 2_000_000.0;
3512        client.delivery_bps = 2_000_000.0;
3513        client.avg_paced_frame_bytes = 30_000.0;
3514        client.avg_preview_frame_bytes = 1_024.0;
3515        let original_min = client.min_rtt_ms;
3516
3517        // 200 ACKs arriving with 500ms RTT (severe congestion).
3518        sim_acks(&mut client, 200, 30_000, 500.0);
3519
3520        assert!(
3521            client.min_rtt_ms < original_min * 2.0,
3522            "min_rtt drifted from {original_min}ms to {:.1}ms after 200 congested ACKs",
3523            client.min_rtt_ms,
3524        );
3525    }
3526
3527    // ── property: fast recovery when congestion clears ──
3528
3529    #[test]
3530    fn delivery_bps_rises_quickly_when_congestion_clears() {
3531        let mut client = congested_client();
3532        let before = client.delivery_bps;
3533
3534        // 10 ACKs at low latency / high throughput.
3535        sim_acks(&mut client, 10, 30_000, 40.0);
3536
3537        assert!(
3538            client.delivery_bps > before * 2.0,
3539            "delivery_bps {:.0} should more than double from {before:.0} after 10 fast ACKs",
3540            client.delivery_bps,
3541        );
3542    }
3543
3544    #[test]
3545    fn pacing_fps_recovers_after_congestion_clears() {
3546        let mut client = congested_client();
3547
3548        // Use window-saturated rounds: fill the window with frames, age the
3549        // goodput window once, then ACK all.  The first ACK each round emits
3550        // a sample; the remaining target-1 ACKs carry over into the next
3551        // window, so sample throughput grows as target grows — mimicking a
3552        // real link where the sender keeps the pipe full across one RTT.
3553        for _ in 0..40 {
3554            let target = target_frame_window(&client).max(2);
3555            for _ in 0..target {
3556                let sent_at = Instant::now() - Duration::from_millis(40);
3557                client.inflight_bytes += 30_000;
3558                client.inflight_frames.push_back(InFlightFrame {
3559                    sent_at,
3560                    bytes: 30_000,
3561                    paced: true,
3562                });
3563            }
3564            client.goodput_window_start = Instant::now() - Duration::from_millis(25);
3565            for _ in 0..target {
3566                record_ack(&mut client);
3567            }
3568        }
3569
3570        let fps = pacing_fps(&client);
3571        assert!(
3572            fps > client.display_fps * 0.7,
3573            "pacing_fps {fps:.0} didn't recover toward display_fps {} \
3574             after window-saturated rounds at low RTT",
3575            client.display_fps,
3576        );
3577    }
3578
3579    #[test]
3580    fn rtt_estimate_drops_quickly_when_congestion_clears() {
3581        let mut client = test_client();
3582        client.rtt_ms = 500.0;
3583        client.min_rtt_ms = 40.0;
3584        client.goodput_bps = 2_000_000.0;
3585        client.avg_paced_frame_bytes = 30_000.0;
3586        client.avg_preview_frame_bytes = 1_024.0;
3587
3588        // The asymmetric EWMA uses rise=0.125, fall=0.25, so rtt_ms drops
3589        // at fall_alpha=0.25 per sample toward the new low.
3590        sim_acks(&mut client, 10, 30_000, 40.0);
3591
3592        assert!(
3593            client.rtt_ms < 300.0,
3594            "rtt_ms {:.0}ms did not fall fast enough after congestion cleared",
3595            client.rtt_ms,
3596        );
3597    }
3598
3599    // ── property: probing ──
3600
3601    #[test]
3602    fn probe_collapses_immediately_on_queue_delay() {
3603        let mut client = test_client();
3604        client.display_fps = 120.0;
3605        client.rtt_ms = 40.0;
3606        client.min_rtt_ms = 40.0;
3607        client.goodput_bps = 5_000_000.0;
3608        client.delivery_bps = 5_000_000.0;
3609        client.last_goodput_sample_bps = 5_000_000.0;
3610        client.avg_paced_frame_bytes = 10_000.0;
3611        client.avg_preview_frame_bytes = 1_024.0;
3612        client.probe_frames = 10.0;
3613
3614        // ACKs arriving with high RTT signal queue buildup.
3615        sim_acks(&mut client, 5, 10_000, 600.0);
3616
3617        assert!(
3618            client.probe_frames < 5.0,
3619            "probe_frames {:.1} should have collapsed on queue delay signal",
3620            client.probe_frames,
3621        );
3622    }
3623
3624    #[test]
3625    fn probe_grows_when_window_saturated_with_clean_rtt() {
3626        let mut client = test_client();
3627        client.display_fps = 120.0;
3628        client.rtt_ms = 40.0;
3629        client.min_rtt_ms = 40.0;
3630        client.goodput_bps = 5_000_000.0;
3631        client.delivery_bps = 5_000_000.0;
3632        client.last_goodput_sample_bps = 5_000_000.0;
3633        client.avg_paced_frame_bytes = 10_000.0;
3634        client.avg_preview_frame_bytes = 1_024.0;
3635        client.goodput_jitter_bps = 0.0;
3636        client.max_goodput_jitter_bps = 0.0;
3637        client.probe_frames = 0.0;
3638
3639        // Saturate inflight so window_saturated returns true during acks.
3640        let target = target_frame_window(&client);
3641        for _ in 0..target {
3642            let sent_at = Instant::now() - Duration::from_millis(40);
3643            client.inflight_bytes += 10_000;
3644            client.inflight_frames.push_back(InFlightFrame {
3645                sent_at,
3646                bytes: 10_000,
3647                paced: true,
3648            });
3649        }
3650
3651        // Ack one frame with clean RTT.  One saturated ACK is sufficient to
3652        // verify the property: as probe_frames increments, target_frame_window
3653        // grows, so the remaining (target-1) frames would fall below the 90%
3654        // threshold and trigger gentle decay.  The property under test is that
3655        // *receiving an ACK while window-saturated* increments probe_frames —
3656        // not that it stays incremented across subsequent unsaturated ACKs.
3657        // Also: do NOT age the goodput window — that would emit a per-frame
3658        // sample far below goodput_bps, spiking jitter and collapsing probe.
3659        record_ack(&mut client);
3660
3661        assert!(
3662            client.probe_frames > 0.0,
3663            "probe_frames should grow when window-saturated with clean RTT"
3664        );
3665    }
3666
3667    // ── property: frame window larger on high-latency links ──
3668
3669    #[test]
3670    fn frame_window_larger_on_high_latency_link() {
3671        let mut lo = test_client();
3672        lo.display_fps = 120.0;
3673        lo.rtt_ms = 10.0;
3674        lo.min_rtt_ms = 10.0;
3675        lo.goodput_bps = 5_000_000.0;
3676        lo.delivery_bps = 5_000_000.0;
3677        lo.avg_paced_frame_bytes = 10_000.0;
3678        lo.avg_preview_frame_bytes = 1_024.0;
3679
3680        let mut hi = test_client();
3681        hi.display_fps = 120.0;
3682        hi.rtt_ms = 200.0;
3683        hi.min_rtt_ms = 200.0;
3684        hi.goodput_bps = 5_000_000.0;
3685        hi.delivery_bps = 5_000_000.0;
3686        hi.avg_paced_frame_bytes = 10_000.0;
3687        hi.avg_preview_frame_bytes = 1_024.0;
3688
3689        let lo_win = target_frame_window(&lo);
3690        let hi_win = target_frame_window(&hi);
3691        assert!(
3692            hi_win > lo_win,
3693            "high-latency link ({hi_win}f) should need more frames in flight \
3694             than low-latency ({lo_win}f)"
3695        );
3696    }
3697
3698    // ── property: small-frame byte window allows pipelining ──
3699
3700    #[test]
3701    fn small_frame_byte_window_enables_pipelining() {
3702        // Tiny terminal frames (~1KB) with a stale congested RTT and low
3703        // goodput estimate (stop-and-wait artifact): byte window must be at
3704        // least target_frame_window × frame_bytes so the sender can pipeline
3705        // rather than stay stuck in stop-and-wait.
3706        let mut client = test_client();
3707        client.display_fps = 120.0;
3708        client.rtt_ms = 165.0;
3709        client.min_rtt_ms = 8.0;
3710        client.goodput_bps = 11_000.0; // stop-and-wait artifact
3711        client.delivery_bps = 6_800.0;
3712        client.last_goodput_sample_bps = 11_000.0;
3713        client.avg_paced_frame_bytes = 1_120.0;
3714        client.avg_preview_frame_bytes = 1_024.0;
3715        client.goodput_jitter_bps = 4_300.0;
3716        client.max_goodput_jitter_bps = 6_500.0;
3717
3718        let window = target_byte_window(&client);
3719        let frames = target_frame_window(&client);
3720        let pipeline = frames * 1_120;
3721
3722        assert!(
3723            window >= pipeline,
3724            "byte window {window}B should be >= pipeline ({frames}f × 1120B = {pipeline}B) \
3725             so small frames can pipeline across the RTT"
3726        );
3727    }
3728
3729    #[test]
3730    fn large_frame_byte_window_bounded_by_one_frame_floor() {
3731        // With large frames (50KB), pipelining the full frame window (5×50KB=250KB)
3732        // would be many multiples of BDP.  Byte window should fall back to
3733        // the one-frame floor so the BDP budget governs.
3734        let mut client = test_client();
3735        client.display_fps = 120.0;
3736        client.rtt_ms = 165.0;
3737        client.min_rtt_ms = 8.0;
3738        client.goodput_bps = 11_000.0;
3739        client.delivery_bps = 6_800.0;
3740        client.last_goodput_sample_bps = 11_000.0;
3741        client.avg_paced_frame_bytes = 50_000.0; // large frame
3742        client.avg_preview_frame_bytes = 1_024.0;
3743        client.goodput_jitter_bps = 0.0;
3744        client.max_goodput_jitter_bps = 0.0;
3745
3746        let window = target_byte_window(&client);
3747        let frames = target_frame_window(&client);
3748        let pipeline = frames.saturating_mul(50_000);
3749
3750        assert!(
3751            window < pipeline,
3752            "byte window {window}B should be < full pipeline {pipeline}B \
3753             ({frames}f × 50KB) — large frames must use one-frame floor"
3754        );
3755        assert!(
3756            window >= 50_000,
3757            "byte window {window}B must be at least one frame (50KB)"
3758        );
3759    }
3760
3761    // ── property: preview reservation applies uniformly ──
3762
3763    #[test]
3764    fn preview_reservation_applies_even_on_low_latency_high_bandwidth_links() {
3765        let mut client = browser_ready_high_bandwidth_client();
3766        client.lead = Some(1);
3767        client.subscriptions.insert(1);
3768        let target = target_frame_window(&client);
3769        fill_inflight(&mut client, target.saturating_sub(1), 512);
3770        assert!(
3771            !lead_window_open(&client, true),
3772            "preview reservation should apply uniformly for lead clients"
3773        );
3774    }
3775
3776    // ── property: blip recovery on healthy paths ──
3777
3778    #[test]
3779    fn probe_recovers_on_healthy_path_after_blip() {
3780        let mut client = browser_ready_high_bandwidth_client();
3781        client.probe_frames = 8.0;
3782
3783        // Blip: 3 ACKs with inflated RTT crush probes.
3784        sim_acks(&mut client, 3, 30_000, 200.0);
3785        let post_blip = client.probe_frames;
3786        assert!(
3787            post_blip < 4.0,
3788            "probe_frames {post_blip:.1} should have dropped after blip"
3789        );
3790
3791        // Reset browser metrics to healthy (browser cleared backlog).
3792        client.browser_backlog_frames = 0;
3793        client.browser_ack_ahead_frames = 0;
3794        client.browser_apply_ms = 0.3;
3795
3796        // Recovery: 20 healthy ACKs at low RTT on an underfilled path.
3797        sim_acks(&mut client, 20, 30_000, 1.0);
3798
3799        assert!(
3800            client.probe_frames > post_blip,
3801            "probe_frames {:.1} should have recovered from {post_blip:.1} after healthy ACKs",
3802            client.probe_frames,
3803        );
3804    }
3805
3806    #[test]
3807    fn jitter_decays_fast_on_browser_ready_path() {
3808        let mut client = browser_ready_high_bandwidth_client();
3809
3810        // Inject elevated jitter (simulating post-blip state).
3811        client.max_goodput_jitter_bps = client.goodput_bps * 0.4;
3812        client.goodput_jitter_bps = client.goodput_bps * 0.3;
3813        let initial_jitter = client.max_goodput_jitter_bps;
3814
3815        // 10 healthy ACKs on a browser-ready path.
3816        sim_acks(&mut client, 10, 30_000, 1.0);
3817
3818        assert!(
3819            client.max_goodput_jitter_bps < initial_jitter * 0.5,
3820            "max_goodput_jitter_bps {:.0} should have decayed below {:.0} \
3821             (50% of initial {initial_jitter:.0}) after 10 healthy ACKs on a ready path",
3822            client.max_goodput_jitter_bps,
3823            initial_jitter * 0.5,
3824        );
3825    }
3826
3827    #[test]
3828    fn byte_budget_uses_floor_when_goodput_depressed() {
3829        let mut client = browser_ready_high_bandwidth_client();
3830        client.goodput_bps = 100_000.0;
3831
3832        let budget = byte_budget_for(&client, 100.0);
3833        let floor_budget = (bandwidth_floor_bps(&client) * 100.0 / 1_000.0).ceil() as usize;
3834
3835        assert!(
3836            budget >= floor_budget,
3837            "byte_budget {budget} should be at least bandwidth_floor-based {floor_budget} \
3838             when goodput_bps is depressed but delivery_bps is high"
3839        );
3840    }
3841
3842    #[test]
3843    fn probe_floor_maintained_under_congestion_signal() {
3844        let mut client = test_client();
3845        client.display_fps = 120.0;
3846        client.rtt_ms = 40.0;
3847        client.min_rtt_ms = 40.0;
3848        client.goodput_bps = 5_000_000.0;
3849        client.delivery_bps = 5_000_000.0;
3850        client.last_goodput_sample_bps = 5_000_000.0;
3851        client.avg_paced_frame_bytes = 10_000.0;
3852        client.avg_preview_frame_bytes = 1_024.0;
3853        client.probe_frames = 10.0;
3854
3855        // Many ACKs with high RTT: probes should not drop below the floor.
3856        sim_acks(&mut client, 20, 10_000, 600.0);
3857
3858        assert!(
3859            client.probe_frames >= 1.0,
3860            "probe_frames {:.1} should not drop below the floor of 1.0",
3861            client.probe_frames,
3862        );
3863    }
3864}