vtcode_memory/event_log/
manifest.rs1use std::collections::VecDeque;
2
3use serde::{Deserialize, Serialize};
4
5use super::now_rfc3339;
6
7#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
9pub struct SessionManifest {
10 pub session_id: String,
12 schema_version: u32,
14 pub created_at: String,
16 pub updated_at: String,
18 pub turn_count: u64,
20 pub event_count: u64,
22 pub status: String,
24 #[serde(default)]
30 pub(super) in_turn: Option<bool>,
31 #[serde(default)]
37 pub(super) persisted_file_len: Option<u64>,
38 #[serde(default)]
44 pub(super) retained_turn_base: Option<u64>,
45}
46
47impl SessionManifest {
48 #[must_use]
50 pub fn evicted_turn_count(&self) -> u64 {
51 self.retained_turn_base.unwrap_or(1).saturating_sub(1)
52 }
53 #[must_use]
55 pub(crate) fn new(session_id: &str) -> Self {
56 let ts = now_rfc3339();
57 Self {
58 session_id: session_id.to_string(),
59 schema_version: crate::SESSION_STORE_SCHEMA_VERSION,
60 created_at: ts.clone(),
61 updated_at: ts,
62 turn_count: 0,
63 event_count: 0,
64 status: "active".to_string(),
65 in_turn: Some(false),
66 persisted_file_len: Some(0),
67 retained_turn_base: Some(1),
68 }
69 }
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
74pub struct TurnIndexEntry {
75 pub(super) turn_number: u64,
77 pub(super) start_offset: u64,
79 pub(super) end_offset: u64,
81 pub(super) event_count: u64,
83 pub(super) ts: String,
85}
86
87#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
89pub struct TurnIndex {
90 pub(super) entries: VecDeque<TurnIndexEntry>,
92}
93
94impl TurnIndex {
95 #[must_use]
97 pub fn len(&self) -> usize {
98 self.entries.len()
99 }
100
101 #[must_use]
103 pub fn is_empty(&self) -> bool {
104 self.entries.is_empty()
105 }
106
107 pub(super) fn is_valid_for_file(&self, file_len: u64) -> bool {
108 let mut previous_end = 0u64;
109 self.entries.iter().all(|entry| {
110 let valid = entry.event_count > 0
111 && entry.start_offset >= previous_end
112 && entry.start_offset <= entry.end_offset
113 && entry.end_offset <= file_len;
114 if valid {
115 previous_end = entry.end_offset;
116 }
117 valid
118 })
119 }
120
121 pub(super) fn is_consistent_with_manifest(&self, manifest: &SessionManifest) -> bool {
122 let expected_last_turn = if manifest.in_turn == Some(true) {
123 manifest.turn_count.saturating_add(1)
124 } else {
125 manifest.turn_count
126 };
127 let entries_are_contiguous = self
128 .entries
129 .iter()
130 .map(|entry| entry.turn_number)
131 .try_fold(None::<u64>, |previous, turn_number| {
132 if previous.is_some_and(|previous| turn_number != previous.saturating_add(1)) {
133 return Err(());
134 }
135 Ok(Some(turn_number))
136 })
137 .is_ok();
138 if !entries_are_contiguous {
139 return false;
140 }
141
142 let expected_first_turn = manifest.retained_turn_base.unwrap_or(1).max(1);
143 match (self.entries.front(), self.entries.back()) {
144 (Some(first), Some(last)) => {
145 first.turn_number == expected_first_turn && last.turn_number == expected_last_turn
146 }
147 (None, None) => {
148 expected_last_turn == 0
149 || manifest
150 .retained_turn_base
151 .is_some_and(|retained_turn_base| retained_turn_base > manifest.turn_count)
152 }
153 _ => false,
154 }
155 }
156}