Skip to main content

leviath_core/
run_archive.rs

1//! A portable, self-contained record of an entire agent run.
2//!
3//! A run archive is a single append-only file that captures everything about a
4//! run - who owns it (which machine, which world/daemon instance), its metadata,
5//! every inference and tool batch, inbound messages, and the evolving context
6//! window - with enough fidelity that copying the file to another machine lets a
7//! daemon **continue the run where it left off** (LLM non-determinism aside) or
8//! replay it for debugging.
9//!
10//! ## Layout
11//!
12//! ```text
13//! MAGIC ("LVR1") | version (u16 BE) | frame*
14//! frame := len (u64 BE) | JSON-encoded RunRecord
15//! ```
16//!
17//! The framing is binary and codec-agnostic (a future release can swap the JSON
18//! payload for a compact binary codec without changing readers that only seek by
19//! frame length). The first record is always a [`RunRecord::Header`].
20//!
21//! ## Portability / future migration
22//!
23//! [`RunIdentity`] records which machine + world/daemon instance owns a run, and
24//! [`RunRecord::OwnershipChanged`] records a handoff. This is deliberately more
25//! than today needs: the format is meant to eventually let a run start on one
26//! machine, pause, and resume on another - including a machine declining a run
27//! whose tools it lacks and waiting for a capable host. That scheduling logic
28//! isn't built yet; the format simply reserves room for it (ownership handoffs
29//! are first-class, the version field gates changes, and new record variants can
30//! be added without disturbing the frame layout).
31//!
32//! ## Efficiency
33//!
34//! Context windows are the bulk of a run. Rather than snapshot the whole window
35//! on every step, a writer emits an occasional full [`RunRecord::ContextCheckpoint`]
36//! and, between checkpoints, small [`RunRecord::ContextDiff`] records describing
37//! only what changed (the common case between inferences is a pure append to one
38//! region). [`diff_context`]/[`apply_delta`] compute and replay those diffs, and
39//! [`fold`] reconstructs the current state from the whole journal.
40
41use std::io::{self, Read, Write};
42
43use serde::{Deserialize, Serialize};
44
45use crate::run_meta::{ContextSnapshot, RegionEntrySnapshot, RegionSnapshot, RunMeta, RunStatus};
46
47/// File magic identifying a leviath run archive (`b"LVR1"`).
48pub const RUN_ARCHIVE_MAGIC: &[u8; 4] = b"LVR1";
49
50/// The archive format version this build writes.
51pub const RUN_ARCHIVE_VERSION: u16 = 1;
52
53/// Identity + ownership of a run.
54///
55/// `machine_id` + `world_id` make a run unambiguously attributable even when
56/// several daemons share a filesystem and might otherwise pick the same
57/// `run_id` - a daemon can read a run's owner before deciding whether to resume
58/// or leave it alone.
59#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
60pub struct RunIdentity {
61    /// The run's id (its directory/file name).
62    pub run_id: String,
63    /// Stable fingerprint of the machine that owns the run.
64    pub machine_id: String,
65    /// Id of the specific world/daemon instance that owns the run.
66    pub world_id: String,
67    /// Unix seconds when the archive was created.
68    pub created_at: i64,
69}
70
71/// A conversation message as recorded in the archive.
72#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
73pub struct MessageRecord {
74    /// `"user"` / `"assistant"` / `"tool"` / `"system"`.
75    pub role: String,
76    /// The message text.
77    pub content: String,
78}
79
80/// A single tool call and (once executed) its result.
81#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
82pub struct ToolCallRecord {
83    /// The tool-call id.
84    pub id: String,
85    /// The tool name.
86    pub name: String,
87    /// The JSON arguments, stringified.
88    pub arguments: String,
89    /// The result text, once the tool has run (`None` while pending).
90    pub result: Option<String>,
91}
92
93/// The outbound request of one inference (a provider-agnostic digest - enough to
94/// reproduce/debug the call without depending on `leviath-providers`).
95#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
96pub struct InferenceRequestRecord {
97    /// The model the request targeted.
98    pub model: String,
99    /// System-block texts, in order.
100    pub system: Vec<String>,
101    /// The conversation messages sent.
102    pub messages: Vec<MessageRecord>,
103    /// The tool names offered to the model.
104    pub tool_names: Vec<String>,
105    /// The temperature used.
106    pub temperature: f32,
107    /// The max output tokens requested.
108    pub max_tokens: usize,
109}
110
111/// The response of one inference.
112#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
113pub struct InferenceResponseRecord {
114    /// The assistant's text.
115    pub content: String,
116    /// Any tool calls the model requested.
117    pub tool_calls: Vec<ToolCallRecord>,
118    /// Prompt tokens billed.
119    pub prompt_tokens: usize,
120    /// Completion tokens billed.
121    pub completion_tokens: usize,
122    /// Tokens read from provider cache.
123    pub cached_tokens: usize,
124    /// Tokens written to provider cache.
125    pub cache_write_tokens: usize,
126}
127
128/// A per-region change within a [`ContextDelta`].
129#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
130pub enum RegionDelta {
131    /// A new region, or a region whose kind/max changed or whose entries were
132    /// rewritten in a non-append way - carried in full.
133    Set(RegionSnapshot),
134    /// Entries appended to an existing region (the common between-inference
135    /// case). The region's kind/max are unchanged.
136    Append {
137        /// The region name.
138        name: String,
139        /// The entries appended after the previously-recorded ones.
140        entries: Vec<RegionEntrySnapshot>,
141        /// The region's new token count.
142        current_tokens: usize,
143    },
144    /// An existing region emptied of entries.
145    Clear {
146        /// The region name.
147        name: String,
148    },
149    /// A region that no longer exists.
150    Remove {
151        /// The region name.
152        name: String,
153    },
154}
155
156/// The change to a context window since the previously-recorded snapshot.
157#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
158pub struct ContextDelta {
159    /// The window's stage name at this point.
160    pub stage_name: String,
161    /// The window's total token count at this point.
162    pub total_tokens: usize,
163    /// The window's max token budget at this point.
164    pub max_tokens: usize,
165    /// Per-region changes.
166    pub regions: Vec<RegionDelta>,
167}
168
169/// One entry in the run journal. Folding the sequence reconstructs the run.
170#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
171pub enum RunRecord {
172    /// The run's identity + static metadata. Always the first record.
173    Header {
174        /// Ownership/identity.
175        identity: RunIdentity,
176        /// The run metadata at archive-creation time.
177        meta: Box<RunMeta>,
178    },
179    /// Ownership handed to a different world/machine (e.g. resumed elsewhere).
180    OwnershipChanged {
181        /// The new owning machine.
182        machine_id: String,
183        /// The new owning world/daemon instance.
184        world_id: String,
185        /// Unix seconds.
186        at: i64,
187    },
188    /// One inference: what went out and what came back.
189    Inference {
190        /// The stage the agent was in.
191        stage: String,
192        /// The stage-local iteration index.
193        iteration: usize,
194        /// The request digest.
195        request: InferenceRequestRecord,
196        /// The response.
197        response: InferenceResponseRecord,
198        /// Unix seconds.
199        at: i64,
200    },
201    /// A batch of tool calls and their results.
202    ToolBatch {
203        /// The calls (with results filled in).
204        calls: Vec<ToolCallRecord>,
205        /// Unix seconds.
206        at: i64,
207    },
208    /// A full context-window snapshot that subsequent diffs rebase on.
209    ContextCheckpoint {
210        /// The full window snapshot.
211        snapshot: ContextSnapshot,
212        /// Unix seconds.
213        at: i64,
214    },
215    /// A context-window change since the previous snapshot/diff.
216    ContextDiff {
217        /// The delta.
218        delta: ContextDelta,
219        /// Unix seconds.
220        at: i64,
221    },
222    /// An inbound message.
223    Message {
224        /// The message.
225        message: MessageRecord,
226        /// Unix seconds.
227        at: i64,
228    },
229    /// A run-status change.
230    StatusChanged {
231        /// The new status.
232        status: RunStatus,
233        /// Unix seconds.
234        at: i64,
235    },
236    /// A full resumable checkpoint: the updated metadata + the full window, so a
237    /// reader can continue without folding the whole journal.
238    Checkpoint {
239        /// The run metadata as of this checkpoint.
240        meta: Box<RunMeta>,
241        /// The full window snapshot as of this checkpoint.
242        context: ContextSnapshot,
243        /// Unix seconds.
244        at: i64,
245    },
246    /// A step forward: the updated metadata plus a *diff* of the context window
247    /// since the previous point. This is the compact per-tick record the writer
248    /// emits between full checkpoints - meta is small, and the context (the bulk)
249    /// is carried as a [`ContextDelta`] rather than a full snapshot.
250    Progress {
251        /// The run metadata as of this step.
252        meta: Box<RunMeta>,
253        /// The context change since the previous recorded point.
254        delta: ContextDelta,
255        /// Unix seconds.
256        at: i64,
257    },
258}
259
260// ─── context diffing ────────────────────────────────────────────────────────
261
262/// Whether `prev` is a prefix of `next` (same entries, in order, at the front).
263fn is_prefix(prev: &[RegionEntrySnapshot], next: &[RegionEntrySnapshot]) -> bool {
264    prev.len() <= next.len() && next[..prev.len()] == *prev
265}
266
267/// Compute the minimal-ish [`ContextDelta`] turning `prev` into `next`. Regions
268/// that only grew at the tail become a compact `Append`; everything else is
269/// carried as a `Set`/`Clear`/`Remove`.
270pub fn diff_context(prev: &ContextSnapshot, next: &ContextSnapshot) -> ContextDelta {
271    let mut regions = Vec::new();
272    for nr in &next.regions {
273        match prev.regions.iter().find(|r| r.name == nr.name) {
274            None => regions.push(RegionDelta::Set(nr.clone())),
275            Some(pr) => {
276                if pr == nr {
277                    // unchanged - emit nothing
278                } else if nr.entries.is_empty() && !pr.entries.is_empty() {
279                    regions.push(RegionDelta::Clear {
280                        name: nr.name.clone(),
281                    });
282                } else if pr.kind == nr.kind
283                    && pr.max_tokens == nr.max_tokens
284                    && is_prefix(&pr.entries, &nr.entries)
285                {
286                    regions.push(RegionDelta::Append {
287                        name: nr.name.clone(),
288                        entries: nr.entries[pr.entries.len()..].to_vec(),
289                        current_tokens: nr.current_tokens,
290                    });
291                } else {
292                    regions.push(RegionDelta::Set(nr.clone()));
293                }
294            }
295        }
296    }
297    for pr in &prev.regions {
298        if !next.regions.iter().any(|r| r.name == pr.name) {
299            regions.push(RegionDelta::Remove {
300                name: pr.name.clone(),
301            });
302        }
303    }
304    ContextDelta {
305        stage_name: next.stage_name.clone(),
306        total_tokens: next.total_tokens,
307        max_tokens: next.max_tokens,
308        regions,
309    }
310}
311
312/// Apply a [`ContextDelta`] to `base` in place. Lenient: a delta referencing a
313/// region that isn't present is skipped rather than erroring, so folding never
314/// fails on a malformed diff.
315pub fn apply_delta(base: &mut ContextSnapshot, delta: &ContextDelta) {
316    base.stage_name = delta.stage_name.clone();
317    base.total_tokens = delta.total_tokens;
318    base.max_tokens = delta.max_tokens;
319    for region_delta in &delta.regions {
320        match region_delta {
321            RegionDelta::Set(snapshot) => {
322                match base.regions.iter_mut().find(|r| r.name == snapshot.name) {
323                    Some(existing) => *existing = snapshot.clone(),
324                    None => base.regions.push(snapshot.clone()),
325                }
326            }
327            RegionDelta::Append {
328                name,
329                entries,
330                current_tokens,
331            } => {
332                if let Some(region) = base.regions.iter_mut().find(|r| &r.name == name) {
333                    region.entries.extend(entries.iter().cloned());
334                    region.current_tokens = *current_tokens;
335                }
336            }
337            RegionDelta::Clear { name } => {
338                if let Some(region) = base.regions.iter_mut().find(|r| &r.name == name) {
339                    region.entries.clear();
340                    region.current_tokens = 0;
341                }
342            }
343            RegionDelta::Remove { name } => {
344                base.regions.retain(|r| &r.name != name);
345            }
346        }
347    }
348}
349
350// ─── codec ──────────────────────────────────────────────────────────────────
351
352/// Write the archive preamble (magic + version). Call once at file start.
353pub fn write_archive_start(w: &mut dyn Write, version: u16) -> io::Result<()> {
354    w.write_all(RUN_ARCHIVE_MAGIC)?;
355    w.write_all(&version.to_be_bytes())?;
356    Ok(())
357}
358
359/// Read + validate the archive preamble, returning the format version.
360pub fn read_archive_start(r: &mut dyn Read) -> io::Result<u16> {
361    let mut magic = [0u8; 4];
362    r.read_exact(&mut magic)?;
363    if &magic != RUN_ARCHIVE_MAGIC {
364        return Err(io::Error::new(
365            io::ErrorKind::InvalidData,
366            "not a leviath run archive (bad magic)",
367        ));
368    }
369    let mut version = [0u8; 2];
370    r.read_exact(&mut version)?;
371    Ok(u16::from_be_bytes(version))
372}
373
374/// Append one framed record. The frame length is a `u64` so it can never
375/// overflow the prefix (a `RunRecord` always serializes to JSON).
376pub fn write_record(w: &mut dyn Write, record: &RunRecord) -> io::Result<()> {
377    let payload = serde_json::to_vec(record).expect("a RunRecord always serializes to JSON");
378    let len = payload.len() as u64;
379    w.write_all(&len.to_be_bytes())?;
380    w.write_all(&payload)?;
381    Ok(())
382}
383
384/// Fill `buf` from `r`, returning `false` on a clean end-of-stream (zero bytes
385/// available at the call) and erroring only on a *partial* read (truncation).
386fn read_exact_or_eof(r: &mut dyn Read, buf: &mut [u8]) -> io::Result<bool> {
387    let mut filled = 0;
388    while filled < buf.len() {
389        match r.read(&mut buf[filled..])? {
390            0 => {
391                if filled == 0 {
392                    return Ok(false); // clean EOF at a record boundary
393                }
394                return Err(io::Error::new(
395                    io::ErrorKind::UnexpectedEof,
396                    "truncated run-archive frame",
397                ));
398            }
399            n => filled += n,
400        }
401    }
402    Ok(true)
403}
404
405/// Read the next framed record, or `None` at a clean end-of-stream.
406pub fn read_record(r: &mut dyn Read) -> io::Result<Option<RunRecord>> {
407    let mut len_bytes = [0u8; 8];
408    if !read_exact_or_eof(r, &mut len_bytes)? {
409        return Ok(None);
410    }
411    let len = u64::from_be_bytes(len_bytes) as usize;
412    let mut payload = vec![0u8; len];
413    if !read_exact_or_eof(r, &mut payload)? {
414        return Err(io::Error::new(
415            io::ErrorKind::UnexpectedEof,
416            "truncated run-archive frame",
417        ));
418    }
419    let record = serde_json::from_slice(&payload)
420        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
421    Ok(Some(record))
422}
423
424/// Read the whole archive: validate the preamble, then read every record.
425pub fn read_archive(r: &mut dyn Read) -> io::Result<(u16, Vec<RunRecord>)> {
426    let version = read_archive_start(r)?;
427    let mut records = Vec::new();
428    while let Some(record) = read_record(r)? {
429        records.push(record);
430    }
431    Ok((version, records))
432}
433
434/// Read the archive tolerantly: validate the preamble strictly, then read records
435/// until a clean end-of-stream **or the first unreadable frame**, returning the
436/// records collected so far.
437///
438/// A crash while the persistence lane is appending a record can leave a partial
439/// final frame (a truncated length prefix or payload). The strict [`read_archive`]
440/// would reject the whole file for that torn tail - and once a fallback-resume
441/// appends fresh records *past* the torn bytes, the archive would stay unreadable
442/// forever. This variant instead stops at the torn tail and keeps everything valid
443/// before it, so recovery can still fold the archive to its last intact point. The
444/// preamble is still validated strictly, so a file that isn't a run archive at all
445/// still errors rather than folding to nothing.
446pub fn read_archive_lenient(r: &mut dyn Read) -> io::Result<(u16, Vec<RunRecord>)> {
447    let version = read_archive_start(r)?;
448    let mut records = Vec::new();
449    // A torn/invalid frame ends the read early with whatever preceded it, rather
450    // than propagating the error.
451    while let Ok(Some(record)) = read_record(r) {
452        records.push(record);
453    }
454    Ok((version, records))
455}
456
457// ─── fold ───────────────────────────────────────────────────────────────────
458
459/// The state reconstructed from a run journal - enough to resume or inspect the
460/// run at its latest recorded point.
461#[derive(Debug, Clone, PartialEq)]
462pub struct FoldedRun {
463    /// The run's current owner/identity.
464    pub identity: RunIdentity,
465    /// The latest run metadata.
466    pub meta: RunMeta,
467    /// The reconstructed current context window.
468    pub context: ContextSnapshot,
469    /// The recorded inbound messages, in order.
470    pub messages: Vec<MessageRecord>,
471    /// Number of inferences recorded.
472    pub inference_count: usize,
473    /// Number of tool calls recorded.
474    pub tool_call_count: usize,
475}
476
477/// Reconstruct a run's current state from its journal. Returns `None` if the
478/// records don't start with a [`RunRecord::Header`].
479pub fn fold(records: &[RunRecord]) -> Option<FoldedRun> {
480    let mut iter = records.iter();
481    let (identity, meta) = match iter.next() {
482        Some(RunRecord::Header { identity, meta }) => (identity.clone(), (**meta).clone()),
483        _ => return None,
484    };
485    let mut folded = FoldedRun {
486        identity,
487        meta,
488        context: ContextSnapshot {
489            stage_name: String::new(),
490            total_tokens: 0,
491            max_tokens: 0,
492            regions: Vec::new(),
493        },
494        messages: Vec::new(),
495        inference_count: 0,
496        tool_call_count: 0,
497    };
498    for record in iter {
499        match record {
500            RunRecord::Header { identity, meta } => {
501                folded.identity = identity.clone();
502                folded.meta = (**meta).clone();
503            }
504            RunRecord::OwnershipChanged {
505                machine_id,
506                world_id,
507                ..
508            } => {
509                folded.identity.machine_id = machine_id.clone();
510                folded.identity.world_id = world_id.clone();
511            }
512            RunRecord::Inference { .. } => folded.inference_count += 1,
513            RunRecord::ToolBatch { calls, .. } => folded.tool_call_count += calls.len(),
514            RunRecord::ContextCheckpoint { snapshot, .. } => folded.context = snapshot.clone(),
515            RunRecord::ContextDiff { delta, .. } => apply_delta(&mut folded.context, delta),
516            RunRecord::Message { message, .. } => folded.messages.push(message.clone()),
517            RunRecord::StatusChanged { status, .. } => folded.meta.status = status.clone(),
518            RunRecord::Checkpoint { meta, context, .. } => {
519                folded.meta = (**meta).clone();
520                folded.context = context.clone();
521            }
522            RunRecord::Progress { meta, delta, .. } => {
523                folded.meta = (**meta).clone();
524                apply_delta(&mut folded.context, delta);
525            }
526        }
527    }
528    Some(folded)
529}
530
531/// A run's context window at one recorded point in time, with the metadata
532/// (stage, iteration, status, …) in effect then. Produced by [`replay_points`].
533#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
534pub struct RunPoint {
535    /// The run metadata at this point.
536    pub meta: RunMeta,
537    /// The full context window at this point.
538    pub context: ContextSnapshot,
539    /// Unix seconds this point was recorded.
540    pub at: i64,
541}
542
543/// Replay a run journal into the sequence of context-window snapshots over time,
544/// one [`RunPoint`] per record that changes the context (a checkpoint, diff, or
545/// progress step). This is what the context-history views (TUI/CLI/API) consume
546/// to show the window "at each stage and point". Returns an empty vec if the
547/// records don't start with a [`RunRecord::Header`].
548pub fn replay_points(records: &[RunRecord]) -> Vec<RunPoint> {
549    let mut iter = records.iter();
550    let mut meta = match iter.next() {
551        Some(RunRecord::Header { meta, .. }) => (**meta).clone(),
552        _ => return Vec::new(),
553    };
554    let mut context = ContextSnapshot {
555        stage_name: String::new(),
556        total_tokens: 0,
557        max_tokens: 0,
558        regions: Vec::new(),
559    };
560    let mut points = Vec::new();
561    for record in iter {
562        match record {
563            RunRecord::Header { meta: m, .. } => meta = (**m).clone(),
564            RunRecord::StatusChanged { status, .. } => meta.status = status.clone(),
565            RunRecord::ContextCheckpoint { snapshot, at } => {
566                context = snapshot.clone();
567                points.push(RunPoint {
568                    meta: meta.clone(),
569                    context: context.clone(),
570                    at: *at,
571                });
572            }
573            RunRecord::ContextDiff { delta, at } => {
574                apply_delta(&mut context, delta);
575                points.push(RunPoint {
576                    meta: meta.clone(),
577                    context: context.clone(),
578                    at: *at,
579                });
580            }
581            RunRecord::Checkpoint {
582                meta: m,
583                context: c,
584                at,
585            } => {
586                meta = (**m).clone();
587                context = c.clone();
588                points.push(RunPoint {
589                    meta: meta.clone(),
590                    context: context.clone(),
591                    at: *at,
592                });
593            }
594            RunRecord::Progress { meta: m, delta, at } => {
595                meta = (**m).clone();
596                apply_delta(&mut context, delta);
597                points.push(RunPoint {
598                    meta: meta.clone(),
599                    context: context.clone(),
600                    at: *at,
601                });
602            }
603            // Non-context records don't add a timeline point.
604            RunRecord::OwnershipChanged { .. }
605            | RunRecord::Inference { .. }
606            | RunRecord::ToolBatch { .. }
607            | RunRecord::Message { .. } => {}
608        }
609    }
610    points
611}
612
613#[cfg(test)]
614mod tests {
615    use super::*;
616    use crate::run_meta::RunStatus;
617
618    fn identity() -> RunIdentity {
619        RunIdentity {
620            run_id: "run-1".to_string(),
621            machine_id: "machine-a".to_string(),
622            world_id: "world-x".to_string(),
623            created_at: 100,
624        }
625    }
626
627    fn meta() -> RunMeta {
628        RunMeta::new(
629            "run-1".to_string(),
630            "coder".to_string(),
631            "/agents/coder".to_string(),
632            "do it".to_string(),
633            Some("anthropic/claude".to_string()),
634            "/work".to_string(),
635            2,
636        )
637    }
638
639    fn entry(content: &str, tokens: usize) -> RegionEntrySnapshot {
640        RegionEntrySnapshot {
641            content: content.to_string(),
642            tokens,
643            kind: crate::region::EntryKind::Text,
644            metadata: None,
645            key: None,
646            taint: Default::default(),
647        }
648    }
649
650    fn region(name: &str, entries: Vec<RegionEntrySnapshot>) -> RegionSnapshot {
651        let current = entries.iter().map(|e| e.tokens).sum();
652        RegionSnapshot {
653            name: name.to_string(),
654            kind: "clearable".to_string(),
655            current_tokens: current,
656            max_tokens: 1000,
657            entries,
658        }
659    }
660
661    fn snapshot(stage: &str, regions: Vec<RegionSnapshot>) -> ContextSnapshot {
662        let total = regions.iter().map(|r| r.current_tokens).sum();
663        ContextSnapshot {
664            stage_name: stage.to_string(),
665            total_tokens: total,
666            max_tokens: 10_000,
667            regions,
668        }
669    }
670
671    fn header() -> RunRecord {
672        RunRecord::Header {
673            identity: identity(),
674            meta: Box::new(meta()),
675        }
676    }
677
678    /// A stable tag per region-delta shape - asserting on this avoids the
679    /// uncovered `false` arm a `matches!` leaves when the assertion passes.
680    /// Every arm is exercised across the diff tests below.
681    fn region_delta_kind(d: &RegionDelta) -> &'static str {
682        match d {
683            RegionDelta::Set(_) => "set",
684            RegionDelta::Append { .. } => "append",
685            RegionDelta::Clear { .. } => "clear",
686            RegionDelta::Remove { .. } => "remove",
687        }
688    }
689
690    // ── diff / apply round-trips ──
691
692    /// Applying `diff(a, b)` to a clone of `a` must reproduce `b`, for every
693    /// region-delta shape (new, append, clear, remove, full-replace, unchanged).
694    fn assert_diff_roundtrip(a: &ContextSnapshot, b: &ContextSnapshot) {
695        let delta = diff_context(a, b);
696        let mut base = a.clone();
697        apply_delta(&mut base, &delta);
698        assert_eq!(&base, b);
699    }
700
701    #[test]
702    fn diff_append_only_growth_is_compact() {
703        let a = snapshot("s1", vec![region("conv", vec![entry("hi", 1)])]);
704        let b = snapshot(
705            "s1",
706            vec![region("conv", vec![entry("hi", 1), entry("there", 2)])],
707        );
708        let delta = diff_context(&a, &b);
709        assert_eq!(region_delta_kind(&delta.regions[0]), "append");
710        assert_diff_roundtrip(&a, &b);
711    }
712
713    #[test]
714    fn diff_new_region_is_set() {
715        let a = snapshot("s1", vec![region("conv", vec![entry("hi", 1)])]);
716        let b = snapshot(
717            "s1",
718            vec![
719                region("conv", vec![entry("hi", 1)]),
720                region("plan", vec![entry("p", 3)]),
721            ],
722        );
723        let delta = diff_context(&a, &b);
724        assert!(delta.regions.iter().any(|d| region_delta_kind(d) == "set"));
725        assert_diff_roundtrip(&a, &b);
726    }
727
728    #[test]
729    fn diff_cleared_region() {
730        let a = snapshot("s1", vec![region("conv", vec![entry("hi", 1)])]);
731        let b = snapshot("s1", vec![region("conv", vec![])]);
732        let delta = diff_context(&a, &b);
733        assert_eq!(region_delta_kind(&delta.regions[0]), "clear");
734        assert_diff_roundtrip(&a, &b);
735    }
736
737    #[test]
738    fn diff_removed_region() {
739        let a = snapshot(
740            "s1",
741            vec![
742                region("conv", vec![entry("hi", 1)]),
743                region("plan", vec![entry("p", 3)]),
744            ],
745        );
746        let b = snapshot("s1", vec![region("conv", vec![entry("hi", 1)])]);
747        let delta = diff_context(&a, &b);
748        assert!(
749            delta
750                .regions
751                .iter()
752                .any(|d| region_delta_kind(d) == "remove")
753        );
754        assert_diff_roundtrip(&a, &b);
755    }
756
757    #[test]
758    fn diff_non_prefix_rewrite_is_set() {
759        // Entries changed at the front (not an append) → full Set.
760        let a = snapshot("s1", vec![region("conv", vec![entry("old", 1)])]);
761        let b = snapshot("s1", vec![region("conv", vec![entry("new", 1)])]);
762        let delta = diff_context(&a, &b);
763        assert_eq!(region_delta_kind(&delta.regions[0]), "set");
764        assert_diff_roundtrip(&a, &b);
765    }
766
767    #[test]
768    fn diff_kind_change_is_set_not_append() {
769        // Same prefix entries but the region's kind changed → Set, not Append.
770        let a = snapshot("s1", vec![region("conv", vec![entry("hi", 1)])]);
771        let mut grown = region("conv", vec![entry("hi", 1), entry("more", 1)]);
772        grown.kind = "sliding".to_string();
773        let b = snapshot("s1", vec![grown]);
774        let delta = diff_context(&a, &b);
775        assert_eq!(region_delta_kind(&delta.regions[0]), "set");
776        assert_diff_roundtrip(&a, &b);
777    }
778
779    #[test]
780    fn diff_unchanged_region_emits_nothing() {
781        let a = snapshot("s1", vec![region("conv", vec![entry("hi", 1)])]);
782        let b = a.clone();
783        let delta = diff_context(&a, &b);
784        assert!(delta.regions.is_empty());
785        assert_diff_roundtrip(&a, &b);
786    }
787
788    #[test]
789    fn apply_delta_skips_unknown_regions_leniently() {
790        // Append/Clear targeting a region not present are no-ops (not errors).
791        let mut base = snapshot("s1", vec![]);
792        let delta = ContextDelta {
793            stage_name: "s1".to_string(),
794            total_tokens: 0,
795            max_tokens: 10_000,
796            regions: vec![
797                RegionDelta::Append {
798                    name: "ghost".to_string(),
799                    entries: vec![entry("x", 1)],
800                    current_tokens: 1,
801                },
802                RegionDelta::Clear {
803                    name: "ghost".to_string(),
804                },
805                RegionDelta::Remove {
806                    name: "ghost".to_string(),
807                },
808            ],
809        };
810        apply_delta(&mut base, &delta);
811        assert!(base.regions.is_empty());
812    }
813
814    // ── codec round-trips ──
815
816    fn all_record_kinds() -> Vec<RunRecord> {
817        vec![
818            header(),
819            RunRecord::OwnershipChanged {
820                machine_id: "machine-b".to_string(),
821                world_id: "world-y".to_string(),
822                at: 101,
823            },
824            RunRecord::Inference {
825                stage: "plan".to_string(),
826                iteration: 0,
827                request: InferenceRequestRecord {
828                    model: "m".to_string(),
829                    system: vec!["sys".to_string()],
830                    messages: vec![MessageRecord {
831                        role: "user".to_string(),
832                        content: "hi".to_string(),
833                    }],
834                    tool_names: vec!["read_file".to_string()],
835                    temperature: 0.7,
836                    max_tokens: 1024,
837                },
838                response: InferenceResponseRecord {
839                    content: "ok".to_string(),
840                    tool_calls: vec![],
841                    prompt_tokens: 10,
842                    completion_tokens: 5,
843                    cached_tokens: 0,
844                    cache_write_tokens: 0,
845                },
846                at: 102,
847            },
848            RunRecord::ToolBatch {
849                calls: vec![ToolCallRecord {
850                    id: "c1".to_string(),
851                    name: "read_file".to_string(),
852                    arguments: "{}".to_string(),
853                    result: Some("body".to_string()),
854                }],
855                at: 103,
856            },
857            RunRecord::ContextCheckpoint {
858                snapshot: snapshot("plan", vec![region("conv", vec![entry("hi", 1)])]),
859                at: 104,
860            },
861            RunRecord::ContextDiff {
862                delta: ContextDelta {
863                    stage_name: "plan".to_string(),
864                    total_tokens: 3,
865                    max_tokens: 10_000,
866                    regions: vec![RegionDelta::Append {
867                        name: "conv".to_string(),
868                        entries: vec![entry("more", 2)],
869                        current_tokens: 3,
870                    }],
871                },
872                at: 105,
873            },
874            RunRecord::Message {
875                message: MessageRecord {
876                    role: "user".to_string(),
877                    content: "another".to_string(),
878                },
879                at: 106,
880            },
881            RunRecord::StatusChanged {
882                status: RunStatus::Complete,
883                at: 107,
884            },
885            RunRecord::Checkpoint {
886                meta: Box::new(meta()),
887                context: snapshot("plan", vec![region("conv", vec![entry("hi", 1)])]),
888                at: 108,
889            },
890            RunRecord::Progress {
891                meta: Box::new(meta()),
892                delta: ContextDelta {
893                    stage_name: "plan".to_string(),
894                    total_tokens: 3,
895                    max_tokens: 10_000,
896                    regions: vec![RegionDelta::Append {
897                        name: "conv".to_string(),
898                        entries: vec![entry("step", 2)],
899                        current_tokens: 3,
900                    }],
901                },
902                at: 109,
903            },
904        ]
905    }
906
907    #[test]
908    fn archive_write_then_read_roundtrips_every_record_kind() {
909        let records = all_record_kinds();
910        let mut buf = Vec::new();
911        write_archive_start(&mut buf, RUN_ARCHIVE_VERSION).unwrap();
912        for r in &records {
913            write_record(&mut buf, r).unwrap();
914        }
915        let (version, read) = read_archive(&mut buf.as_slice()).unwrap();
916        assert_eq!(version, RUN_ARCHIVE_VERSION);
917        assert_eq!(read, records);
918    }
919
920    #[test]
921    fn read_archive_start_rejects_bad_magic() {
922        let mut bytes: &[u8] = b"XXXX\x00\x01";
923        let err = read_archive_start(&mut bytes).unwrap_err();
924        assert_eq!(err.kind(), io::ErrorKind::InvalidData);
925    }
926
927    #[test]
928    fn read_archive_start_reports_version() {
929        let mut buf = Vec::new();
930        write_archive_start(&mut buf, 7).unwrap();
931        assert_eq!(read_archive_start(&mut buf.as_slice()).unwrap(), 7);
932    }
933
934    #[test]
935    fn read_record_returns_none_at_clean_eof() {
936        let empty: &[u8] = &[];
937        assert!(read_record(&mut { empty }).unwrap().is_none());
938    }
939
940    #[test]
941    fn read_record_errors_on_truncated_length_prefix() {
942        // Two bytes where an 8-byte length is expected → partial read → error.
943        let mut bytes: &[u8] = &[0, 0];
944        let err = read_record(&mut bytes).unwrap_err();
945        assert_eq!(err.kind(), io::ErrorKind::UnexpectedEof);
946    }
947
948    #[test]
949    fn read_record_errors_on_truncated_payload() {
950        // A frame claiming 10 bytes but only 2 present after the 8-byte length.
951        let mut bytes: &[u8] = &[0, 0, 0, 0, 0, 0, 0, 10, 1, 2];
952        let err = read_record(&mut bytes).unwrap_err();
953        assert_eq!(err.kind(), io::ErrorKind::UnexpectedEof);
954    }
955
956    #[test]
957    fn read_record_errors_on_empty_payload_at_boundary() {
958        // A non-zero length with zero payload bytes → clean EOF at the payload
959        // start is still a truncation (the frame promised bytes).
960        let mut bytes: &[u8] = &[0, 0, 0, 0, 0, 0, 0, 10];
961        let err = read_record(&mut bytes).unwrap_err();
962        assert_eq!(err.kind(), io::ErrorKind::UnexpectedEof);
963    }
964
965    #[test]
966    fn read_record_errors_on_invalid_json_payload() {
967        // A well-framed payload that isn't a valid RunRecord.
968        let mut buf = Vec::new();
969        let bad = b"not json";
970        buf.extend_from_slice(&(bad.len() as u64).to_be_bytes());
971        buf.extend_from_slice(bad);
972        let err = read_record(&mut buf.as_slice()).unwrap_err();
973        assert_eq!(err.kind(), io::ErrorKind::InvalidData);
974    }
975
976    /// A reader whose `read` always errors, to exercise the read error path
977    /// inside `read_exact_or_eof` (distinct from a clean EOF).
978    struct FailingReader;
979    impl Read for FailingReader {
980        fn read(&mut self, _buf: &mut [u8]) -> io::Result<usize> {
981            Err(io::Error::other("device error"))
982        }
983    }
984
985    #[test]
986    fn read_record_propagates_reader_errors() {
987        let err = read_record(&mut FailingReader).unwrap_err();
988        assert_eq!(err.kind(), io::ErrorKind::Other);
989    }
990
991    #[test]
992    fn read_archive_propagates_a_bad_preamble() {
993        // Too short to even hold the magic → the preamble read errors.
994        let mut bytes: &[u8] = b"LV";
995        assert!(read_archive(&mut bytes).is_err());
996    }
997
998    #[test]
999    fn read_archive_propagates_a_bad_frame() {
1000        // Valid preamble, then a truncated frame → the record read errors.
1001        let mut buf = Vec::new();
1002        write_archive_start(&mut buf, RUN_ARCHIVE_VERSION).unwrap();
1003        buf.extend_from_slice(&[0, 0, 0, 0, 0, 0, 0, 5, 1, 2]); // len 5, 2 present
1004        let err = read_archive(&mut buf.as_slice()).unwrap_err();
1005        assert_eq!(err.kind(), io::ErrorKind::UnexpectedEof);
1006    }
1007
1008    /// A writer that fails after `ok_bytes` bytes, to exercise write error paths.
1009    struct FailAfter {
1010        remaining: usize,
1011    }
1012    impl Write for FailAfter {
1013        fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
1014            if self.remaining == 0 {
1015                return Err(io::Error::other("disk full"));
1016            }
1017            let n = buf.len().min(self.remaining);
1018            self.remaining -= n;
1019            Ok(n)
1020        }
1021        fn flush(&mut self) -> io::Result<()> {
1022            Ok(())
1023        }
1024    }
1025
1026    #[test]
1027    fn fail_after_writer_flush_is_a_noop() {
1028        assert!(FailAfter { remaining: 1 }.flush().is_ok());
1029    }
1030
1031    #[test]
1032    fn write_archive_start_propagates_write_errors() {
1033        // Fail on the magic write (0 bytes allowed) and on the version write.
1034        assert!(write_archive_start(&mut FailAfter { remaining: 0 }, 1).is_err());
1035        assert!(write_archive_start(&mut FailAfter { remaining: 4 }, 1).is_err());
1036    }
1037
1038    #[test]
1039    fn write_record_propagates_write_errors() {
1040        let rec = header();
1041        // Fail on the 8-byte length prefix, and (after it) on the payload.
1042        assert!(write_record(&mut FailAfter { remaining: 0 }, &rec).is_err());
1043        assert!(write_record(&mut FailAfter { remaining: 8 }, &rec).is_err());
1044    }
1045
1046    #[test]
1047    fn read_archive_lenient_matches_strict_on_a_clean_archive() {
1048        // With no torn tail, the lenient reader returns exactly what the strict
1049        // reader does.
1050        let records = all_record_kinds();
1051        let mut buf = Vec::new();
1052        write_archive_start(&mut buf, RUN_ARCHIVE_VERSION).unwrap();
1053        for r in &records {
1054            write_record(&mut buf, r).unwrap();
1055        }
1056        let (version, read) = read_archive_lenient(&mut buf.as_slice()).unwrap();
1057        assert_eq!(version, RUN_ARCHIVE_VERSION);
1058        assert_eq!(read, records);
1059    }
1060
1061    #[test]
1062    fn read_archive_lenient_keeps_valid_prefix_before_a_torn_tail() {
1063        // A valid preamble + two full records, then a truncated frame (a crash
1064        // mid-append). The strict reader would reject the whole file; the lenient
1065        // reader returns the two intact records and stops at the torn tail.
1066        let mut buf = Vec::new();
1067        write_archive_start(&mut buf, RUN_ARCHIVE_VERSION).unwrap();
1068        write_record(&mut buf, &header()).unwrap();
1069        write_record(
1070            &mut buf,
1071            &RunRecord::ContextCheckpoint {
1072                snapshot: snapshot("plan", vec![region("conv", vec![entry("hi", 1)])]),
1073                at: 1,
1074            },
1075        )
1076        .unwrap();
1077        // A frame claiming 10 payload bytes but only 2 present → torn tail.
1078        buf.extend_from_slice(&[0, 0, 0, 0, 0, 0, 0, 10, 1, 2]);
1079
1080        // Strict rejects the whole archive.
1081        assert!(read_archive(&mut buf.as_slice()).is_err());
1082        // Lenient keeps the valid prefix and folds cleanly.
1083        let (version, records) = read_archive_lenient(&mut buf.as_slice()).unwrap();
1084        assert_eq!(version, RUN_ARCHIVE_VERSION);
1085        assert_eq!(records.len(), 2);
1086        let folded = fold(&records).expect("prefix starts with a Header");
1087        assert_eq!(folded.context.regions[0].entries.len(), 1);
1088    }
1089
1090    #[test]
1091    fn read_archive_lenient_still_errors_on_a_bad_preamble() {
1092        // The preamble is validated strictly: a file that isn't a run archive at
1093        // all errors rather than folding to nothing.
1094        let mut bad_magic: &[u8] = b"XXXX\x00\x01";
1095        assert!(read_archive_lenient(&mut bad_magic).is_err());
1096        // A truncated version (valid magic, no version bytes) also errors.
1097        let mut short: &[u8] = b"LVR1";
1098        assert!(read_archive_lenient(&mut short).is_err());
1099    }
1100
1101    #[test]
1102    fn read_archive_start_errors_on_truncated_version() {
1103        // Valid 4-byte magic but no version bytes → the version read errors.
1104        let mut bytes: &[u8] = b"LVR1";
1105        let err = read_archive_start(&mut bytes).unwrap_err();
1106        assert_eq!(err.kind(), io::ErrorKind::UnexpectedEof);
1107    }
1108
1109    // ── fold ──
1110
1111    #[test]
1112    fn fold_requires_a_header_first() {
1113        assert!(fold(&[]).is_none());
1114        assert!(
1115            fold(&[RunRecord::StatusChanged {
1116                status: RunStatus::Complete,
1117                at: 1
1118            }])
1119            .is_none()
1120        );
1121    }
1122
1123    #[test]
1124    fn fold_reconstructs_state_from_the_journal() {
1125        let records = all_record_kinds();
1126        let folded = fold(&records).expect("has header");
1127        // Ownership was reassigned mid-journal.
1128        assert_eq!(folded.identity.machine_id, "machine-b");
1129        assert_eq!(folded.identity.world_id, "world-y");
1130        // Counters.
1131        assert_eq!(folded.inference_count, 1);
1132        assert_eq!(folded.tool_call_count, 1);
1133        // One inbound message recorded.
1134        assert_eq!(folded.messages.len(), 1);
1135        assert_eq!(folded.messages[0].content, "another");
1136        // The Progress step is the last context-affecting record: it layers its
1137        // append diff onto the preceding Checkpoint's window (hi + step).
1138        assert_eq!(folded.context.regions[0].name, "conv");
1139        assert_eq!(folded.context.regions[0].entries.len(), 2);
1140        assert_eq!(folded.context.total_tokens, 3);
1141        assert_eq!(folded.meta.run_id, "run-1");
1142    }
1143
1144    #[test]
1145    fn fold_applies_context_diffs_over_a_checkpoint() {
1146        // Header → checkpoint → diff (append). The diff must layer on the checkpoint.
1147        let records = vec![
1148            header(),
1149            RunRecord::ContextCheckpoint {
1150                snapshot: snapshot("plan", vec![region("conv", vec![entry("hi", 1)])]),
1151                at: 1,
1152            },
1153            RunRecord::ContextDiff {
1154                delta: ContextDelta {
1155                    stage_name: "plan".to_string(),
1156                    total_tokens: 3,
1157                    max_tokens: 10_000,
1158                    regions: vec![RegionDelta::Append {
1159                        name: "conv".to_string(),
1160                        entries: vec![entry("there", 2)],
1161                        current_tokens: 3,
1162                    }],
1163                },
1164                at: 2,
1165            },
1166        ];
1167        let folded = fold(&records).unwrap();
1168        assert_eq!(folded.context.regions[0].entries.len(), 2);
1169        assert_eq!(folded.context.total_tokens, 3);
1170    }
1171
1172    #[test]
1173    fn fold_later_header_updates_identity_and_meta() {
1174        // A second Header (unusual, but tolerated) refreshes identity + meta.
1175        let mut second_meta = meta();
1176        second_meta.status = RunStatus::Running;
1177        let records = vec![
1178            header(),
1179            RunRecord::Header {
1180                identity: RunIdentity {
1181                    run_id: "run-1".to_string(),
1182                    machine_id: "machine-c".to_string(),
1183                    world_id: "world-z".to_string(),
1184                    created_at: 200,
1185                },
1186                meta: Box::new(second_meta),
1187            },
1188        ];
1189        let folded = fold(&records).unwrap();
1190        assert_eq!(folded.identity.machine_id, "machine-c");
1191        assert_eq!(folded.meta.status, RunStatus::Running);
1192    }
1193
1194    #[test]
1195    fn fold_progress_applies_meta_and_context_diff() {
1196        let mut advanced = meta();
1197        advanced.status = RunStatus::Running;
1198        advanced.iteration = 5;
1199        let records = vec![
1200            header(),
1201            RunRecord::ContextCheckpoint {
1202                snapshot: snapshot("plan", vec![region("conv", vec![entry("hi", 1)])]),
1203                at: 1,
1204            },
1205            RunRecord::Progress {
1206                meta: Box::new(advanced),
1207                delta: ContextDelta {
1208                    stage_name: "plan".to_string(),
1209                    total_tokens: 3,
1210                    max_tokens: 10_000,
1211                    regions: vec![RegionDelta::Append {
1212                        name: "conv".to_string(),
1213                        entries: vec![entry("there", 2)],
1214                        current_tokens: 3,
1215                    }],
1216                },
1217                at: 2,
1218            },
1219        ];
1220        let folded = fold(&records).unwrap();
1221        assert_eq!(folded.meta.iteration, 5);
1222        assert_eq!(folded.meta.status, RunStatus::Running);
1223        assert_eq!(folded.context.regions[0].entries.len(), 2);
1224    }
1225
1226    // ── replay_points (context-window history) ──
1227
1228    #[test]
1229    fn replay_points_requires_a_header() {
1230        assert!(replay_points(&[]).is_empty());
1231        assert!(
1232            replay_points(&[RunRecord::Message {
1233                message: MessageRecord {
1234                    role: "user".to_string(),
1235                    content: "x".to_string(),
1236                },
1237                at: 1,
1238            }])
1239            .is_empty()
1240        );
1241    }
1242
1243    #[test]
1244    fn replay_points_emits_a_snapshot_per_context_change() {
1245        // Header (no point) → checkpoint (point 1) → status (no point, but tracked)
1246        // → progress diff (point 2). Non-context records don't add points.
1247        let mut running = meta();
1248        running.status = RunStatus::Running;
1249        let records = vec![
1250            header(),
1251            RunRecord::Inference {
1252                stage: "plan".to_string(),
1253                iteration: 0,
1254                request: InferenceRequestRecord {
1255                    model: "m".to_string(),
1256                    system: vec![],
1257                    messages: vec![],
1258                    tool_names: vec![],
1259                    temperature: 0.7,
1260                    max_tokens: 10,
1261                },
1262                response: InferenceResponseRecord {
1263                    content: "ok".to_string(),
1264                    tool_calls: vec![],
1265                    prompt_tokens: 1,
1266                    completion_tokens: 1,
1267                    cached_tokens: 0,
1268                    cache_write_tokens: 0,
1269                },
1270                at: 1,
1271            },
1272            RunRecord::ContextCheckpoint {
1273                snapshot: snapshot("plan", vec![region("conv", vec![entry("hi", 1)])]),
1274                at: 2,
1275            },
1276            RunRecord::StatusChanged {
1277                status: RunStatus::Running,
1278                at: 3,
1279            },
1280            RunRecord::Progress {
1281                meta: Box::new(running),
1282                delta: ContextDelta {
1283                    stage_name: "implement".to_string(),
1284                    total_tokens: 3,
1285                    max_tokens: 10_000,
1286                    regions: vec![RegionDelta::Append {
1287                        name: "conv".to_string(),
1288                        entries: vec![entry("more", 2)],
1289                        current_tokens: 3,
1290                    }],
1291                },
1292                at: 4,
1293            },
1294        ];
1295        let points = replay_points(&records);
1296        assert_eq!(points.len(), 2, "one point per context change");
1297        // First point: the checkpoint window.
1298        assert_eq!(points[0].at, 2);
1299        assert_eq!(points[0].context.regions[0].entries.len(), 1);
1300        // Second point: the progress diff layered on, with the running status
1301        // carried from the StatusChanged + the progress meta.
1302        assert_eq!(points[1].at, 4);
1303        assert_eq!(points[1].context.regions[0].entries.len(), 2);
1304        assert_eq!(points[1].context.stage_name, "implement");
1305        assert_eq!(points[1].meta.status, RunStatus::Running);
1306    }
1307
1308    #[test]
1309    fn replay_points_handles_context_diff_and_a_later_header() {
1310        // A standalone ContextDiff is a point; a second Header refreshes meta
1311        // without adding a point.
1312        let mut relabeled = meta();
1313        relabeled.agent_name = "renamed".to_string();
1314        let records = vec![
1315            header(),
1316            RunRecord::ContextCheckpoint {
1317                snapshot: snapshot("plan", vec![region("conv", vec![entry("hi", 1)])]),
1318                at: 1,
1319            },
1320            RunRecord::Header {
1321                identity: identity(),
1322                meta: Box::new(relabeled),
1323            },
1324            RunRecord::ContextDiff {
1325                delta: ContextDelta {
1326                    stage_name: "plan".to_string(),
1327                    total_tokens: 3,
1328                    max_tokens: 10_000,
1329                    regions: vec![RegionDelta::Append {
1330                        name: "conv".to_string(),
1331                        entries: vec![entry("more", 2)],
1332                        current_tokens: 3,
1333                    }],
1334                },
1335                at: 2,
1336            },
1337        ];
1338        let points = replay_points(&records);
1339        assert_eq!(points.len(), 2); // checkpoint + diff (header adds no point)
1340        assert_eq!(points[1].context.regions[0].entries.len(), 2);
1341        // The later Header's meta is in effect at the diff point.
1342        assert_eq!(points[1].meta.agent_name, "renamed");
1343    }
1344
1345    #[test]
1346    fn replay_points_over_a_full_checkpoint() {
1347        // A `Checkpoint` (full meta+context) is also a point.
1348        let records = vec![
1349            header(),
1350            RunRecord::Checkpoint {
1351                meta: Box::new(meta()),
1352                context: snapshot("review", vec![region("conv", vec![entry("x", 4)])]),
1353                at: 9,
1354            },
1355        ];
1356        let points = replay_points(&records);
1357        assert_eq!(points.len(), 1);
1358        assert_eq!(points[0].context.stage_name, "review");
1359        assert_eq!(points[0].context.regions[0].entries[0].tokens, 4);
1360    }
1361}