Skip to main content

agentmux_srv/backend/blockcontroller/shell/
stamps.rs

1// Copyright 2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Receive-time stamps for a range of transcript lines, from the
5//! `output.tsidx` sidecar — without loading the sidecar whole.
6//!
7//! The sidecar holds one record per append, `{"off":<byte offset>,"ms":<unix
8//! ms>}\n`, in append order, so `off` rises through the file. A line's stamp is
9//! the newest record at or before the line's own byte offset (`0` = unknown).
10//!
11//! `blockfile:read_range` used to read the whole sidecar and parse every
12//! record to stamp ~200 lines: 38 MB and ~1.1M records on a long-lived agent,
13//! about 0.6 s per read. Instead, a binary search over the sidecar's bytes
14//! finds the record the first line needs, and only a window around the
15//! requested range is read and parsed.
16//!
17//! Records a little out of order (two srv instances mirroring into one global
18//! zone, older builds stamping outside the append's transaction) are
19//! tolerated: the window reaches [`MARGIN`] bytes past both ends of the range
20//! and its records are sorted. A record further out of place than that can be
21//! missed, so a line may get an earlier stamp — stamps are best-effort display
22//! times, and a miss that far out would need thousands of records out of
23//! order.
24
25use crate::backend::storage::error::StoreError;
26
27/// Bytes read before the range's first record and past its last, for
28/// out-of-order records (~2,000 records at ~30 bytes each).
29pub(crate) const MARGIN: i64 = 64 * 1024;
30/// Bytes read at each binary-search probe; a record is ~30 bytes.
31const PROBE: i64 = 512;
32/// Once the search interval is this small, the window just covers it.
33const SEARCH_STOP: i64 = 4096;
34/// Chunk size for the window read.
35const CHUNK: i64 = 64 * 1024;
36
37#[derive(serde::Deserialize)]
38struct Record {
39    off: u64,
40    ms: i64,
41}
42
43fn parse(line: &[u8]) -> Option<(u64, i64)> {
44    let line = line.strip_suffix(b"\r").unwrap_or(line);
45    serde_json::from_slice::<Record>(line).ok().map(|r| (r.off, r.ms))
46}
47
48/// Stamps for `line_offsets` (ascending byte offsets in `output`), read from a
49/// sidecar of `size` bytes through `read(offset, len)` — `None` from `read`
50/// means those bytes aren't all stored (a writer mid-append), and so no stamps.
51/// `Ok(None)`: no sidecar records near the range at all.
52pub(crate) fn stamps_for(
53    read: &dyn Fn(i64, i64) -> Result<Option<Vec<u8>>, StoreError>,
54    size: i64,
55    line_offsets: &[u64],
56) -> Result<Option<Vec<i64>>, StoreError> {
57    let (Some(&first), Some(&last)) = (line_offsets.first(), line_offsets.last()) else {
58        return Ok(Some(Vec::new()));
59    };
60    if size <= 0 {
61        return Ok(None);
62    }
63
64    // The latest record boundary whose record is at or before `first`.
65    let (mut lo, mut hi) = (0i64, size);
66    while hi - lo > SEARCH_STOP {
67        let mid = lo + (hi - lo) / 2;
68        match first_record_after(read, mid, hi)? {
69            Probe::Found { start, off } if off <= first => lo = start,
70            Probe::Found { .. } | Probe::NoneBefore => hi = mid,
71            Probe::Unreadable => return Ok(None),
72        }
73    }
74
75    // Everything from MARGIN before that record to MARGIN past the first
76    // record beyond `last`.
77    let from = (lo - MARGIN).max(0);
78    let mut records: Vec<(u64, i64)> = Vec::new();
79    let mut pos = from;
80    let mut carry: Vec<u8> = Vec::new();
81    let mut at_line_start = from == 0;
82    let mut stop_at: Option<i64> = None;
83    while pos < size && stop_at.map_or(true, |s| pos < s) {
84        let len = CHUNK.min(size - pos);
85        let Some(chunk) = read(pos, len)? else { return Ok(None) };
86        carry.extend_from_slice(&chunk);
87        pos += len;
88        let Some(last_nl) = carry.iter().rposition(|&b| b == b'\n') else { continue };
89        let complete: Vec<u8> = carry.drain(..=last_nl).collect();
90        let mut lines = complete.split(|&b| b == b'\n');
91        if !at_line_start {
92            // The window began mid-record: skip to the first boundary.
93            lines.next();
94            at_line_start = true;
95        }
96        for line in lines {
97            if let Some((off, ms)) = parse(line) {
98                records.push((off, ms));
99                if off > last && stop_at.is_none() {
100                    stop_at = Some(pos + MARGIN);
101                }
102            }
103        }
104    }
105    // An unterminated last record (a stamp mid-write) still counts if whole.
106    if pos >= size && at_line_start {
107        if let Some(rec) = parse(&carry) {
108            records.push(rec);
109        }
110    }
111    if records.is_empty() {
112        return Ok(None);
113    }
114    records.sort_by_key(|&(off, _)| off);
115    Ok(Some(
116        line_offsets
117            .iter()
118            .map(|&line_off| match records.partition_point(|&(off, _)| off <= line_off) {
119                0 => 0,
120                p => records[p - 1].1,
121            })
122            .collect(),
123    ))
124}
125
126enum Probe {
127    /// The first parsable record starting after `pos`.
128    Found { start: i64, off: u64 },
129    /// No parsable record starts in `[pos, hi)`.
130    NoneBefore,
131    /// Bytes not stored.
132    Unreadable,
133}
134
135/// The first parsable record that starts after `pos` (at a line boundary
136/// strictly past it) and before `hi`.
137fn first_record_after(
138    read: &dyn Fn(i64, i64) -> Result<Option<Vec<u8>>, StoreError>,
139    pos: i64,
140    hi: i64,
141) -> Result<Probe, StoreError> {
142    let mut at = pos;
143    let mut buf: Vec<u8> = Vec::new();
144    let mut boundary: Option<i64> = None;
145    while at < hi {
146        let len = PROBE.min(hi - at);
147        let Some(chunk) = read(at, len)? else { return Ok(Probe::Unreadable) };
148        buf.extend_from_slice(&chunk);
149        at += len;
150        let base = pos;
151        if boundary.is_none() {
152            let Some(nl) = buf.iter().position(|&b| b == b'\n') else { continue };
153            boundary = Some(base + nl as i64 + 1);
154        }
155        // Parse complete records from the boundary on.
156        let mut start = boundary.unwrap();
157        loop {
158            let rel = (start - base) as usize;
159            if rel >= buf.len() || start >= hi {
160                break;
161            }
162            let Some(len) = buf[rel..].iter().position(|&b| b == b'\n') else { break };
163            if let Some((off, _)) = parse(&buf[rel..rel + len]) {
164                return Ok(Probe::Found { start, off });
165            }
166            start += len as i64 + 1;
167        }
168        boundary = Some(start);
169    }
170    Ok(Probe::NoneBefore)
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176    use proptest::prelude::*;
177
178    /// What `read_range` computed before: every record, sorted.
179    fn reference(sidecar: &[u8], line_offsets: &[u64]) -> Option<Vec<i64>> {
180        let mut entries: Vec<(u64, i64)> = sidecar.split(|&b| b == b'\n').filter_map(parse).collect();
181        if entries.is_empty() {
182            return None;
183        }
184        entries.sort_by_key(|&(off, _)| off);
185        Some(
186            line_offsets
187                .iter()
188                .map(|&l| match entries.partition_point(|&(off, _)| off <= l) {
189                    0 => 0,
190                    p => entries[p - 1].1,
191                })
192                .collect(),
193        )
194    }
195
196    fn windowed(sidecar: &[u8], line_offsets: &[u64]) -> Option<Vec<i64>> {
197        let read = |off: i64, len: i64| -> Result<Option<Vec<u8>>, StoreError> {
198            let (a, b) = (off as usize, ((off + len) as usize).min(sidecar.len()));
199            Ok(Some(sidecar[a..b].to_vec()))
200        };
201        stamps_for(&read, sidecar.len() as i64, line_offsets).unwrap()
202    }
203
204    fn sidecar(records: &[(u64, i64)]) -> Vec<u8> {
205        records.iter().map(|(o, m)| format!("{{\"off\":{o},\"ms\":{m}}}\n")).collect::<String>().into_bytes()
206    }
207
208    #[test]
209    fn a_large_sidecar_gives_the_same_stamps_reading_a_small_window() {
210        let records: Vec<(u64, i64)> = (0..200_000u64).map(|i| (i * 300, 1_700_000_000_000 + i as i64)).collect();
211        let bytes = sidecar(&records);
212        let lines: Vec<u64> = (0..200u64).map(|k| 45_000_000 + k * 150).collect();
213        let read_bytes = std::cell::Cell::new(0i64);
214        let read = |off: i64, len: i64| -> Result<Option<Vec<u8>>, StoreError> {
215            read_bytes.set(read_bytes.get() + len);
216            let (a, b) = (off as usize, ((off + len) as usize).min(bytes.len()));
217            Ok(Some(bytes[a..b].to_vec()))
218        };
219        let got = stamps_for(&read, bytes.len() as i64, &lines).unwrap();
220        assert_eq!(got, reference(&bytes, &lines));
221        assert!(read_bytes.get() < 400 * 1024, "read {} of {} bytes", read_bytes.get(), bytes.len());
222    }
223
224    #[test]
225    fn lines_before_every_record_are_unknown_and_an_empty_sidecar_has_none() {
226        let bytes = sidecar(&[(100, 7), (200, 8)]);
227        assert_eq!(windowed(&bytes, &[0, 99, 100, 250]), Some(vec![0, 0, 7, 8]));
228        assert_eq!(windowed(b"", &[1]), None);
229        assert_eq!(windowed(b"garbage\n", &[1]), None);
230        assert_eq!(windowed(&bytes, &[]), Some(vec![]));
231    }
232
233    #[test]
234    fn bytes_not_stored_mean_no_stamps() {
235        let bytes = sidecar(&(0..5000u64).map(|i| (i * 10, i as i64)).collect::<Vec<_>>());
236        let read = |off: i64, len: i64| -> Result<Option<Vec<u8>>, StoreError> {
237            Ok((off + len < 60_000).then(|| bytes[off as usize..(off + len) as usize].to_vec()))
238        };
239        assert_eq!(stamps_for(&read, bytes.len() as i64, &[49_000]).unwrap(), None);
240    }
241
242    proptest! {
243        /// Sorted sidecars with local disorder, garbage lines, CRLF and an
244        /// unterminated last record: the windowed read matches the full parse.
245        #[test]
246        fn windowed_stamps_equal_the_full_parse(
247            n in 1usize..6000,
248            gaps in prop::collection::vec(0u64..400, 1..64),
249            swaps in prop::collection::vec(0usize..6000, 0..40),
250            garbage in prop::collection::vec(0usize..6000, 0..10),
251            crlf in any::<bool>(),
252            unterminated in any::<bool>(),
253            picks in prop::collection::vec(0u64..2_500_000, 1..60),
254        ) {
255            let mut off = 0u64;
256            let mut records: Vec<(u64, i64)> = (0..n).map(|i| { off += gaps[i % gaps.len()]; (off, i as i64 + 1) }).collect();
257            // Local disorder: neighbours swapped.
258            for s in swaps { if s + 1 < records.len() { records.swap(s, s + 1); } }
259            let mut text = String::new();
260            for (i, (o, m)) in records.iter().enumerate() {
261                if garbage.contains(&i) { text.push_str("{\"off\":oops\n"); }
262                text.push_str(&format!("{{\"off\":{o},\"ms\":{m}}}"));
263                text.push_str(if crlf { "\r\n" } else { "\n" });
264            }
265            if unterminated { let t = text.trim_end().len(); text.truncate(t); }
266            let bytes = text.into_bytes();
267            let mut lines = picks.clone();
268            lines.sort_unstable();
269            // A read_range asks for one contiguous run of lines: bound the span.
270            let lo = lines[0];
271            let lines: Vec<u64> = lines.into_iter().filter(|&l| l - lo < 20_000).collect();
272            prop_assert_eq!(windowed(&bytes, &lines), reference(&bytes, &lines));
273        }
274    }
275}