Skip to main content

blit_server/
lib.rs

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