1use std::io::{self, Read, Write};
42
43use serde::{Deserialize, Serialize};
44
45use crate::run_meta::{ContextSnapshot, RegionEntrySnapshot, RegionSnapshot, RunMeta, RunStatus};
46
47pub const RUN_ARCHIVE_MAGIC: &[u8; 4] = b"LVR1";
49
50pub const RUN_ARCHIVE_VERSION: u16 = 1;
52
53#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
60pub struct RunIdentity {
61 pub run_id: String,
63 pub machine_id: String,
65 pub world_id: String,
67 pub created_at: i64,
69}
70
71#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
73pub struct MessageRecord {
74 pub role: String,
76 pub content: String,
78}
79
80#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
82pub struct ToolCallRecord {
83 pub id: String,
85 pub name: String,
87 pub arguments: String,
89 pub result: Option<String>,
91}
92
93#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
96pub struct InferenceRequestRecord {
97 pub model: String,
99 pub system: Vec<String>,
101 pub messages: Vec<MessageRecord>,
103 pub tool_names: Vec<String>,
105 pub temperature: f32,
107 pub max_tokens: usize,
109}
110
111#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
113pub struct InferenceResponseRecord {
114 pub content: String,
116 pub tool_calls: Vec<ToolCallRecord>,
118 pub prompt_tokens: usize,
120 pub completion_tokens: usize,
122 pub cached_tokens: usize,
124 pub cache_write_tokens: usize,
126}
127
128#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
130pub enum RegionDelta {
131 Set(RegionSnapshot),
134 Append {
137 name: String,
139 entries: Vec<RegionEntrySnapshot>,
141 current_tokens: usize,
143 },
144 Clear {
146 name: String,
148 },
149 Remove {
151 name: String,
153 },
154}
155
156#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
158pub struct ContextDelta {
159 pub stage_name: String,
161 pub total_tokens: usize,
163 pub max_tokens: usize,
165 pub regions: Vec<RegionDelta>,
167}
168
169#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
171pub enum RunRecord {
172 Header {
174 identity: RunIdentity,
176 meta: Box<RunMeta>,
178 },
179 OwnershipChanged {
181 machine_id: String,
183 world_id: String,
185 at: i64,
187 },
188 Inference {
190 stage: String,
192 iteration: usize,
194 request: InferenceRequestRecord,
196 response: InferenceResponseRecord,
198 at: i64,
200 },
201 ToolBatch {
203 calls: Vec<ToolCallRecord>,
205 at: i64,
207 },
208 ContextCheckpoint {
210 snapshot: ContextSnapshot,
212 at: i64,
214 },
215 ContextDiff {
217 delta: ContextDelta,
219 at: i64,
221 },
222 Message {
224 message: MessageRecord,
226 at: i64,
228 },
229 StatusChanged {
231 status: RunStatus,
233 at: i64,
235 },
236 Checkpoint {
239 meta: Box<RunMeta>,
241 context: ContextSnapshot,
243 at: i64,
245 },
246 Progress {
251 meta: Box<RunMeta>,
253 delta: ContextDelta,
255 at: i64,
257 },
258}
259
260fn is_prefix(prev: &[RegionEntrySnapshot], next: &[RegionEntrySnapshot]) -> bool {
264 prev.len() <= next.len() && next[..prev.len()] == *prev
265}
266
267pub 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 } 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
312pub 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
350pub 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
359pub 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
374pub 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
384fn 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); }
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
405pub 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
424pub 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
434pub 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 while let Ok(Some(record)) = read_record(r) {
452 records.push(record);
453 }
454 Ok((version, records))
455}
456
457#[derive(Debug, Clone, PartialEq)]
462pub struct FoldedRun {
463 pub identity: RunIdentity,
465 pub meta: RunMeta,
467 pub context: ContextSnapshot,
469 pub messages: Vec<MessageRecord>,
471 pub inference_count: usize,
473 pub tool_call_count: usize,
475}
476
477pub 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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
534pub struct RunPoint {
535 pub meta: RunMeta,
537 pub context: ContextSnapshot,
539 pub at: i64,
541}
542
543pub 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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]); let err = read_archive(&mut buf.as_slice()).unwrap_err();
1005 assert_eq!(err.kind(), io::ErrorKind::UnexpectedEof);
1006 }
1007
1008 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 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 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 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 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 buf.extend_from_slice(&[0, 0, 0, 0, 0, 0, 0, 10, 1, 2]);
1079
1080 assert!(read_archive(&mut buf.as_slice()).is_err());
1082 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 let mut bad_magic: &[u8] = b"XXXX\x00\x01";
1095 assert!(read_archive_lenient(&mut bad_magic).is_err());
1096 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 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 #[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 assert_eq!(folded.identity.machine_id, "machine-b");
1129 assert_eq!(folded.identity.world_id, "world-y");
1130 assert_eq!(folded.inference_count, 1);
1132 assert_eq!(folded.tool_call_count, 1);
1133 assert_eq!(folded.messages.len(), 1);
1135 assert_eq!(folded.messages[0].content, "another");
1136 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 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 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 #[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 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 assert_eq!(points[0].at, 2);
1299 assert_eq!(points[0].context.regions[0].entries.len(), 1);
1300 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 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); assert_eq!(points[1].context.regions[0].entries.len(), 2);
1341 assert_eq!(points[1].meta.agent_name, "renamed");
1343 }
1344
1345 #[test]
1346 fn replay_points_over_a_full_checkpoint() {
1347 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}