1use std::io::{self, Read, Write};
42use std::ops::ControlFlow;
43
44use serde::{Deserialize, Serialize};
45
46use crate::run_meta::{ContextSnapshot, RegionEntrySnapshot, RegionSnapshot, RunMeta, RunStatus};
47
48pub const RUN_ARCHIVE_MAGIC: &[u8; 4] = b"LVR1";
50
51pub const RUN_ARCHIVE_VERSION: u16 = 1;
53
54#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
61pub struct RunIdentity {
62 pub run_id: String,
64 pub machine_id: String,
66 pub world_id: String,
68 pub created_at: i64,
70}
71
72#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
74pub struct MessageRecord {
75 pub role: String,
77 pub content: String,
79}
80
81#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
83pub struct ToolCallRecord {
84 pub id: String,
86 pub name: String,
88 pub arguments: String,
90 pub result: Option<String>,
92 #[serde(default, skip_serializing_if = "Option::is_none")]
96 pub thought_signature: Option<String>,
97}
98
99#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
102pub struct InferenceRequestRecord {
103 pub model: String,
105 pub system: Vec<String>,
107 pub messages: Vec<MessageRecord>,
109 pub tool_names: Vec<String>,
111 pub temperature: f32,
113 pub max_tokens: usize,
115}
116
117#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
119pub struct InferenceResponseRecord {
120 pub content: String,
122 pub tool_calls: Vec<ToolCallRecord>,
124 pub prompt_tokens: usize,
126 pub completion_tokens: usize,
128 pub cached_tokens: usize,
130 pub cache_write_tokens: usize,
132}
133
134#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
136pub enum RegionDelta {
137 Set(RegionSnapshot),
140 Append {
143 name: String,
145 entries: Vec<RegionEntrySnapshot>,
147 current_tokens: usize,
149 },
150 Clear {
152 name: String,
154 },
155 Remove {
157 name: String,
159 },
160}
161
162#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
164pub struct ContextDelta {
165 pub stage_name: String,
167 pub total_tokens: usize,
169 pub max_tokens: usize,
171 pub regions: Vec<RegionDelta>,
173}
174
175#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
183#[serde(rename_all = "snake_case")]
184pub enum InferenceKind {
185 #[default]
189 Stage,
190 Compaction,
192 Title,
194 Routing,
196}
197
198impl InferenceKind {
199 pub fn label(&self) -> &'static str {
201 match self {
202 InferenceKind::Stage => "stage",
203 InferenceKind::Compaction => "compaction",
204 InferenceKind::Title => "title",
205 InferenceKind::Routing => "routing",
206 }
207 }
208
209 pub fn is_stage_work(&self) -> bool {
212 matches!(self, InferenceKind::Stage)
213 }
214}
215
216#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
218pub enum RunRecord {
219 Header {
221 identity: RunIdentity,
223 meta: Box<RunMeta>,
225 },
226 OwnershipChanged {
228 machine_id: String,
230 world_id: String,
232 at: i64,
234 },
235 Inference {
237 stage: String,
239 iteration: usize,
241 request: InferenceRequestRecord,
243 response: InferenceResponseRecord,
245 at: i64,
247 },
248 InferenceUsage {
263 #[serde(default)]
265 kind: InferenceKind,
266 stage: String,
269 iteration: usize,
271 provider: String,
273 model: String,
275 prompt_tokens: usize,
277 completion_tokens: usize,
279 cached_tokens: usize,
281 cache_write_tokens: usize,
283 at: i64,
285 },
286 ToolBatch {
294 calls: Vec<ToolCallRecord>,
296 at: i64,
298 #[serde(default)]
300 stage_index: usize,
301 #[serde(default)]
304 iteration: usize,
305 #[serde(default)]
307 response: String,
308 },
309 ToolCallDone {
311 iteration: usize,
313 call_id: String,
315 result: String,
317 at: i64,
319 },
320 ContextCheckpoint {
322 snapshot: ContextSnapshot,
324 at: i64,
326 },
327 ContextDiff {
329 delta: ContextDelta,
331 at: i64,
333 },
334 Message {
336 message: MessageRecord,
338 at: i64,
340 },
341 StatusChanged {
343 status: RunStatus,
345 at: i64,
347 },
348 Checkpoint {
351 meta: Box<RunMeta>,
353 context: ContextSnapshot,
355 at: i64,
357 },
358 Progress {
363 meta: Box<RunMeta>,
365 delta: ContextDelta,
367 at: i64,
369 },
370}
371
372fn is_prefix(prev: &[RegionEntrySnapshot], next: &[RegionEntrySnapshot]) -> bool {
376 prev.len() <= next.len() && next[..prev.len()] == *prev
377}
378
379pub fn diff_context(prev: &ContextSnapshot, next: &ContextSnapshot) -> ContextDelta {
383 let mut regions = Vec::new();
384 for nr in &next.regions {
385 match prev.regions.iter().find(|r| r.name == nr.name) {
386 None => regions.push(RegionDelta::Set(nr.clone())),
387 Some(pr) => {
388 if pr == nr {
389 } else if nr.entries.is_empty() && !pr.entries.is_empty() {
391 regions.push(RegionDelta::Clear {
392 name: nr.name.clone(),
393 });
394 } else if pr.kind == nr.kind
395 && pr.max_tokens == nr.max_tokens
396 && is_prefix(&pr.entries, &nr.entries)
397 {
398 regions.push(RegionDelta::Append {
399 name: nr.name.clone(),
400 entries: nr.entries[pr.entries.len()..].to_vec(),
401 current_tokens: nr.current_tokens,
402 });
403 } else {
404 regions.push(RegionDelta::Set(nr.clone()));
405 }
406 }
407 }
408 }
409 for pr in &prev.regions {
410 if !next.regions.iter().any(|r| r.name == pr.name) {
411 regions.push(RegionDelta::Remove {
412 name: pr.name.clone(),
413 });
414 }
415 }
416 ContextDelta {
417 stage_name: next.stage_name.clone(),
418 total_tokens: next.total_tokens,
419 max_tokens: next.max_tokens,
420 regions,
421 }
422}
423
424#[derive(Debug, Clone, PartialEq)]
435pub struct RegionDigest {
436 pub name: String,
438 pub kind: String,
440 pub current_tokens: usize,
442 pub max_tokens: usize,
444 pub entries: Vec<u64>,
446}
447
448#[derive(Debug, Clone, PartialEq, Default)]
450pub struct ContextDigest {
451 pub regions: Vec<RegionDigest>,
453}
454
455fn entry_digest(entry: &RegionEntrySnapshot) -> u64 {
459 use std::hash::{Hash, Hasher};
460 let mut hasher = std::collections::hash_map::DefaultHasher::new();
461 entry.content.hash(&mut hasher);
462 entry.tokens.hash(&mut hasher);
463 entry.key.hash(&mut hasher);
464 serde_json::to_string(&entry.kind)
467 .expect("EntryKind always serializes")
468 .hash(&mut hasher);
469 serde_json::to_string(&entry.metadata)
470 .expect("entry metadata always serializes")
471 .hash(&mut hasher);
472 serde_json::to_string(&entry.taint)
473 .expect("taint always serializes")
474 .hash(&mut hasher);
475 hasher.finish()
476}
477
478pub fn digest_context(snapshot: &ContextSnapshot) -> ContextDigest {
480 ContextDigest {
481 regions: snapshot
482 .regions
483 .iter()
484 .map(|r| RegionDigest {
485 name: r.name.clone(),
486 kind: r.kind.clone(),
487 current_tokens: r.current_tokens,
488 max_tokens: r.max_tokens,
489 entries: r.entries.iter().map(entry_digest).collect(),
490 })
491 .collect(),
492 }
493}
494
495fn is_prefix_digest(prev: &[u64], next: &[RegionEntrySnapshot]) -> bool {
497 prev.len() <= next.len()
498 && prev
499 .iter()
500 .zip(next)
501 .all(|(hash, entry)| *hash == entry_digest(entry))
502}
503
504pub fn diff_context_digest(prev: &ContextDigest, next: &ContextSnapshot) -> ContextDelta {
509 let mut regions = Vec::new();
510 for nr in &next.regions {
511 match prev.regions.iter().find(|r| r.name == nr.name) {
512 None => regions.push(RegionDelta::Set(nr.clone())),
513 Some(pr) => {
514 let unchanged = pr.kind == nr.kind
515 && pr.max_tokens == nr.max_tokens
516 && pr.current_tokens == nr.current_tokens
517 && pr.entries.len() == nr.entries.len()
518 && is_prefix_digest(&pr.entries, &nr.entries);
519 if unchanged {
520 } else if nr.entries.is_empty() && !pr.entries.is_empty() {
522 regions.push(RegionDelta::Clear {
523 name: nr.name.clone(),
524 });
525 } else if pr.kind == nr.kind
526 && pr.max_tokens == nr.max_tokens
527 && is_prefix_digest(&pr.entries, &nr.entries)
528 {
529 regions.push(RegionDelta::Append {
530 name: nr.name.clone(),
531 entries: nr.entries[pr.entries.len()..].to_vec(),
532 current_tokens: nr.current_tokens,
533 });
534 } else {
535 regions.push(RegionDelta::Set(nr.clone()));
536 }
537 }
538 }
539 }
540 for pr in &prev.regions {
541 if !next.regions.iter().any(|r| r.name == pr.name) {
542 regions.push(RegionDelta::Remove {
543 name: pr.name.clone(),
544 });
545 }
546 }
547 ContextDelta {
548 stage_name: next.stage_name.clone(),
549 total_tokens: next.total_tokens,
550 max_tokens: next.max_tokens,
551 regions,
552 }
553}
554
555pub fn apply_delta(base: &mut ContextSnapshot, delta: &ContextDelta) {
559 base.stage_name = delta.stage_name.clone();
560 base.total_tokens = delta.total_tokens;
561 base.max_tokens = delta.max_tokens;
562 for region_delta in &delta.regions {
563 match region_delta {
564 RegionDelta::Set(snapshot) => {
565 match base.regions.iter_mut().find(|r| r.name == snapshot.name) {
566 Some(existing) => *existing = snapshot.clone(),
567 None => base.regions.push(snapshot.clone()),
568 }
569 }
570 RegionDelta::Append {
571 name,
572 entries,
573 current_tokens,
574 } => {
575 if let Some(region) = base.regions.iter_mut().find(|r| &r.name == name) {
576 region.entries.extend(entries.iter().cloned());
577 region.current_tokens = *current_tokens;
578 }
579 }
580 RegionDelta::Clear { name } => {
581 if let Some(region) = base.regions.iter_mut().find(|r| &r.name == name) {
582 region.entries.clear();
583 region.current_tokens = 0;
584 }
585 }
586 RegionDelta::Remove { name } => {
587 base.regions.retain(|r| &r.name != name);
588 }
589 }
590 }
591}
592
593pub fn write_archive_start(w: &mut dyn Write, version: u16) -> io::Result<()> {
597 w.write_all(RUN_ARCHIVE_MAGIC)?;
598 w.write_all(&version.to_be_bytes())?;
599 Ok(())
600}
601
602pub fn read_archive_start(r: &mut dyn Read) -> io::Result<u16> {
604 let mut magic = [0u8; 4];
605 r.read_exact(&mut magic)?;
606 if &magic != RUN_ARCHIVE_MAGIC {
607 return Err(io::Error::new(
608 io::ErrorKind::InvalidData,
609 "not a leviath run archive (bad magic)",
610 ));
611 }
612 let mut version = [0u8; 2];
613 r.read_exact(&mut version)?;
614 Ok(u16::from_be_bytes(version))
615}
616
617pub fn write_record(w: &mut dyn Write, record: &RunRecord) -> io::Result<()> {
620 let payload = serde_json::to_vec(record).expect("a RunRecord always serializes to JSON");
621 let len = payload.len() as u64;
622 w.write_all(&len.to_be_bytes())?;
623 w.write_all(&payload)?;
624 Ok(())
625}
626
627fn read_exact_or_eof(r: &mut dyn Read, buf: &mut [u8]) -> io::Result<bool> {
630 let mut filled = 0;
631 while filled < buf.len() {
632 match r.read(&mut buf[filled..])? {
633 0 => {
634 if filled == 0 {
635 return Ok(false); }
637 return Err(io::Error::new(
638 io::ErrorKind::UnexpectedEof,
639 "truncated run-archive frame",
640 ));
641 }
642 n => filled += n,
643 }
644 }
645 Ok(true)
646}
647
648const MAX_RECORD_BYTES: u64 = 256 * 1024 * 1024;
655
656pub fn read_record(r: &mut dyn Read) -> io::Result<Option<RunRecord>> {
658 let mut len_bytes = [0u8; 8];
659 if !read_exact_or_eof(r, &mut len_bytes)? {
660 return Ok(None);
661 }
662 let len = u64::from_be_bytes(len_bytes);
663 if len > MAX_RECORD_BYTES {
670 return Err(io::Error::new(
671 io::ErrorKind::InvalidData,
672 format!("run-archive frame claims {len} bytes, over the {MAX_RECORD_BYTES} cap"),
673 ));
674 }
675 let mut payload = vec![0u8; len as usize];
676 if !read_exact_or_eof(r, &mut payload)? {
677 return Err(io::Error::new(
678 io::ErrorKind::UnexpectedEof,
679 "truncated run-archive frame",
680 ));
681 }
682 let record = serde_json::from_slice(&payload)
683 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
684 Ok(Some(record))
685}
686
687pub fn read_archive(r: &mut dyn Read) -> io::Result<(u16, Vec<RunRecord>)> {
689 let version = read_archive_start(r)?;
690 let mut records = Vec::new();
691 while let Some(record) = read_record(r)? {
692 records.push(record);
693 }
694 Ok((version, records))
695}
696
697pub fn read_archive_lenient(r: &mut dyn Read) -> io::Result<(u16, Vec<RunRecord>)> {
710 let version = read_archive_start(r)?;
711 let mut records = Vec::new();
712 while let Ok(Some(record)) = read_record(r) {
715 records.push(record);
716 }
717 Ok((version, records))
718}
719
720#[derive(Debug, Clone, PartialEq)]
727pub struct PendingToolBatch {
728 pub stage_index: usize,
730 pub iteration: usize,
732 pub response: String,
734 pub calls: Vec<ToolCallRecord>,
736}
737
738#[derive(Debug, Clone, PartialEq, Eq)]
743pub struct InferenceUsageRecord {
744 pub kind: InferenceKind,
746 pub stage: String,
748 pub iteration: usize,
750 pub provider: String,
752 pub model: String,
754 pub prompt_tokens: usize,
756 pub completion_tokens: usize,
758 pub cached_tokens: usize,
760 pub cache_write_tokens: usize,
762 pub at: i64,
764}
765
766#[derive(Debug, Clone, PartialEq)]
769pub struct FoldedRun {
770 pub identity: RunIdentity,
772 pub meta: RunMeta,
774 pub context: ContextSnapshot,
776 pub messages: Vec<MessageRecord>,
778 pub inference_count: usize,
780 pub inference_usage: Vec<InferenceUsageRecord>,
787 pub tool_call_count: usize,
789 pub pending_batch: Option<PendingToolBatch>,
793}
794
795pub fn context_contains_batch(context: &ContextSnapshot, batch: &PendingToolBatch) -> bool {
799 let Some(first_id) = batch.calls.first().map(|c| c.id.as_str()) else {
800 return false;
801 };
802 context.regions.iter().any(|region| {
803 region.entries.iter().any(|entry| {
804 matches!(
805 &entry.kind,
806 crate::region::EntryKind::AssistantTurn { tool_calls }
807 if tool_calls.iter().any(|tc| tc.id == first_id)
808 )
809 })
810 })
811}
812
813pub fn fold(records: &[RunRecord]) -> Option<FoldedRun> {
816 let mut iter = records.iter();
817 let (identity, meta) = match iter.next() {
818 Some(RunRecord::Header { identity, meta }) => (identity.clone(), (**meta).clone()),
819 _ => return None,
820 };
821 let mut folded = FoldedRun {
822 identity,
823 meta,
824 context: ContextSnapshot {
825 stage_name: String::new(),
826 total_tokens: 0,
827 max_tokens: 0,
828 regions: Vec::new(),
829 },
830 messages: Vec::new(),
831 inference_count: 0,
832 inference_usage: Vec::new(),
833 tool_call_count: 0,
834 pending_batch: None,
835 };
836 for record in iter {
837 match record {
838 RunRecord::Header { identity, meta } => {
839 folded.identity = identity.clone();
840 folded.meta = (**meta).clone();
841 }
842 RunRecord::OwnershipChanged {
843 machine_id,
844 world_id,
845 ..
846 } => {
847 folded.identity.machine_id = machine_id.clone();
848 folded.identity.world_id = world_id.clone();
849 }
850 RunRecord::Inference { .. } => folded.inference_count += 1,
851 RunRecord::InferenceUsage {
852 kind,
853 stage,
854 iteration,
855 provider,
856 model,
857 prompt_tokens,
858 completion_tokens,
859 cached_tokens,
860 cache_write_tokens,
861 at,
862 } => {
863 folded.inference_count += 1;
867 folded.inference_usage.push(InferenceUsageRecord {
868 kind: *kind,
869 stage: stage.clone(),
870 iteration: *iteration,
871 provider: provider.clone(),
872 model: model.clone(),
873 prompt_tokens: *prompt_tokens,
874 completion_tokens: *completion_tokens,
875 cached_tokens: *cached_tokens,
876 cache_write_tokens: *cache_write_tokens,
877 at: *at,
878 });
879 }
880 RunRecord::ToolBatch {
881 calls,
882 stage_index,
883 iteration,
884 response,
885 ..
886 } => {
887 folded.tool_call_count += calls.len();
888 folded.pending_batch = Some(PendingToolBatch {
891 stage_index: *stage_index,
892 iteration: *iteration,
893 response: response.clone(),
894 calls: calls.clone(),
895 });
896 }
897 RunRecord::ToolCallDone {
898 iteration,
899 call_id,
900 result,
901 ..
902 } => {
903 if let Some(batch) = folded
906 .pending_batch
907 .as_mut()
908 .filter(|b| b.iteration == *iteration)
909 && let Some(call) = batch.calls.iter_mut().find(|c| c.id == *call_id)
910 {
911 call.result = Some(result.clone());
912 }
913 }
914 RunRecord::ContextCheckpoint { snapshot, .. } => folded.context = snapshot.clone(),
915 RunRecord::ContextDiff { delta, .. } => apply_delta(&mut folded.context, delta),
916 RunRecord::Message { message, .. } => folded.messages.push(message.clone()),
917 RunRecord::StatusChanged { status, .. } => folded.meta.status = status.clone(),
918 RunRecord::Checkpoint { meta, context, .. } => {
919 folded.meta = (**meta).clone();
920 folded.context = context.clone();
921 }
922 RunRecord::Progress { meta, delta, .. } => {
923 folded.meta = (**meta).clone();
924 apply_delta(&mut folded.context, delta);
925 }
926 }
927 }
928 if let Some(batch) = &folded.pending_batch
933 && (folded.meta.iteration != batch.iteration
934 || context_contains_batch(&folded.context, batch))
935 {
936 folded.pending_batch = None;
937 }
938 Some(folded)
939}
940
941#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
944pub struct RunPoint {
945 pub meta: RunMeta,
947 pub context: ContextSnapshot,
949 pub at: i64,
951}
952
953#[derive(Debug)]
956pub struct PointRef<'a> {
957 pub index: usize,
961 pub at: i64,
963 pub meta: &'a RunMeta,
965 pub context: &'a ContextSnapshot,
967}
968
969pub fn visit_points(records: &[RunRecord], visit: &mut dyn FnMut(PointRef<'_>) -> ControlFlow<()>) {
988 let mut iter = records.iter();
989 let Some(mut folder) = (match iter.next() {
990 Some(first) => PointFolder::start(first),
991 None => None,
992 }) else {
993 return;
994 };
995 for record in iter {
996 if folder.push(record, visit).is_break() {
997 return;
998 }
999 }
1000}
1001
1002pub fn visit_archive_points(
1012 r: &mut dyn Read,
1013 visit: &mut dyn FnMut(PointRef<'_>) -> ControlFlow<()>,
1014) -> io::Result<()> {
1015 read_archive_start(r)?;
1016 let mut folder = match read_record(r) {
1017 Ok(Some(first)) => match PointFolder::start(&first) {
1018 Some(folder) => folder,
1019 None => return Ok(()),
1020 },
1021 _ => return Ok(()),
1022 };
1023 while let Ok(Some(record)) = read_record(r) {
1024 if folder.push(&record, visit).is_break() {
1025 return Ok(());
1026 }
1027 }
1028 Ok(())
1029}
1030
1031struct PointFolder {
1036 meta: RunMeta,
1037 context: ContextSnapshot,
1038 index: usize,
1039}
1040
1041impl PointFolder {
1042 fn start(first: &RunRecord) -> Option<Self> {
1046 match first {
1047 RunRecord::Header { meta, .. } => Some(Self {
1048 meta: (**meta).clone(),
1049 context: ContextSnapshot {
1050 stage_name: String::new(),
1051 total_tokens: 0,
1052 max_tokens: 0,
1053 regions: Vec::new(),
1054 },
1055 index: 0,
1056 }),
1057 _ => None,
1058 }
1059 }
1060
1061 fn push(
1063 &mut self,
1064 record: &RunRecord,
1065 visit: &mut dyn FnMut(PointRef<'_>) -> ControlFlow<()>,
1066 ) -> ControlFlow<()> {
1067 let at = match record {
1068 RunRecord::Header { meta: m, .. } => {
1069 self.meta = (**m).clone();
1070 return ControlFlow::Continue(());
1071 }
1072 RunRecord::StatusChanged { status, .. } => {
1073 self.meta.status = status.clone();
1074 return ControlFlow::Continue(());
1075 }
1076 RunRecord::ContextCheckpoint { snapshot, at } => {
1077 self.context = snapshot.clone();
1078 *at
1079 }
1080 RunRecord::ContextDiff { delta, at } => {
1081 apply_delta(&mut self.context, delta);
1082 *at
1083 }
1084 RunRecord::Checkpoint {
1085 meta: m,
1086 context: c,
1087 at,
1088 } => {
1089 self.meta = (**m).clone();
1090 self.context = c.clone();
1091 *at
1092 }
1093 RunRecord::Progress { meta: m, delta, at } => {
1094 self.meta = (**m).clone();
1095 apply_delta(&mut self.context, delta);
1096 *at
1097 }
1098 RunRecord::OwnershipChanged { .. }
1103 | RunRecord::Inference { .. }
1104 | RunRecord::InferenceUsage { .. }
1105 | RunRecord::ToolBatch { .. }
1106 | RunRecord::ToolCallDone { .. }
1107 | RunRecord::Message { .. } => return ControlFlow::Continue(()),
1108 };
1109 let flow = visit(PointRef {
1110 index: self.index,
1111 at,
1112 meta: &self.meta,
1113 context: &self.context,
1114 });
1115 self.index += 1;
1116 flow
1117 }
1118}
1119
1120pub fn replay_points(records: &[RunRecord]) -> Vec<RunPoint> {
1130 let mut points = Vec::new();
1131 visit_points(records, &mut |point| {
1132 points.push(RunPoint {
1133 meta: point.meta.clone(),
1134 context: point.context.clone(),
1135 at: point.at,
1136 });
1137 ControlFlow::Continue(())
1138 });
1139 points
1140}
1141
1142#[cfg(test)]
1143mod tests {
1144 use super::*;
1145 use crate::run_meta::RunStatus;
1146
1147 fn identity() -> RunIdentity {
1148 RunIdentity {
1149 run_id: "run-1".to_string(),
1150 machine_id: "machine-a".to_string(),
1151 world_id: "world-x".to_string(),
1152 created_at: 100,
1153 }
1154 }
1155
1156 const FIXTURE_NOW: i64 = 1_700_000_000;
1164
1165 fn meta() -> RunMeta {
1166 let mut meta = RunMeta::new(
1167 "run-1".to_string(),
1168 "coder".to_string(),
1169 "/agents/coder".to_string(),
1170 "do it".to_string(),
1171 Some("anthropic/claude".to_string()),
1172 "/work".to_string(),
1173 2,
1174 );
1175 meta.started_at = FIXTURE_NOW;
1176 meta.updated_at = FIXTURE_NOW;
1177 meta
1178 }
1179
1180 #[test]
1184 fn the_fixture_does_not_move_with_the_clock() {
1185 let first = meta();
1186 let mut later = meta();
1187 assert_eq!(first, later, "the fixture is rebuilt identically");
1191 later.started_at += 1;
1192 assert_ne!(
1193 first, later,
1194 "and the comparison is sensitive to the field that used to drift"
1195 );
1196 }
1197
1198 fn entry(content: &str, tokens: usize) -> RegionEntrySnapshot {
1199 RegionEntrySnapshot {
1200 content: content.to_string(),
1201 tokens,
1202 kind: crate::region::EntryKind::Text,
1203 metadata: None,
1204 key: None,
1205 taint: Default::default(),
1206 }
1207 }
1208
1209 fn region(name: &str, entries: Vec<RegionEntrySnapshot>) -> RegionSnapshot {
1210 let current = entries.iter().map(|e| e.tokens).sum();
1211 RegionSnapshot {
1212 name: name.to_string(),
1213 kind: "clearable".to_string(),
1214 current_tokens: current,
1215 max_tokens: 1000,
1216 entries,
1217 }
1218 }
1219
1220 fn snapshot(stage: &str, regions: Vec<RegionSnapshot>) -> ContextSnapshot {
1221 let total = regions.iter().map(|r| r.current_tokens).sum();
1222 ContextSnapshot {
1223 stage_name: stage.to_string(),
1224 total_tokens: total,
1225 max_tokens: 10_000,
1226 regions,
1227 }
1228 }
1229
1230 fn header() -> RunRecord {
1231 RunRecord::Header {
1232 identity: identity(),
1233 meta: Box::new(meta()),
1234 }
1235 }
1236
1237 fn region_delta_kind(d: &RegionDelta) -> &'static str {
1241 match d {
1242 RegionDelta::Set(_) => "set",
1243 RegionDelta::Append { .. } => "append",
1244 RegionDelta::Clear { .. } => "clear",
1245 RegionDelta::Remove { .. } => "remove",
1246 }
1247 }
1248
1249 fn assert_diff_roundtrip(a: &ContextSnapshot, b: &ContextSnapshot) {
1254 let delta = diff_context(a, b);
1255 let mut base = a.clone();
1256 apply_delta(&mut base, &delta);
1257 assert_eq!(&base, b);
1258 }
1259
1260 #[test]
1261 fn diff_append_only_growth_is_compact() {
1262 let a = snapshot("s1", vec![region("conv", vec![entry("hi", 1)])]);
1263 let b = snapshot(
1264 "s1",
1265 vec![region("conv", vec![entry("hi", 1), entry("there", 2)])],
1266 );
1267 let delta = diff_context(&a, &b);
1268 assert_eq!(region_delta_kind(&delta.regions[0]), "append");
1269 assert_diff_roundtrip(&a, &b);
1270 }
1271
1272 #[test]
1273 fn diff_new_region_is_set() {
1274 let a = snapshot("s1", vec![region("conv", vec![entry("hi", 1)])]);
1275 let b = snapshot(
1276 "s1",
1277 vec![
1278 region("conv", vec![entry("hi", 1)]),
1279 region("plan", vec![entry("p", 3)]),
1280 ],
1281 );
1282 let delta = diff_context(&a, &b);
1283 assert!(delta.regions.iter().any(|d| region_delta_kind(d) == "set"));
1284 assert_diff_roundtrip(&a, &b);
1285 }
1286
1287 #[test]
1288 fn diff_cleared_region() {
1289 let a = snapshot("s1", vec![region("conv", vec![entry("hi", 1)])]);
1290 let b = snapshot("s1", vec![region("conv", vec![])]);
1291 let delta = diff_context(&a, &b);
1292 assert_eq!(region_delta_kind(&delta.regions[0]), "clear");
1293 assert_diff_roundtrip(&a, &b);
1294 }
1295
1296 #[test]
1297 fn diff_removed_region() {
1298 let a = snapshot(
1299 "s1",
1300 vec![
1301 region("conv", vec![entry("hi", 1)]),
1302 region("plan", vec![entry("p", 3)]),
1303 ],
1304 );
1305 let b = snapshot("s1", vec![region("conv", vec![entry("hi", 1)])]);
1306 let delta = diff_context(&a, &b);
1307 assert!(
1308 delta
1309 .regions
1310 .iter()
1311 .any(|d| region_delta_kind(d) == "remove")
1312 );
1313 assert_diff_roundtrip(&a, &b);
1314 }
1315
1316 #[test]
1317 fn diff_non_prefix_rewrite_is_set() {
1318 let a = snapshot("s1", vec![region("conv", vec![entry("old", 1)])]);
1320 let b = snapshot("s1", vec![region("conv", vec![entry("new", 1)])]);
1321 let delta = diff_context(&a, &b);
1322 assert_eq!(region_delta_kind(&delta.regions[0]), "set");
1323 assert_diff_roundtrip(&a, &b);
1324 }
1325
1326 #[test]
1327 fn diff_kind_change_is_set_not_append() {
1328 let a = snapshot("s1", vec![region("conv", vec![entry("hi", 1)])]);
1330 let mut grown = region("conv", vec![entry("hi", 1), entry("more", 1)]);
1331 grown.kind = "sliding".to_string();
1332 let b = snapshot("s1", vec![grown]);
1333 let delta = diff_context(&a, &b);
1334 assert_eq!(region_delta_kind(&delta.regions[0]), "set");
1335 assert_diff_roundtrip(&a, &b);
1336 }
1337
1338 fn framed(records: &[RunRecord]) -> Vec<u8> {
1342 let mut buf = Vec::new();
1343 write_archive_start(&mut buf, RUN_ARCHIVE_VERSION).unwrap();
1344 for record in records {
1345 write_record(&mut buf, record).unwrap();
1346 }
1347 buf
1348 }
1349
1350 fn try_collect_streamed(bytes: &[u8]) -> io::Result<Vec<(usize, i64, usize)>> {
1354 let mut seen = Vec::new();
1355 visit_archive_points(&mut &bytes[..], &mut |p| {
1356 seen.push((p.index, p.at, p.context.total_tokens));
1357 ControlFlow::Continue(())
1358 })?;
1359 Ok(seen)
1360 }
1361
1362 fn collect_streamed(bytes: &[u8]) -> Vec<(usize, i64, usize)> {
1364 try_collect_streamed(bytes).unwrap()
1365 }
1366
1367 #[test]
1368 fn visit_archive_points_matches_visit_points() {
1369 let records = vec![
1370 header(),
1371 RunRecord::ContextCheckpoint {
1372 snapshot: snapshot("s1", vec![region("conv", vec![entry("hi", 1)])]),
1373 at: 10,
1374 },
1375 RunRecord::StatusChanged {
1376 status: RunStatus::Running,
1377 at: 11,
1378 },
1379 RunRecord::Progress {
1380 meta: Box::new(meta()),
1381 delta: diff_context(
1382 &snapshot("s1", vec![region("conv", vec![entry("hi", 1)])]),
1383 &snapshot(
1384 "s1",
1385 vec![region("conv", vec![entry("hi", 1), entry("more", 2)])],
1386 ),
1387 ),
1388 at: 12,
1389 },
1390 ];
1391 let mut in_memory = Vec::new();
1392 visit_points(&records, &mut |p| {
1393 in_memory.push((p.index, p.at, p.context.total_tokens));
1394 ControlFlow::Continue(())
1395 });
1396 assert_eq!(collect_streamed(&framed(&records)), in_memory);
1397 assert_eq!(in_memory.len(), 2, "checkpoint + progress = two points");
1398 }
1399
1400 #[test]
1401 fn visit_archive_points_rejects_a_bad_preamble() {
1402 assert!(try_collect_streamed(b"not an archive at all").is_err());
1403 }
1404
1405 #[test]
1406 fn visit_archive_points_is_lenient_about_a_torn_tail() {
1407 let records = vec![
1408 header(),
1409 RunRecord::ContextCheckpoint {
1410 snapshot: snapshot("s1", vec![region("conv", vec![entry("hi", 1)])]),
1411 at: 10,
1412 },
1413 ];
1414 let mut bytes = framed(&records);
1415 bytes.extend_from_slice(&1000u64.to_be_bytes());
1417 bytes.extend_from_slice(b"partial");
1418 assert_eq!(collect_streamed(&bytes).len(), 1, "points before the tear");
1419 }
1420
1421 #[test]
1422 fn visit_archive_points_visits_nothing_without_a_header() {
1423 let records = vec![RunRecord::ContextCheckpoint {
1424 snapshot: snapshot("s1", vec![region("conv", vec![entry("hi", 1)])]),
1425 at: 10,
1426 }];
1427 assert!(collect_streamed(&framed(&records)).is_empty());
1428 assert!(collect_streamed(&framed(&[])).is_empty());
1430 }
1431
1432 #[test]
1433 fn visit_archive_points_stops_on_break() {
1434 let records = vec![
1435 header(),
1436 RunRecord::ContextCheckpoint {
1437 snapshot: snapshot("s1", vec![region("conv", vec![entry("a", 1)])]),
1438 at: 10,
1439 },
1440 RunRecord::ContextCheckpoint {
1441 snapshot: snapshot("s1", vec![region("conv", vec![entry("b", 2)])]),
1442 at: 11,
1443 },
1444 ];
1445 let bytes = framed(&records);
1446 let mut seen = 0;
1447 visit_archive_points(&mut &bytes[..], &mut |_| {
1448 seen += 1;
1449 ControlFlow::Break(())
1450 })
1451 .unwrap();
1452 assert_eq!(seen, 1);
1453 }
1454
1455 fn assert_digest_matches_full_diff(a: &ContextSnapshot, b: &ContextSnapshot) {
1462 let via_digest = diff_context_digest(&digest_context(a), b);
1463 assert_eq!(via_digest, diff_context(a, b));
1464 let mut base = a.clone();
1466 apply_delta(&mut base, &via_digest);
1467 assert_eq!(&base, b);
1468 }
1469
1470 #[test]
1471 fn digest_diff_append_only_growth_is_compact() {
1472 let a = snapshot("s1", vec![region("conv", vec![entry("hi", 1)])]);
1473 let b = snapshot(
1474 "s1",
1475 vec![region("conv", vec![entry("hi", 1), entry("there", 2)])],
1476 );
1477 let delta = diff_context_digest(&digest_context(&a), &b);
1478 assert_eq!(region_delta_kind(&delta.regions[0]), "append");
1479 assert_digest_matches_full_diff(&a, &b);
1480 }
1481
1482 #[test]
1483 fn digest_diff_new_cleared_removed_and_rewritten_regions() {
1484 let a = snapshot(
1485 "s1",
1486 vec![
1487 region("conv", vec![entry("hi", 1)]),
1488 region("gone", vec![entry("bye", 1)]),
1489 region("wiped", vec![entry("w", 1)]),
1490 region("rewritten", vec![entry("old", 1)]),
1491 ],
1492 );
1493 let b = snapshot(
1494 "s1",
1495 vec![
1496 region("conv", vec![entry("hi", 1)]),
1497 region("wiped", vec![]),
1498 region("rewritten", vec![entry("new", 1)]),
1499 region("fresh", vec![entry("f", 2)]),
1500 ],
1501 );
1502 let delta = diff_context_digest(&digest_context(&a), &b);
1503 let kinds: Vec<_> = delta.regions.iter().map(region_delta_kind).collect();
1504 assert_eq!(kinds, vec!["clear", "set", "set", "remove"]);
1505 assert_digest_matches_full_diff(&a, &b);
1506 }
1507
1508 #[test]
1509 fn digest_diff_unchanged_region_emits_nothing() {
1510 let a = snapshot("s1", vec![region("conv", vec![entry("hi", 1)])]);
1511 let delta = diff_context_digest(&digest_context(&a), &a.clone());
1512 assert!(delta.regions.is_empty());
1513 assert_digest_matches_full_diff(&a, &a.clone());
1514 }
1515
1516 #[test]
1517 fn digest_diff_kind_change_is_set_not_append() {
1518 let a = snapshot("s1", vec![region("conv", vec![entry("hi", 1)])]);
1519 let mut grown = region("conv", vec![entry("hi", 1), entry("more", 1)]);
1520 grown.kind = "sliding".to_string();
1521 let b = snapshot("s1", vec![grown]);
1522 let delta = diff_context_digest(&digest_context(&a), &b);
1523 assert_eq!(region_delta_kind(&delta.regions[0]), "set");
1524 assert_digest_matches_full_diff(&a, &b);
1525 }
1526
1527 #[test]
1530 fn digest_diff_token_recount_is_an_empty_append() {
1531 let a = snapshot("s1", vec![region("conv", vec![entry("hi", 1)])]);
1532 let mut recounted = region("conv", vec![entry("hi", 1)]);
1533 recounted.current_tokens = 42;
1534 let b = snapshot("s1", vec![recounted]);
1535 let delta = diff_context_digest(&digest_context(&a), &b);
1536 assert_eq!(region_delta_kind(&delta.regions[0]), "append");
1537 assert_digest_matches_full_diff(&a, &b);
1538 }
1539
1540 #[test]
1543 fn entry_digest_covers_every_field() {
1544 let base = entry("text", 1);
1545 let variants = [
1546 entry("other", 1),
1547 entry("text", 2),
1548 RegionEntrySnapshot {
1549 key: Some("k".to_string()),
1550 ..entry("text", 1)
1551 },
1552 RegionEntrySnapshot {
1553 metadata: Some(serde_json::json!({"a": 1})),
1554 ..entry("text", 1)
1555 },
1556 RegionEntrySnapshot {
1557 kind: crate::region::EntryKind::ToolResult {
1558 tool_call_id: "c1".to_string(),
1559 tool_name: "shell".to_string(),
1560 is_error: false,
1561 },
1562 ..entry("text", 1)
1563 },
1564 ];
1565 let base_hash = entry_digest(&base);
1566 for variant in &variants {
1567 assert_ne!(
1568 entry_digest(variant),
1569 base_hash,
1570 "field change must change the digest: {variant:?}"
1571 );
1572 }
1573 assert_eq!(entry_digest(&base), entry_digest(&entry("text", 1)));
1575 }
1576
1577 #[test]
1578 fn diff_unchanged_region_emits_nothing() {
1579 let a = snapshot("s1", vec![region("conv", vec![entry("hi", 1)])]);
1580 let b = a.clone();
1581 let delta = diff_context(&a, &b);
1582 assert!(delta.regions.is_empty());
1583 assert_diff_roundtrip(&a, &b);
1584 }
1585
1586 #[test]
1587 fn apply_delta_skips_unknown_regions_leniently() {
1588 let mut base = snapshot("s1", vec![]);
1590 let delta = ContextDelta {
1591 stage_name: "s1".to_string(),
1592 total_tokens: 0,
1593 max_tokens: 10_000,
1594 regions: vec![
1595 RegionDelta::Append {
1596 name: "ghost".to_string(),
1597 entries: vec![entry("x", 1)],
1598 current_tokens: 1,
1599 },
1600 RegionDelta::Clear {
1601 name: "ghost".to_string(),
1602 },
1603 RegionDelta::Remove {
1604 name: "ghost".to_string(),
1605 },
1606 ],
1607 };
1608 apply_delta(&mut base, &delta);
1609 assert!(base.regions.is_empty());
1610 }
1611
1612 fn all_record_kinds() -> Vec<RunRecord> {
1615 vec![
1616 header(),
1617 RunRecord::OwnershipChanged {
1618 machine_id: "machine-b".to_string(),
1619 world_id: "world-y".to_string(),
1620 at: 101,
1621 },
1622 RunRecord::Inference {
1623 stage: "plan".to_string(),
1624 iteration: 0,
1625 request: InferenceRequestRecord {
1626 model: "m".to_string(),
1627 system: vec!["sys".to_string()],
1628 messages: vec![MessageRecord {
1629 role: "user".to_string(),
1630 content: "hi".to_string(),
1631 }],
1632 tool_names: vec!["read_file".to_string()],
1633 temperature: 0.7,
1634 max_tokens: 1024,
1635 },
1636 response: InferenceResponseRecord {
1637 content: "ok".to_string(),
1638 tool_calls: vec![],
1639 prompt_tokens: 10,
1640 completion_tokens: 5,
1641 cached_tokens: 0,
1642 cache_write_tokens: 0,
1643 },
1644 at: 102,
1645 },
1646 RunRecord::InferenceUsage {
1647 kind: InferenceKind::Compaction,
1648 stage: "plan".to_string(),
1649 iteration: 2,
1650 provider: "anthropic".to_string(),
1651 model: "claude-sonnet-5".to_string(),
1652 prompt_tokens: 7000,
1653 completion_tokens: 70,
1654 cached_tokens: 12,
1655 cache_write_tokens: 34,
1656 at: 102,
1657 },
1658 RunRecord::ToolBatch {
1659 calls: vec![ToolCallRecord {
1660 id: "c1".to_string(),
1661 name: "read_file".to_string(),
1662 arguments: "{}".to_string(),
1663 result: Some("body".to_string()),
1664 thought_signature: Some("sig".to_string()),
1665 }],
1666 at: 103,
1667 stage_index: 0,
1668 iteration: 0,
1669 response: "reading".to_string(),
1670 },
1671 RunRecord::ToolCallDone {
1672 iteration: 0,
1673 call_id: "c1".to_string(),
1674 result: "body".to_string(),
1675 at: 103,
1676 },
1677 RunRecord::ContextCheckpoint {
1678 snapshot: snapshot("plan", vec![region("conv", vec![entry("hi", 1)])]),
1679 at: 104,
1680 },
1681 RunRecord::ContextDiff {
1682 delta: ContextDelta {
1683 stage_name: "plan".to_string(),
1684 total_tokens: 3,
1685 max_tokens: 10_000,
1686 regions: vec![RegionDelta::Append {
1687 name: "conv".to_string(),
1688 entries: vec![entry("more", 2)],
1689 current_tokens: 3,
1690 }],
1691 },
1692 at: 105,
1693 },
1694 RunRecord::Message {
1695 message: MessageRecord {
1696 role: "user".to_string(),
1697 content: "another".to_string(),
1698 },
1699 at: 106,
1700 },
1701 RunRecord::StatusChanged {
1702 status: RunStatus::Complete,
1703 at: 107,
1704 },
1705 RunRecord::Checkpoint {
1706 meta: Box::new(meta()),
1707 context: snapshot("plan", vec![region("conv", vec![entry("hi", 1)])]),
1708 at: 108,
1709 },
1710 RunRecord::Progress {
1711 meta: Box::new(meta()),
1712 delta: ContextDelta {
1713 stage_name: "plan".to_string(),
1714 total_tokens: 3,
1715 max_tokens: 10_000,
1716 regions: vec![RegionDelta::Append {
1717 name: "conv".to_string(),
1718 entries: vec![entry("step", 2)],
1719 current_tokens: 3,
1720 }],
1721 },
1722 at: 109,
1723 },
1724 ]
1725 }
1726
1727 #[test]
1728 fn archive_write_then_read_roundtrips_every_record_kind() {
1729 let records = all_record_kinds();
1730 let mut buf = Vec::new();
1731 write_archive_start(&mut buf, RUN_ARCHIVE_VERSION).unwrap();
1732 for r in &records {
1733 write_record(&mut buf, r).unwrap();
1734 }
1735 let (version, read) = read_archive(&mut buf.as_slice()).unwrap();
1736 assert_eq!(version, RUN_ARCHIVE_VERSION);
1737 assert_eq!(read, records);
1738 }
1739
1740 #[test]
1741 fn read_archive_start_rejects_bad_magic() {
1742 let mut bytes: &[u8] = b"XXXX\x00\x01";
1743 let err = read_archive_start(&mut bytes).unwrap_err();
1744 assert_eq!(err.kind(), io::ErrorKind::InvalidData);
1745 }
1746
1747 #[test]
1748 fn read_archive_start_reports_version() {
1749 let mut buf = Vec::new();
1750 write_archive_start(&mut buf, 7).unwrap();
1751 assert_eq!(read_archive_start(&mut buf.as_slice()).unwrap(), 7);
1752 }
1753
1754 #[test]
1755 fn read_record_returns_none_at_clean_eof() {
1756 let empty: &[u8] = &[];
1757 assert!(read_record(&mut { empty }).unwrap().is_none());
1758 }
1759
1760 #[test]
1761 fn read_record_errors_on_truncated_length_prefix() {
1762 let mut bytes: &[u8] = &[0, 0];
1764 let err = read_record(&mut bytes).unwrap_err();
1765 assert_eq!(err.kind(), io::ErrorKind::UnexpectedEof);
1766 }
1767
1768 #[test]
1769 fn read_record_errors_on_truncated_payload() {
1770 let mut bytes: &[u8] = &[0, 0, 0, 0, 0, 0, 0, 10, 1, 2];
1772 let err = read_record(&mut bytes).unwrap_err();
1773 assert_eq!(err.kind(), io::ErrorKind::UnexpectedEof);
1774 }
1775
1776 #[test]
1777 fn read_record_errors_on_empty_payload_at_boundary() {
1778 let mut bytes: &[u8] = &[0, 0, 0, 0, 0, 0, 0, 10];
1781 let err = read_record(&mut bytes).unwrap_err();
1782 assert_eq!(err.kind(), io::ErrorKind::UnexpectedEof);
1783 }
1784
1785 #[test]
1786 fn read_record_errors_on_invalid_json_payload() {
1787 let mut buf = Vec::new();
1789 let bad = b"not json";
1790 buf.extend_from_slice(&(bad.len() as u64).to_be_bytes());
1791 buf.extend_from_slice(bad);
1792 let err = read_record(&mut buf.as_slice()).unwrap_err();
1793 assert_eq!(err.kind(), io::ErrorKind::InvalidData);
1794 }
1795
1796 struct FailingReader;
1799 impl Read for FailingReader {
1800 fn read(&mut self, _buf: &mut [u8]) -> io::Result<usize> {
1801 Err(io::Error::other("device error"))
1802 }
1803 }
1804
1805 #[test]
1806 fn read_record_propagates_reader_errors() {
1807 let err = read_record(&mut FailingReader).unwrap_err();
1808 assert_eq!(err.kind(), io::ErrorKind::Other);
1809 }
1810
1811 #[test]
1812 fn read_archive_propagates_a_bad_preamble() {
1813 let mut bytes: &[u8] = b"LV";
1815 assert!(read_archive(&mut bytes).is_err());
1816 }
1817
1818 #[test]
1819 fn read_archive_propagates_a_bad_frame() {
1820 let mut buf = Vec::new();
1822 write_archive_start(&mut buf, RUN_ARCHIVE_VERSION).unwrap();
1823 buf.extend_from_slice(&[0, 0, 0, 0, 0, 0, 0, 5, 1, 2]); let err = read_archive(&mut buf.as_slice()).unwrap_err();
1825 assert_eq!(err.kind(), io::ErrorKind::UnexpectedEof);
1826 }
1827
1828 struct FailAfter {
1830 remaining: usize,
1831 }
1832 impl Write for FailAfter {
1833 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
1834 if self.remaining == 0 {
1835 return Err(io::Error::other("disk full"));
1836 }
1837 let n = buf.len().min(self.remaining);
1838 self.remaining -= n;
1839 Ok(n)
1840 }
1841 fn flush(&mut self) -> io::Result<()> {
1842 Ok(())
1843 }
1844 }
1845
1846 #[test]
1847 fn fail_after_writer_flush_is_a_noop() {
1848 assert!(FailAfter { remaining: 1 }.flush().is_ok());
1849 }
1850
1851 #[test]
1852 fn write_archive_start_propagates_write_errors() {
1853 assert!(write_archive_start(&mut FailAfter { remaining: 0 }, 1).is_err());
1855 assert!(write_archive_start(&mut FailAfter { remaining: 4 }, 1).is_err());
1856 }
1857
1858 #[test]
1859 fn write_record_propagates_write_errors() {
1860 let rec = header();
1861 assert!(write_record(&mut FailAfter { remaining: 0 }, &rec).is_err());
1863 assert!(write_record(&mut FailAfter { remaining: 8 }, &rec).is_err());
1864 }
1865
1866 #[test]
1872 fn an_absurd_frame_length_is_an_error_not_an_allocation() {
1873 let mut buf = Vec::new();
1874 write_archive_start(&mut buf, RUN_ARCHIVE_VERSION).unwrap();
1875 write_record(&mut buf, &header()).unwrap();
1876 buf.extend_from_slice(&u64::MAX.to_be_bytes());
1878
1879 let err = read_archive(&mut buf.as_slice())
1880 .expect_err("the strict reader must refuse an impossible frame");
1881 assert_eq!(err.kind(), io::ErrorKind::InvalidData, "{err}");
1882
1883 let (_, records) = read_archive_lenient(&mut buf.as_slice()).unwrap();
1886 assert_eq!(records, vec![header()]);
1887 }
1888
1889 #[test]
1890 fn read_archive_lenient_matches_strict_on_a_clean_archive() {
1891 let records = all_record_kinds();
1894 let mut buf = Vec::new();
1895 write_archive_start(&mut buf, RUN_ARCHIVE_VERSION).unwrap();
1896 for r in &records {
1897 write_record(&mut buf, r).unwrap();
1898 }
1899 let (version, read) = read_archive_lenient(&mut buf.as_slice()).unwrap();
1900 assert_eq!(version, RUN_ARCHIVE_VERSION);
1901 assert_eq!(read, records);
1902 }
1903
1904 #[test]
1905 fn read_archive_lenient_keeps_valid_prefix_before_a_torn_tail() {
1906 let mut buf = Vec::new();
1910 write_archive_start(&mut buf, RUN_ARCHIVE_VERSION).unwrap();
1911 write_record(&mut buf, &header()).unwrap();
1912 write_record(
1913 &mut buf,
1914 &RunRecord::ContextCheckpoint {
1915 snapshot: snapshot("plan", vec![region("conv", vec![entry("hi", 1)])]),
1916 at: 1,
1917 },
1918 )
1919 .unwrap();
1920 buf.extend_from_slice(&[0, 0, 0, 0, 0, 0, 0, 10, 1, 2]);
1922
1923 assert!(read_archive(&mut buf.as_slice()).is_err());
1925 let (version, records) = read_archive_lenient(&mut buf.as_slice()).unwrap();
1927 assert_eq!(version, RUN_ARCHIVE_VERSION);
1928 assert_eq!(records.len(), 2);
1929 let folded = fold(&records).expect("prefix starts with a Header");
1930 assert_eq!(folded.context.regions[0].entries.len(), 1);
1931 }
1932
1933 #[test]
1934 fn read_archive_lenient_still_errors_on_a_bad_preamble() {
1935 let mut bad_magic: &[u8] = b"XXXX\x00\x01";
1938 assert!(read_archive_lenient(&mut bad_magic).is_err());
1939 let mut short: &[u8] = b"LVR1";
1941 assert!(read_archive_lenient(&mut short).is_err());
1942 }
1943
1944 #[test]
1945 fn read_archive_start_errors_on_truncated_version() {
1946 let mut bytes: &[u8] = b"LVR1";
1948 let err = read_archive_start(&mut bytes).unwrap_err();
1949 assert_eq!(err.kind(), io::ErrorKind::UnexpectedEof);
1950 }
1951
1952 #[test]
1955 fn fold_requires_a_header_first() {
1956 assert!(fold(&[]).is_none());
1957 assert!(
1958 fold(&[RunRecord::StatusChanged {
1959 status: RunStatus::Complete,
1960 at: 1
1961 }])
1962 .is_none()
1963 );
1964 }
1965
1966 #[test]
1967 fn fold_reconstructs_state_from_the_journal() {
1968 let records = all_record_kinds();
1969 let folded = fold(&records).expect("has header");
1970 assert_eq!(folded.identity.machine_id, "machine-b");
1972 assert_eq!(folded.identity.world_id, "world-y");
1973 assert_eq!(folded.inference_count, 2);
1976 assert_eq!(folded.inference_usage.len(), 1);
1977 assert_eq!(folded.tool_call_count, 1);
1978 assert_eq!(folded.messages.len(), 1);
1980 assert_eq!(folded.messages[0].content, "another");
1981 assert_eq!(folded.context.regions[0].name, "conv");
1984 assert_eq!(folded.context.regions[0].entries.len(), 2);
1985 assert_eq!(folded.context.total_tokens, 3);
1986 assert_eq!(folded.meta.run_id, "run-1");
1987 let pending = folded.pending_batch.expect("batch never applied");
1990 assert_eq!(pending.calls[0].result.as_deref(), Some("body"));
1991 }
1992
1993 #[test]
1994 fn fold_applies_context_diffs_over_a_checkpoint() {
1995 let records = vec![
1997 header(),
1998 RunRecord::ContextCheckpoint {
1999 snapshot: snapshot("plan", vec![region("conv", vec![entry("hi", 1)])]),
2000 at: 1,
2001 },
2002 RunRecord::ContextDiff {
2003 delta: ContextDelta {
2004 stage_name: "plan".to_string(),
2005 total_tokens: 3,
2006 max_tokens: 10_000,
2007 regions: vec![RegionDelta::Append {
2008 name: "conv".to_string(),
2009 entries: vec![entry("there", 2)],
2010 current_tokens: 3,
2011 }],
2012 },
2013 at: 2,
2014 },
2015 ];
2016 let folded = fold(&records).unwrap();
2017 assert_eq!(folded.context.regions[0].entries.len(), 2);
2018 assert_eq!(folded.context.total_tokens, 3);
2019 }
2020
2021 #[test]
2022 fn fold_later_header_updates_identity_and_meta() {
2023 let mut second_meta = meta();
2025 second_meta.status = RunStatus::Running;
2026 let records = vec![
2027 header(),
2028 RunRecord::Header {
2029 identity: RunIdentity {
2030 run_id: "run-1".to_string(),
2031 machine_id: "machine-c".to_string(),
2032 world_id: "world-z".to_string(),
2033 created_at: 200,
2034 },
2035 meta: Box::new(second_meta),
2036 },
2037 ];
2038 let folded = fold(&records).unwrap();
2039 assert_eq!(folded.identity.machine_id, "machine-c");
2040 assert_eq!(folded.meta.status, RunStatus::Running);
2041 }
2042
2043 #[test]
2044 fn fold_progress_applies_meta_and_context_diff() {
2045 let mut advanced = meta();
2046 advanced.status = RunStatus::Running;
2047 advanced.iteration = 5;
2048 let records = vec![
2049 header(),
2050 RunRecord::ContextCheckpoint {
2051 snapshot: snapshot("plan", vec![region("conv", vec![entry("hi", 1)])]),
2052 at: 1,
2053 },
2054 RunRecord::Progress {
2055 meta: Box::new(advanced),
2056 delta: ContextDelta {
2057 stage_name: "plan".to_string(),
2058 total_tokens: 3,
2059 max_tokens: 10_000,
2060 regions: vec![RegionDelta::Append {
2061 name: "conv".to_string(),
2062 entries: vec![entry("there", 2)],
2063 current_tokens: 3,
2064 }],
2065 },
2066 at: 2,
2067 },
2068 ];
2069 let folded = fold(&records).unwrap();
2070 assert_eq!(folded.meta.iteration, 5);
2071 assert_eq!(folded.meta.status, RunStatus::Running);
2072 assert_eq!(folded.context.regions[0].entries.len(), 2);
2073 }
2074
2075 #[test]
2079 fn fold_carries_a_submitted_final_output_through_progress() {
2080 let mut answered = meta();
2081 answered.final_output = Some(
2082 crate::output::FinalOutput::new(
2083 "renamed two helpers",
2084 Some("markdown".to_string()),
2085 "summary".to_string(),
2086 9,
2087 )
2088 .descriptor(),
2089 );
2090 answered.output_request = Some(crate::output::OutputSpec {
2091 format: Some("a2ui".to_string()),
2092 ..Default::default()
2093 });
2094 let records = vec![
2095 header(),
2096 RunRecord::Progress {
2097 meta: Box::new(answered),
2098 delta: ContextDelta {
2099 stage_name: "summary".to_string(),
2100 total_tokens: 0,
2101 max_tokens: 10_000,
2102 regions: vec![],
2103 },
2104 at: 2,
2105 },
2106 ];
2107 let folded = fold(&records).unwrap();
2108 let output = folded.meta.final_output.expect("the answer folded through");
2109 assert_eq!(output.bytes, "renamed two helpers".len());
2112 assert_eq!(output.stage, "summary");
2113 assert_eq!(
2114 folded.meta.output_request.and_then(|s| s.format).as_deref(),
2115 Some("a2ui")
2116 );
2117 }
2118
2119 fn call(id: &str, result: Option<&str>) -> ToolCallRecord {
2122 ToolCallRecord {
2123 id: id.to_string(),
2124 name: "shell".to_string(),
2125 arguments: "{}".to_string(),
2126 result: result.map(str::to_string),
2127 thought_signature: None,
2128 }
2129 }
2130
2131 fn batch(iteration: usize, calls: Vec<ToolCallRecord>) -> RunRecord {
2132 RunRecord::ToolBatch {
2133 calls,
2134 at: 10,
2135 stage_index: 0,
2136 iteration,
2137 response: "running tools".to_string(),
2138 }
2139 }
2140
2141 fn turn_entry(call_ids: &[&str]) -> RegionEntrySnapshot {
2143 let mut e = entry("turn", 1);
2144 e.kind = crate::region::EntryKind::AssistantTurn {
2145 tool_calls: call_ids
2146 .iter()
2147 .map(|id| crate::region::SerializedToolCall {
2148 id: id.to_string(),
2149 name: "shell".to_string(),
2150 arguments: serde_json::Value::Null,
2151 thought_signature: None,
2152 })
2153 .collect(),
2154 };
2155 e
2156 }
2157
2158 #[test]
2159 fn fold_surfaces_a_pending_batch_with_merged_results() {
2160 let records = vec![
2165 header(),
2166 batch(
2167 0,
2168 vec![
2169 call("c1", None),
2170 call("c2", Some("inline")),
2171 call("c3", None),
2172 ],
2173 ),
2174 RunRecord::ToolCallDone {
2175 iteration: 0,
2176 call_id: "c1".to_string(),
2177 result: "ran".to_string(),
2178 at: 11,
2179 },
2180 ];
2181 let folded = fold(&records).unwrap();
2182 let pending = folded.pending_batch.expect("batch is pending");
2183 assert_eq!(pending.iteration, 0);
2184 assert_eq!(pending.response, "running tools");
2185 assert_eq!(pending.calls[0].result.as_deref(), Some("ran"));
2186 assert_eq!(pending.calls[1].result.as_deref(), Some("inline"));
2187 assert_eq!(pending.calls[2].result, None);
2188 assert_eq!(folded.tool_call_count, 3);
2189 }
2190
2191 #[test]
2192 fn fold_keeps_only_the_latest_batch_and_ignores_stale_done_records() {
2193 let mut advanced = meta();
2196 advanced.iteration = 1;
2197 let records = vec![
2198 header(),
2199 batch(0, vec![call("c1", None)]),
2200 RunRecord::Progress {
2201 meta: Box::new(advanced),
2202 delta: ContextDelta {
2203 stage_name: "plan".to_string(),
2204 total_tokens: 0,
2205 max_tokens: 10_000,
2206 regions: vec![],
2207 },
2208 at: 11,
2209 },
2210 batch(1, vec![call("c2", None)]),
2211 RunRecord::ToolCallDone {
2212 iteration: 0,
2213 call_id: "c1".to_string(),
2214 result: "stale".to_string(),
2215 at: 12,
2216 },
2217 RunRecord::ToolCallDone {
2218 iteration: 1,
2219 call_id: "unknown".to_string(),
2220 result: "nowhere to land".to_string(),
2221 at: 13,
2222 },
2223 ];
2224 let folded = fold(&records).unwrap();
2225 let pending = folded.pending_batch.expect("latest batch is pending");
2226 assert_eq!(pending.iteration, 1);
2227 assert_eq!(pending.calls.len(), 1);
2228 assert_eq!(pending.calls[0].id, "c2");
2229 assert_eq!(pending.calls[0].result, None, "stale/unknown dones ignored");
2230 }
2231
2232 #[test]
2233 fn fold_clears_a_batch_once_the_iteration_moves_on() {
2234 let mut advanced = meta();
2237 advanced.iteration = 1;
2238 let records = vec![
2239 header(),
2240 batch(0, vec![call("c1", Some("done"))]),
2241 RunRecord::Progress {
2242 meta: Box::new(advanced),
2243 delta: ContextDelta {
2244 stage_name: "plan".to_string(),
2245 total_tokens: 0,
2246 max_tokens: 10_000,
2247 regions: vec![],
2248 },
2249 at: 11,
2250 },
2251 ];
2252 assert_eq!(fold(&records).unwrap().pending_batch, None);
2253 }
2254
2255 #[test]
2256 fn fold_clears_a_batch_whose_turn_already_landed_in_the_window() {
2257 let records = vec![
2260 header(),
2261 batch(0, vec![call("c1", Some("done"))]),
2262 RunRecord::ContextCheckpoint {
2263 snapshot: snapshot("plan", vec![region("conv", vec![turn_entry(&["c1"])])]),
2264 at: 11,
2265 },
2266 ];
2267 assert_eq!(fold(&records).unwrap().pending_batch, None);
2268 }
2269
2270 #[test]
2271 fn context_contains_batch_matches_only_the_batch_turn() {
2272 let pending = PendingToolBatch {
2273 stage_index: 0,
2274 iteration: 0,
2275 response: String::new(),
2276 calls: vec![call("c1", None)],
2277 };
2278 let other = snapshot("plan", vec![region("conv", vec![turn_entry(&["zz"])])]);
2280 assert!(!context_contains_batch(&other, &pending));
2281 let own = snapshot(
2283 "plan",
2284 vec![region("conv", vec![turn_entry(&["c1", "c2"])])],
2285 );
2286 assert!(context_contains_batch(&own, &pending));
2287 let empty = PendingToolBatch {
2289 calls: vec![],
2290 ..pending
2291 };
2292 assert!(!context_contains_batch(&own, &empty));
2293 }
2294
2295 #[test]
2296 fn old_shape_tool_batch_json_still_parses() {
2297 let json = br#"{"ToolBatch":{"calls":[{"id":"c1","name":"shell","arguments":"{}","result":"ok"}],"at":9}}"#;
2301 let mut buf = Vec::new();
2302 buf.extend_from_slice(&(json.len() as u64).to_be_bytes());
2303 buf.extend_from_slice(json);
2304 let record = read_record(&mut buf.as_slice()).unwrap().unwrap();
2305 assert_eq!(
2306 record,
2307 RunRecord::ToolBatch {
2308 calls: vec![call("c1", Some("ok"))],
2309 at: 9,
2310 stage_index: 0,
2311 iteration: 0,
2312 response: String::new(),
2313 }
2314 );
2315 }
2316
2317 fn three_point_records() -> Vec<RunRecord> {
2321 let mut running = meta();
2322 running.status = RunStatus::Running;
2323 vec![
2324 header(),
2325 RunRecord::ContextCheckpoint {
2326 snapshot: snapshot("plan", vec![region("conv", vec![entry("first", 1)])]),
2327 at: 10,
2328 },
2329 RunRecord::ContextDiff {
2330 delta: ContextDelta {
2331 stage_name: "plan".to_string(),
2332 total_tokens: 2,
2333 max_tokens: 10_000,
2334 regions: vec![RegionDelta::Append {
2335 name: "conv".to_string(),
2336 entries: vec![entry("second", 1)],
2337 current_tokens: 2,
2338 }],
2339 },
2340 at: 20,
2341 },
2342 RunRecord::Progress {
2343 meta: Box::new(running),
2344 delta: ContextDelta {
2345 stage_name: "code".to_string(),
2346 total_tokens: 3,
2347 max_tokens: 10_000,
2348 regions: vec![RegionDelta::Append {
2349 name: "conv".to_string(),
2350 entries: vec![entry("third", 1)],
2351 current_tokens: 3,
2352 }],
2353 },
2354 at: 30,
2355 },
2356 ]
2357 }
2358
2359 #[test]
2360 fn visit_points_indexes_points_in_order_and_carries_the_running_window() {
2361 let records = three_point_records();
2362 let mut seen: Vec<(usize, i64, usize)> = Vec::new();
2363 visit_points(&records, &mut |point| {
2364 seen.push((
2365 point.index,
2366 point.at,
2367 point.context.regions[0].entries.len(),
2368 ));
2369 ControlFlow::Continue(())
2370 });
2371 assert_eq!(seen, vec![(0, 10, 1), (1, 20, 2), (2, 30, 3)]);
2373 }
2374
2375 #[test]
2378 fn visit_points_stops_at_the_first_break() {
2379 let records = three_point_records();
2380 let mut visits = 0;
2381 visit_points(&records, &mut |point| {
2382 visits += 1;
2383 if point.index == 1 {
2384 ControlFlow::Break(())
2385 } else {
2386 ControlFlow::Continue(())
2387 }
2388 });
2389 assert_eq!(
2390 visits, 2,
2391 "stopped at the breaking point, did not run the third"
2392 );
2393 }
2394
2395 #[test]
2396 fn visit_points_without_a_header_visits_nothing() {
2397 let mut visits = 0;
2398 {
2399 let mut count = |_: PointRef<'_>| {
2400 visits += 1;
2401 ControlFlow::Continue(())
2402 };
2403
2404 visit_points(&three_point_records(), &mut count);
2408 visit_points(&[], &mut count);
2411 visit_points(
2412 &[RunRecord::ContextCheckpoint {
2413 snapshot: snapshot("plan", vec![]),
2414 at: 1,
2415 }],
2416 &mut count,
2417 );
2418 }
2419 assert_eq!(visits, 3, "only the well-formed journal produced points");
2420 }
2421
2422 #[test]
2426 fn visit_points_and_replay_points_agree() {
2427 for records in [
2428 three_point_records(),
2429 vec![header()],
2430 vec![],
2431 vec![RunRecord::Message {
2432 message: MessageRecord {
2433 role: "user".to_string(),
2434 content: "x".to_string(),
2435 },
2436 at: 1,
2437 }],
2438 ] {
2439 let collected: Vec<RunPoint> = {
2440 let mut out = Vec::new();
2441 visit_points(&records, &mut |point| {
2442 out.push(RunPoint {
2443 meta: point.meta.clone(),
2444 context: point.context.clone(),
2445 at: point.at,
2446 });
2447 ControlFlow::Continue(())
2448 });
2449 out
2450 };
2451 assert_eq!(collected, replay_points(&records));
2452 }
2453 }
2454
2455 #[test]
2456 fn replay_points_requires_a_header() {
2457 assert!(replay_points(&[]).is_empty());
2458 assert!(
2459 replay_points(&[RunRecord::Message {
2460 message: MessageRecord {
2461 role: "user".to_string(),
2462 content: "x".to_string(),
2463 },
2464 at: 1,
2465 }])
2466 .is_empty()
2467 );
2468 }
2469
2470 #[test]
2471 fn replay_points_emits_a_snapshot_per_context_change() {
2472 let mut running = meta();
2475 running.status = RunStatus::Running;
2476 let records = vec![
2477 header(),
2478 RunRecord::Inference {
2479 stage: "plan".to_string(),
2480 iteration: 0,
2481 request: InferenceRequestRecord {
2482 model: "m".to_string(),
2483 system: vec![],
2484 messages: vec![],
2485 tool_names: vec![],
2486 temperature: 0.7,
2487 max_tokens: 10,
2488 },
2489 response: InferenceResponseRecord {
2490 content: "ok".to_string(),
2491 tool_calls: vec![],
2492 prompt_tokens: 1,
2493 completion_tokens: 1,
2494 cached_tokens: 0,
2495 cache_write_tokens: 0,
2496 },
2497 at: 1,
2498 },
2499 batch(0, vec![call("c1", None)]),
2500 RunRecord::ToolCallDone {
2501 iteration: 0,
2502 call_id: "c1".to_string(),
2503 result: "ran".to_string(),
2504 at: 1,
2505 },
2506 RunRecord::ContextCheckpoint {
2507 snapshot: snapshot("plan", vec![region("conv", vec![entry("hi", 1)])]),
2508 at: 2,
2509 },
2510 RunRecord::StatusChanged {
2511 status: RunStatus::Running,
2512 at: 3,
2513 },
2514 RunRecord::Progress {
2515 meta: Box::new(running),
2516 delta: ContextDelta {
2517 stage_name: "implement".to_string(),
2518 total_tokens: 3,
2519 max_tokens: 10_000,
2520 regions: vec![RegionDelta::Append {
2521 name: "conv".to_string(),
2522 entries: vec![entry("more", 2)],
2523 current_tokens: 3,
2524 }],
2525 },
2526 at: 4,
2527 },
2528 ];
2529 let points = replay_points(&records);
2530 assert_eq!(points.len(), 2, "one point per context change");
2531 assert_eq!(points[0].at, 2);
2533 assert_eq!(points[0].context.regions[0].entries.len(), 1);
2534 assert_eq!(points[1].at, 4);
2537 assert_eq!(points[1].context.regions[0].entries.len(), 2);
2538 assert_eq!(points[1].context.stage_name, "implement");
2539 assert_eq!(points[1].meta.status, RunStatus::Running);
2540 }
2541
2542 #[test]
2543 fn replay_points_handles_context_diff_and_a_later_header() {
2544 let mut relabeled = meta();
2547 relabeled.agent_name = "renamed".to_string();
2548 let records = vec![
2549 header(),
2550 RunRecord::ContextCheckpoint {
2551 snapshot: snapshot("plan", vec![region("conv", vec![entry("hi", 1)])]),
2552 at: 1,
2553 },
2554 RunRecord::Header {
2555 identity: identity(),
2556 meta: Box::new(relabeled),
2557 },
2558 RunRecord::ContextDiff {
2559 delta: ContextDelta {
2560 stage_name: "plan".to_string(),
2561 total_tokens: 3,
2562 max_tokens: 10_000,
2563 regions: vec![RegionDelta::Append {
2564 name: "conv".to_string(),
2565 entries: vec![entry("more", 2)],
2566 current_tokens: 3,
2567 }],
2568 },
2569 at: 2,
2570 },
2571 ];
2572 let points = replay_points(&records);
2573 assert_eq!(points.len(), 2); assert_eq!(points[1].context.regions[0].entries.len(), 2);
2575 assert_eq!(points[1].meta.agent_name, "renamed");
2577 }
2578
2579 #[test]
2580 fn replay_points_over_a_full_checkpoint() {
2581 let records = vec![
2583 header(),
2584 RunRecord::Checkpoint {
2585 meta: Box::new(meta()),
2586 context: snapshot("review", vec![region("conv", vec![entry("x", 4)])]),
2587 at: 9,
2588 },
2589 ];
2590 let points = replay_points(&records);
2591 assert_eq!(points.len(), 1);
2592 assert_eq!(points[0].context.stage_name, "review");
2593 assert_eq!(points[0].context.regions[0].entries[0].tokens, 4);
2594 }
2595
2596 #[test]
2600 fn every_inference_kind_has_a_distinct_label_and_serialized_name() {
2601 let all = [
2602 (InferenceKind::Stage, "stage"),
2603 (InferenceKind::Compaction, "compaction"),
2604 (InferenceKind::Title, "title"),
2605 (InferenceKind::Routing, "routing"),
2606 ];
2607 for (kind, label) in all {
2608 assert_eq!(kind.label(), label);
2609 assert_eq!(serde_json::to_value(kind).unwrap(), label);
2610 }
2611 let labels: std::collections::HashSet<_> = all.iter().map(|(k, _)| k.label()).collect();
2612 assert_eq!(labels.len(), all.len(), "labels must not collide");
2613 }
2614
2615 #[test]
2619 fn only_a_stage_turn_counts_as_stage_work() {
2620 assert!(InferenceKind::Stage.is_stage_work());
2621 for kind in [
2622 InferenceKind::Compaction,
2623 InferenceKind::Title,
2624 InferenceKind::Routing,
2625 ] {
2626 assert!(
2627 !kind.is_stage_work(),
2628 "{kind:?} is machinery, not stage work"
2629 );
2630 }
2631 }
2632
2633 #[test]
2637 fn a_usage_record_without_a_kind_reads_back_as_stage_work() {
2638 let json = serde_json::json!({
2639 "InferenceUsage": {
2640 "stage": "plan",
2641 "iteration": 1,
2642 "provider": "anthropic",
2643 "model": "claude-sonnet-5",
2644 "prompt_tokens": 10,
2645 "completion_tokens": 2,
2646 "cached_tokens": 0,
2647 "cache_write_tokens": 0,
2648 "at": 5,
2649 }
2650 });
2651 let record: RunRecord = serde_json::from_value(json).unwrap();
2656 assert_eq!(
2657 record,
2658 RunRecord::InferenceUsage {
2659 kind: InferenceKind::Stage,
2660 stage: "plan".to_string(),
2661 iteration: 1,
2662 provider: "anthropic".to_string(),
2663 model: "claude-sonnet-5".to_string(),
2664 prompt_tokens: 10,
2665 completion_tokens: 2,
2666 cached_tokens: 0,
2667 cache_write_tokens: 0,
2668 at: 5,
2669 }
2670 );
2671 }
2672
2673 #[test]
2678 fn folding_keeps_each_call_separate_instead_of_summing_them() {
2679 let usage = |kind, prompt, at| RunRecord::InferenceUsage {
2680 kind,
2681 stage: "plan".to_string(),
2682 iteration: 1,
2683 provider: "anthropic".to_string(),
2684 model: "claude-sonnet-5".to_string(),
2685 prompt_tokens: prompt,
2686 completion_tokens: 1,
2687 cached_tokens: 0,
2688 cache_write_tokens: 0,
2689 at,
2690 };
2691 let folded = fold(&[
2694 header(),
2695 usage(InferenceKind::Compaction, 7000, 1),
2696 usage(InferenceKind::Stage, 21_000, 2),
2697 ])
2698 .unwrap();
2699
2700 assert_eq!(folded.inference_count, 2);
2701 let seen: Vec<_> = folded
2702 .inference_usage
2703 .iter()
2704 .map(|u| (u.kind, u.prompt_tokens))
2705 .collect();
2706 assert_eq!(
2707 seen,
2708 vec![
2709 (InferenceKind::Compaction, 7000),
2710 (InferenceKind::Stage, 21_000)
2711 ]
2712 );
2713 assert!(
2717 folded
2718 .inference_usage
2719 .iter()
2720 .all(|u| u.prompt_tokens < 32_000),
2721 "no single call exceeded the window, and the journal can now prove it"
2722 );
2723 }
2724
2725 #[test]
2728 fn both_inference_record_kinds_count_as_one_call_each() {
2729 let records = all_record_kinds();
2730 let folded = fold(&records).unwrap();
2731 let written = records
2732 .iter()
2733 .filter(|r| {
2734 matches!(
2735 r,
2736 RunRecord::Inference { .. } | RunRecord::InferenceUsage { .. }
2737 )
2738 })
2739 .count();
2740 assert_eq!(folded.inference_count, written);
2741 assert_eq!(folded.inference_usage.len(), 1);
2742 }
2743}