use std::collections::HashSet;
use std::time::Duration;
use chrono::{DateTime, Utc};
use crate::tailer::{ReplayItem, Source, Timing, Update};
pub struct Timeline {
pub items: Vec<ReplayItem>,
pub replay: bool,
pub cursor: Option<DateTime<Utc>>,
pub folded: usize,
pub follow_head: bool,
pub speed: f64,
head: Option<DateTime<Utc>>,
ended: bool,
gap_anchor: Option<DateTime<Utc>>,
gap_progress: f64,
undated_agents: HashSet<String>,
pub compress_gaps: bool,
}
const GAP_FAITHFUL_KNEE: f64 = 0.8;
const GAP_COMPRESS_SCALE: f64 = 0.6;
const GAP_MARKER_SECS: i64 = 60;
fn compress_gap(faithful: f64) -> f64 {
if faithful <= GAP_FAITHFUL_KNEE {
faithful
} else {
GAP_FAITHFUL_KNEE
+ GAP_COMPRESS_SCALE * (1.0 + (faithful - GAP_FAITHFUL_KNEE) / GAP_FAITHFUL_KNEE).ln()
}
}
impl Default for Timeline {
fn default() -> Self {
Self::new()
}
}
impl Timeline {
pub fn new() -> Self {
Timeline {
items: Vec::new(),
replay: false,
cursor: None,
folded: 0,
follow_head: true,
speed: 1.0,
head: None,
ended: false,
gap_anchor: None,
gap_progress: 0.0,
undated_agents: HashSet::new(),
compress_gaps: true,
}
}
pub fn load_replay(&mut self, items: Vec<ReplayItem>, speed: f64) {
let start = items.iter().find_map(|i| i.ts());
self.head = items.iter().filter_map(|i| i.ts()).max();
self.items = items;
self.replay = true;
self.cursor = start;
self.folded = 0;
self.follow_head = true;
self.speed = if speed > 0.0 { speed } else { 1.0 };
self.ended = false;
self.gap_anchor = start;
self.gap_progress = 0.0;
self.rescan_undated();
}
pub fn append_live(&mut self, updates: Vec<Update>) {
let before = self.items.len();
let was_at_edge = match (self.cursor, self.head) {
(_, None) => true, (None, Some(_)) => false, (Some(c), Some(h)) => c >= h, };
let mut tail_ts = self.items.last().and_then(|i| i.ts());
let mut in_order = true;
let mut needs_dating = false;
for update in updates {
let item = ReplayItem::live(update);
match item.ts() {
Some(ts) => {
self.head = Some(self.head.map_or(ts, |h| h.max(ts)));
if tail_ts.is_some_and(|t| ts < t) {
in_order = false;
}
tail_ts = Some(tail_ts.map_or(ts, |t| t.max(ts)));
if !self.undated_agents.is_empty()
&& let Update::Entry {
source: Source::Sub(id),
..
} = &item.update
&& self.undated_agents.contains(id)
{
needs_dating = true;
}
}
None => needs_dating = true,
}
self.items.push(item);
}
if needs_dating || !in_order {
crate::tailer::date_and_sort_live(&mut self.items);
self.rescan_undated();
}
if self.items.len() != before {
self.ended = false;
}
if self.follow_head && was_at_edge {
self.cursor = self.head;
}
}
fn rescan_undated(&mut self) {
self.undated_agents.clear();
for item in &self.items {
if let Timing::Pending(agent) = &item.timing {
self.undated_agents.insert(agent.clone());
}
}
}
pub fn following(&self) -> bool {
self.follow_head && self.cursor.zip(self.head).is_none_or(|(c, h)| c >= h)
}
pub fn head_ts(&self) -> Option<DateTime<Utc>> {
self.head
}
fn due(&self, item: &ReplayItem) -> bool {
match (item.ts(), self.cursor) {
(Some(t), Some(c)) => t <= c,
(None, _) => true,
(Some(_), None) => false,
}
}
pub fn fold_target(&self) -> usize {
self.items.partition_point(|item| self.due(item))
}
pub fn advance(&mut self, elapsed: Duration, paused: bool) {
if paused || !self.follow_head {
return;
}
let (Some(cur), Some(h)) = (self.cursor, self.head) else {
return;
};
if cur >= h {
self.cursor = Some(h);
return;
}
let idx = self
.items
.partition_point(|i| i.ts().is_none_or(|t| t <= cur));
let Some(boundary) = self.items.get(idx).and_then(|i| i.ts()) else {
self.cursor = self.head;
return;
};
let anchor = self.items[..idx]
.iter()
.rev()
.find_map(|i| i.ts())
.unwrap_or(cur);
if self.gap_anchor != Some(anchor) {
self.gap_anchor = Some(anchor);
self.gap_progress = 0.0;
}
let gap_ms = (boundary - anchor).num_milliseconds().max(0) as f64;
let faithful = gap_ms / 1000.0 / self.speed;
let budget = if self.compress_gaps {
compress_gap(faithful)
} else {
faithful
};
if budget > 0.0 {
self.gap_progress += elapsed.as_secs_f64() / budget;
}
if budget <= 0.0 || self.gap_progress >= 1.0 {
self.cursor = Some(boundary); } else {
self.cursor =
Some(anchor + chrono::Duration::milliseconds((gap_ms * self.gap_progress) as i64));
}
}
pub fn reset_pacing(&mut self) {
self.gap_anchor = None;
self.gap_progress = 0.0;
}
pub fn at_edge(&self) -> bool {
!self.items.is_empty() && self.folded >= self.items.len()
}
pub fn now_reference(&self) -> Option<DateTime<Utc>> {
if !self.replay && self.follow_head && self.at_edge() {
Some(Utc::now())
} else {
self.cursor
}
}
pub fn just_ended(&mut self) -> bool {
if self.replay && self.at_edge() && !self.ended {
self.ended = true;
return true;
}
false
}
pub fn start_ts(&self) -> Option<DateTime<Utc>> {
self.items.iter().find_map(|i| i.ts())
}
pub fn has_span(&self) -> bool {
self.head.is_some()
}
pub fn floor(&self) -> usize {
let Some(start) = self.start_ts() else {
return self.items.len(); };
let mut n = 0;
for item in &self.items {
match item.ts() {
Some(t) if t <= start => n += 1,
None => n += 1, Some(_) => break,
}
}
n
}
pub fn progress(&self) -> f64 {
let floor = self.floor();
let reach = self.items.len().saturating_sub(floor);
if reach == 0 {
return if self.folded > 0 { 1.0 } else { 0.0 };
}
(self.folded.saturating_sub(floor) as f64 / reach as f64).clamp(0.0, 1.0)
}
pub fn fold_at_fraction(&self, f: f64) -> usize {
let floor = self.floor();
let reach = self.items.len().saturating_sub(floor);
floor + (f.clamp(0.0, 1.0) * reach as f64).round() as usize
}
pub fn bar_fraction_for_index(&self, idx: usize) -> f64 {
let floor = self.floor();
let reach = self.items.len().saturating_sub(floor);
if reach == 0 {
return 0.0;
}
((idx + 1).saturating_sub(floor) as f64 / reach as f64).clamp(0.0, 1.0)
}
pub fn gap_markers(&self) -> Vec<usize> {
let mut out = Vec::new();
let mut prev: Option<DateTime<Utc>> = None;
for (i, item) in self.items.iter().enumerate() {
if let Some(ts) = item.ts() {
if let Some(p) = prev
&& (ts - p).num_seconds() >= GAP_MARKER_SECS
{
out.push(i);
}
prev = Some(ts);
}
}
out
}
pub fn prompt_markers(&self) -> Vec<usize> {
self.items
.iter()
.enumerate()
.filter_map(|(i, item)| match &item.update {
Update::Entry {
source: Source::Main,
entry: crate::transcript::Entry::User(e),
} if e.is_human_prompt() => Some(i),
_ => None,
})
.collect()
}
pub fn ts_at_index(&self, idx: usize) -> Option<DateTime<Utc>> {
let end = (idx + 1).min(self.items.len());
self.items[..end]
.iter()
.rev()
.find_map(|i| i.ts())
.or_else(|| self.start_ts())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tailer::{ReplayItem, Source, Update};
use crate::transcript::{self, Entry};
fn ts(s: &str) -> DateTime<Utc> {
s.parse().unwrap()
}
fn entry_item(t: &str) -> ReplayItem {
let line = format!(
"{{\"type\":\"user\",\"uuid\":\"u\",\"parentUuid\":null,\"origin\":{{\"kind\":\"human\"}},\"timestamp\":\"{t}\",\"message\":{{\"role\":\"user\",\"content\":\"x\"}}}}"
);
ReplayItem::at(
Some(ts(t)),
Update::Entry {
source: Source::Main,
entry: transcript::parse_line(&line).unwrap(),
},
)
}
fn meta_item() -> ReplayItem {
ReplayItem::at(
None,
Update::Entry {
source: Source::Main,
entry: Entry::Unknown,
},
)
}
#[test]
fn following_requires_both_follow_head_and_at_edge() {
let mut tl = Timeline::new();
tl.load_replay(
vec![
entry_item("2026-06-05T10:00:00.000Z"),
entry_item("2026-06-05T10:00:10.000Z"),
],
8.0,
);
assert!(!tl.following(), "a cursor behind the edge is not following");
tl.cursor = tl.head_ts();
assert!(tl.following());
tl.follow_head = false;
assert!(
!tl.following(),
"a parked cursor at the edge is not following"
);
}
#[test]
fn now_reference_is_wall_clock_only_at_a_live_edge_else_the_playhead() {
let old = ts("2020-01-01T00:00:00.000Z");
let mut tl = Timeline::new();
tl.items = vec![entry_item("2020-01-01T00:00:00.000Z")];
tl.cursor = Some(old);
tl.folded = 1;
tl.replay = false;
tl.follow_head = true;
let now = tl.now_reference().unwrap();
assert!(
now > old && (Utc::now() - now).num_seconds() < 5,
"a live edge judges liveness against real now"
);
tl.replay = true;
assert_eq!(
tl.now_reference(),
Some(old),
"a replay judges liveness against the playhead"
);
tl.replay = false;
tl.follow_head = false;
assert_eq!(
tl.now_reference(),
Some(old),
"scrubbed back → the playhead"
);
}
#[test]
fn gap_markers_flag_gaps_at_or_over_the_threshold() {
let mut tl = Timeline::new();
tl.items = vec![
entry_item("2026-06-05T10:00:00.000Z"), entry_item("2026-06-05T10:00:59.000Z"), entry_item("2026-06-05T10:01:59.000Z"), entry_item("2026-06-05T10:03:00.000Z"), ];
assert_eq!(
tl.gap_markers(),
vec![2, 3],
"a 60s gap flags; a 59s gap does not"
);
}
#[test]
fn prompt_markers_index_human_prompts_only() {
let system = {
let line = r#"{"type":"user","uuid":"u","parentUuid":null,"origin":{"kind":"task-notification"},"timestamp":"2026-06-05T10:30:00.000Z","message":{"role":"user","content":"3 background agents were stopped"}}"#;
ReplayItem::at(
Some(ts("2026-06-05T10:30:00.000Z")),
Update::Entry {
source: Source::Main,
entry: transcript::parse_line(line).unwrap(),
},
)
};
let mut tl = Timeline::new();
tl.items = vec![
entry_item("2026-06-05T10:00:00.000Z"), system, meta_item(), entry_item("2026-06-05T11:00:00.000Z"), ];
assert_eq!(tl.prompt_markers(), vec![0, 3]);
}
#[test]
fn bar_fraction_for_index_spans_zero_to_one_and_clamps() {
let tl = Timeline::new();
assert_eq!(tl.bar_fraction_for_index(0), 0.0);
let mut tl = Timeline::new();
tl.load_replay(
vec![
entry_item("2026-06-05T10:00:00.000Z"),
entry_item("2026-06-05T10:00:10.000Z"),
entry_item("2026-06-05T10:00:20.000Z"),
],
8.0,
);
let last = tl.items.len() - 1;
assert_eq!(
tl.bar_fraction_for_index(last),
1.0,
"last item → far right"
);
assert_eq!(tl.bar_fraction_for_index(999), 1.0, "beyond the end clamps");
let f0 = tl.bar_fraction_for_index(0);
assert!((0.0..=1.0).contains(&f0));
assert!(tl.bar_fraction_for_index(1) >= f0);
}
#[test]
fn replay_starts_at_first_item_and_folds_it() {
let mut tl = Timeline::new();
tl.load_replay(
vec![
entry_item("2026-06-05T10:00:00.000Z"),
entry_item("2026-06-05T10:00:10.000Z"),
],
8.0,
);
assert!(tl.replay);
assert_eq!(tl.cursor, Some(ts("2026-06-05T10:00:00.000Z")));
assert_eq!(tl.fold_target(), 1);
}
#[test]
fn advance_paces_cursor_by_speed_and_clamps_to_head() {
let mut tl = Timeline::new();
tl.load_replay(
vec![
entry_item("2026-06-05T10:00:00.000Z"),
entry_item("2026-06-05T10:00:02.000Z"),
],
8.0,
);
tl.advance(Duration::from_millis(125), false);
assert_eq!(tl.cursor, Some(ts("2026-06-05T10:00:01.000Z")));
assert_eq!(tl.fold_target(), 1);
tl.advance(Duration::from_millis(125), false);
assert_eq!(tl.cursor, Some(ts("2026-06-05T10:00:02.000Z")));
assert_eq!(tl.fold_target(), 2);
}
#[test]
fn advance_is_a_noop_when_paused_or_parked() {
let mut tl = Timeline::new();
tl.load_replay(
vec![
entry_item("2026-06-05T10:00:00.000Z"),
entry_item("2026-06-05T10:00:10.000Z"),
],
8.0,
);
tl.advance(Duration::from_secs(1), true); assert_eq!(tl.cursor, Some(ts("2026-06-05T10:00:00.000Z")));
tl.follow_head = false; tl.advance(Duration::from_secs(1), false);
assert_eq!(tl.cursor, Some(ts("2026-06-05T10:00:00.000Z")));
}
#[test]
fn advance_compresses_long_idle_gaps_but_eases() {
let mut tl = Timeline::new();
tl.load_replay(
vec![
entry_item("2026-06-05T10:00:00.000Z"),
entry_item("2026-06-05T11:00:00.000Z"), ],
1.0,
);
tl.advance(Duration::from_millis(100), false);
let mid = tl.cursor.unwrap();
assert!(mid > ts("2026-06-05T10:00:00.000Z") && mid < ts("2026-06-05T11:00:00.000Z"));
tl.advance(Duration::from_secs(10), false);
assert_eq!(tl.cursor, Some(ts("2026-06-05T11:00:00.000Z")));
}
#[test]
fn faithful_mode_paces_gaps_in_real_time() {
let mut tl = Timeline::new();
tl.compress_gaps = false; tl.load_replay(
vec![
entry_item("2026-06-05T10:00:00.000Z"),
entry_item("2026-06-05T11:00:00.000Z"), ],
1.0,
);
tl.advance(Duration::from_secs(10), false);
let c = tl.cursor.unwrap();
assert!(
c >= ts("2026-06-05T10:00:09.000Z") && c <= ts("2026-06-05T10:00:11.000Z"),
"faithful pacing crosses ~10s of the hour, got {c}"
);
}
#[test]
fn toggling_inactivity_skip_mid_gap_never_rewinds_the_cursor() {
let mut tl = Timeline::new(); tl.load_replay(
vec![
entry_item("2026-06-05T10:00:00.000Z"),
entry_item("2026-06-05T11:00:00.000Z"), ],
1.0,
);
tl.advance(Duration::from_secs(1), false);
let before = tl.cursor.unwrap();
assert!(
before > ts("2026-06-05T10:00:00.000Z"),
"compressed pacing moved off the anchor"
);
tl.compress_gaps = false;
tl.advance(Duration::from_secs(1), false);
let after = tl.cursor.unwrap();
assert!(
after >= before,
"cursor rewound on toggle: {before} -> {after}"
);
}
#[test]
fn compress_gap_is_faithful_below_knee_then_bounded_and_graded() {
assert_eq!(compress_gap(0.5), 0.5);
assert!((compress_gap(GAP_FAITHFUL_KNEE) - GAP_FAITHFUL_KNEE).abs() < 1e-9);
assert!(compress_gap(3600.0) < 10.0);
assert!(compress_gap(3600.0) > compress_gap(60.0));
assert!(compress_gap(60.0) > compress_gap(10.0));
}
#[test]
fn live_following_pins_cursor_to_growing_head() {
let mut tl = Timeline::new();
assert!(!tl.replay);
tl.append_live(vec![meta_item().update]);
assert_eq!(tl.fold_target(), tl.items.len());
tl.append_live(vec![entry_item("2026-06-05T10:00:05.000Z").update]);
assert_eq!(tl.head_ts(), Some(ts("2026-06-05T10:00:05.000Z")));
assert_eq!(tl.cursor, Some(ts("2026-06-05T10:00:05.000Z")));
assert_eq!(
tl.fold_target(),
tl.items.len(),
"following folds everything"
);
}
#[test]
fn append_live_keeps_items_timestamp_sorted() {
let mut tl = Timeline::new();
tl.append_live(vec![
entry_item("2026-06-05T10:00:05.000Z").update,
entry_item("2026-06-05T10:00:01.000Z").update,
entry_item("2026-06-05T10:00:03.000Z").update,
]);
let got: Vec<_> = tl.items.iter().filter_map(|i| i.ts()).collect();
let mut want = got.clone();
want.sort();
assert_eq!(
got, want,
"items are kept timestamp-ordered for correct seeks"
);
assert_eq!(tl.head_ts(), Some(ts("2026-06-05T10:00:05.000Z")));
tl.append_live(vec![
entry_item("2026-06-05T10:00:08.000Z").update,
entry_item("2026-06-05T10:00:04.000Z").update,
]);
let got: Vec<_> = tl.items.iter().filter_map(|i| i.ts()).collect();
let mut want = got.clone();
want.sort();
assert_eq!(got, want);
}
#[test]
fn early_journal_result_is_redated_once_its_agent_arrives() {
let mut tl = Timeline::new();
tl.append_live(vec![entry_item("2026-06-05T10:00:00.000Z").update]);
let result_line = r#"{"type":"result","key":"v2:abcd","agentId":"aaaaaaaaaaaaaaaaa","result":{"summary":"done"}}"#;
let entry = transcript::parse_line(result_line).unwrap();
tl.append_live(vec![Update::Entry {
source: Source::Journal("wf1".into()),
entry,
}]);
let journal_ts = |tl: &Timeline| {
tl.items
.iter()
.find(|i| {
matches!(
&i.update,
Update::Entry {
source: Source::Journal(_),
..
}
)
})
.unwrap()
.ts()
};
assert_eq!(journal_ts(&tl), None, "stays undated while agent unknown");
let sub_line = r#"{"type":"user","uuid":"s1","parentUuid":null,"isSidechain":true,"agentId":"aaaaaaaaaaaaaaaaa","timestamp":"2026-06-05T11:30:00.000Z","message":{"role":"user","content":"task"}}"#;
tl.append_live(vec![Update::Entry {
source: Source::Sub("aaaaaaaaaaaaaaaaa".into()),
entry: transcript::parse_line(sub_line).unwrap(),
}]);
assert_eq!(
journal_ts(&tl),
Some(ts("2026-06-05T11:30:00.000Z")),
"re-dated to the agent's entry, not the session start"
);
}
#[test]
fn append_live_fast_path_keeps_order_without_resort() {
let mut tl = Timeline::new();
tl.append_live(vec![
entry_item("2026-06-05T10:00:01.000Z").update,
entry_item("2026-06-05T10:00:02.000Z").update,
]);
tl.append_live(vec![entry_item("2026-06-05T10:00:03.000Z").update]);
let got: Vec<_> = tl.items.iter().filter_map(|i| i.ts()).collect();
let mut want = got.clone();
want.sort();
assert_eq!(got, want);
assert_eq!(tl.fold_target(), 3);
}
#[test]
fn append_live_keeps_a_behind_the_edge_cursor_in_place() {
let mut tl = Timeline::new();
tl.load_replay(
vec![
entry_item("2026-06-05T10:00:00.000Z"),
entry_item("2026-06-05T10:00:10.000Z"),
],
8.0,
);
tl.cursor = Some(ts("2026-06-05T10:00:00.000Z"));
tl.follow_head = false;
tl.append_live(vec![entry_item("2026-06-05T10:00:20.000Z").update]);
assert_eq!(tl.head_ts(), Some(ts("2026-06-05T10:00:20.000Z")));
assert_eq!(
tl.cursor,
Some(ts("2026-06-05T10:00:00.000Z")),
"parked cursor stays put"
);
tl.follow_head = true;
tl.append_live(vec![entry_item("2026-06-05T10:00:30.000Z").update]);
assert_eq!(tl.head_ts(), Some(ts("2026-06-05T10:00:30.000Z")));
assert_eq!(
tl.cursor,
Some(ts("2026-06-05T10:00:00.000Z")),
"a catching-up cursor isn't snapped to the live edge"
);
}
#[test]
fn progress_is_event_based_and_inverts_fold_at_fraction() {
let mut tl = Timeline::new();
tl.load_replay(
vec![
entry_item("2026-06-05T10:00:00.000Z"),
entry_item("2026-06-05T10:00:01.000Z"),
entry_item("2026-06-05T10:00:02.000Z"),
entry_item("2026-06-05T10:00:03.000Z"),
],
8.0,
);
assert_eq!(tl.floor(), 1);
tl.folded = 1;
assert!((tl.progress() - 0.0).abs() < 1e-9);
tl.folded = 4;
assert!((tl.progress() - 1.0).abs() < 1e-9);
assert_eq!(tl.fold_at_fraction(0.0), 1);
assert_eq!(tl.fold_at_fraction(1.0), 4);
assert_eq!(tl.fold_at_fraction(0.5), 3); }
#[test]
fn floor_absorbs_a_start_clump_so_the_left_reaches_zero() {
let mut tl = Timeline::new();
tl.load_replay(
vec![
meta_item(), entry_item("2026-06-05T10:00:00.000Z"),
entry_item("2026-06-05T10:00:00.000Z"), entry_item("2026-06-05T10:00:05.000Z"),
],
8.0,
);
assert_eq!(tl.floor(), 3);
let target = tl.fold_at_fraction(0.0);
assert_eq!(target, 3);
tl.folded = target;
assert!((tl.progress() - 0.0).abs() < 1e-9, "left edge must reach 0");
}
#[test]
fn ts_at_index_falls_back_to_start_for_leading_untimed_items() {
let mut tl = Timeline::new();
tl.load_replay(
vec![
meta_item(),
entry_item("2026-06-05T10:00:00.000Z"),
entry_item("2026-06-05T10:00:10.000Z"),
],
8.0,
);
assert_eq!(tl.ts_at_index(0), Some(ts("2026-06-05T10:00:00.000Z")));
}
#[test]
fn playback_reaches_the_end_across_a_huge_trailing_gap() {
let mut items = Vec::new();
for i in 0..20 {
items.push(entry_item(&format!("2026-06-05T10:00:{i:02}.000Z")));
}
items.push(entry_item("2026-06-05T20:00:00.000Z")); let head = ts("2026-06-05T20:00:00.000Z");
let mut tl = Timeline::new();
tl.load_replay(items, 8.0);
let mut frames = 0;
while !tl.at_edge() && frames < 5000 {
tl.advance(Duration::from_millis(16), false);
tl.folded = tl.fold_target();
frames += 1;
}
assert!(tl.at_edge(), "playback stalled before the end");
assert_eq!(tl.cursor, Some(head), "cursor must land on the edge");
}
#[test]
fn just_ended_latches_once_at_replay_end() {
let mut tl = Timeline::new();
tl.load_replay(vec![entry_item("2026-06-05T10:00:00.000Z")], 8.0);
tl.folded = tl.items.len();
assert!(tl.just_ended(), "fires once when fully folded");
assert!(!tl.just_ended(), "does not refire");
}
struct Rng(u64);
impl Rng {
fn new(seed: u64) -> Self {
Rng(seed.wrapping_add(0x9E37_79B9_7F4A_7C15))
}
fn next_u64(&mut self) -> u64 {
self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = self.0;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
fn below(&mut self, n: usize) -> usize {
(self.next_u64() % n as u64) as usize
}
}
fn main_entry(sec: u64, uid: usize) -> Update {
let line = format!(
"{{\"type\":\"user\",\"uuid\":\"m{uid}\",\"parentUuid\":null,\"timestamp\":\"2026-06-05T10:00:{sec:02}.000Z\",\"message\":{{\"role\":\"user\",\"content\":\"x\"}}}}"
);
Update::Entry {
source: Source::Main,
entry: transcript::parse_line(&line).unwrap(),
}
}
fn sub_entry(agent: &str, sec: u64, uid: usize) -> Update {
let line = format!(
"{{\"type\":\"user\",\"uuid\":\"s{uid}\",\"parentUuid\":null,\"isSidechain\":true,\"agentId\":\"{agent}\",\"timestamp\":\"2026-06-05T10:00:{sec:02}.000Z\",\"message\":{{\"role\":\"user\",\"content\":\"x\"}}}}"
);
Update::Entry {
source: Source::Sub(agent.to_string()),
entry: transcript::parse_line(&line).unwrap(),
}
}
fn meta_update(agent: &str) -> Update {
Update::SubagentMeta {
agent_id: agent.to_string(),
workflow: None,
meta: transcript::SubagentMeta {
agent_type: Some("explorer".into()),
description: None,
tool_use_id: None,
stopped_by_user: None,
},
}
}
fn journal_result(agent: &str) -> Update {
let line = format!(
"{{\"type\":\"result\",\"key\":\"v2:x\",\"agentId\":\"{agent}\",\"result\":{{\"summary\":\"done\"}}}}"
);
Update::Entry {
source: Source::Journal("wf1".into()),
entry: transcript::parse_line(&line).unwrap(),
}
}
fn scenario(seed: u64) -> Vec<Vec<Update>> {
let mut rng = Rng::new(seed);
let mut uid = 0;
let mut streams = Vec::new();
let mut main: Vec<(u64, Update)> = (0..(2 + rng.below(4)))
.map(|_| {
let sec = rng.below(30) as u64;
let u = main_entry(sec, uid);
uid += 1;
(sec, u)
})
.collect();
main.sort_by_key(|(sec, _)| *sec);
streams.push(main.into_iter().map(|(_, u)| u).collect());
let agents = [
"a1000000000000001",
"a2000000000000002",
"a3000000000000003",
];
for &agent in agents.iter().take(1 + rng.below(agents.len())) {
let mut s = vec![meta_update(agent)];
let mut entries: Vec<(u64, Update)> = (0..(1 + rng.below(3)))
.map(|_| {
let sec = rng.below(30) as u64;
let u = sub_entry(agent, sec, uid);
uid += 1;
(sec, u)
})
.collect();
entries.sort_by_key(|(sec, _)| *sec);
s.extend(entries.into_iter().map(|(_, u)| u));
if rng.below(2) == 0 {
s.push(journal_result(agent)); }
streams.push(s);
}
streams
}
#[test]
fn live_delivery_converges_to_bulk_ordering() {
use std::collections::VecDeque;
for seed in 0..400u64 {
let mut bulk: Vec<ReplayItem> = scenario(seed)
.into_iter()
.flatten()
.map(ReplayItem::live)
.collect();
crate::tailer::date_and_sort(&mut bulk);
let bulk_ts: Vec<_> = bulk.iter().map(|i| i.ts()).collect();
let mut rng = Rng::new(seed ^ 0x00AB_CDEF);
let mut decks: Vec<VecDeque<Update>> =
scenario(seed).into_iter().map(VecDeque::from).collect();
let mut merged = Vec::new();
loop {
let live: Vec<usize> = decks
.iter()
.enumerate()
.filter(|(_, d)| !d.is_empty())
.map(|(i, _)| i)
.collect();
let Some(&pick) = live.get(rng.below(live.len().max(1))) else {
break;
};
merged.push(decks[pick].pop_front().unwrap());
}
let mut tl = Timeline::new();
let mut rest = merged;
while !rest.is_empty() {
let take = (1 + rng.below(3)).min(rest.len());
let tail = rest.split_off(take);
tl.append_live(rest);
rest = tail;
}
let live_ts: Vec<_> = tl.items.iter().map(|i| i.ts()).collect();
assert!(
tl.items.iter().all(|i| i.ts().is_some()),
"seed {seed}: an item stayed undated (unexpected orphan)"
);
assert_eq!(
bulk_ts, live_ts,
"seed {seed}: live delivery diverged from bulk ordering"
);
}
}
}