vtcode_memory/event_log/
append.rs1use super::*;
7
8impl SessionEventLog {
9 pub fn append(&self, event: &ThreadEvent) -> Result<(), SessionStoreError> {
11 let _eviction_guard = self.shared.eviction_lock.lock().map_err(poison)?;
12 let mut st = self.shared.state.lock().map_err(poison)?;
13
14 if st.write_failed {
15 return Err(self.event_write_failed());
16 }
17
18 if st.needs_separator {
19 st.write_buf.push(b'\n');
20 st.next_offset += 1;
21 st.needs_separator = false;
22 }
23
24 let (start, end) = st.serialize_event(event)?;
28
29 st.manifest.event_count += 1;
30 st.manifest.updated_at = now_rfc3339();
31
32 let is_turn_boundary = st.apply_lifecycle_event(LifecycleKind::from_event(event), start, end);
36 if is_turn_boundary {
37 self.persist_meta_locked(&mut st)?;
38 }
39
40 if st.write_buf.len() >= MAX_WRITE_BUFFER_BYTES {
41 self.persist_meta_locked(&mut st)?;
45 }
46 drop(st);
47 self.enforce_event_cap()
48 }
49
50 fn enforce_event_cap(&self) -> Result<(), SessionStoreError> {
54 let mut st = self.shared.state.lock().map_err(poison)?;
55
56 let Some(plan) = st.plan_cap_eviction(self.max_events) else {
59 return Ok(());
60 };
61
62 self.flush_write_buf_locked(&mut st)?;
66
67 let (evicted, mut remaining, previous_file_len) = {
68 let mut file_slot = self.shared.file.lock().map_err(poison)?;
69 let file = file_slot.as_mut().ok_or_else(|| self.event_file_unavailable())?;
70 let file_len = file
71 .metadata()
72 .map_err(|error| SessionStoreError::io(&self.events_path, error))?
73 .len();
74 if plan.truncate_offset > file_len {
75 return Err(SessionStoreError::io(
76 &self.events_path,
77 std::io::Error::new(std::io::ErrorKind::InvalidData, "cap offset exceeds event log length"),
78 ));
79 }
80 file.seek(SeekFrom::Start(0))
81 .map_err(|e| SessionStoreError::io(&self.events_path, e))?;
82 let mut evicted = vec![
83 0u8;
84 usize::try_from(plan.truncate_offset).map_err(|error| {
85 SessionStoreError::io(
86 &self.events_path,
87 std::io::Error::new(std::io::ErrorKind::InvalidData, error),
88 )
89 })?
90 ];
91 file.read_exact(&mut evicted)
92 .map_err(|e| SessionStoreError::io(&self.events_path, e))?;
93 file.seek(SeekFrom::Start(plan.truncate_offset))
94 .map_err(|e| SessionStoreError::io(&self.events_path, e))?;
95 let mut remaining = Vec::new();
96 file.read_to_end(&mut remaining)
97 .map_err(|e| SessionStoreError::io(&self.events_path, e))?;
98 (evicted, remaining, file_len)
99 };
100 let retained_turn_base = st.retained_turn_base_after_eviction(plan.evicted_turn_count);
101 drop(st);
102
103 let mut plan = plan;
109 plan.evicted_event_count = count_persisted_event_records(&evicted);
110 let evicted_events = decode_events(&evicted);
111 (self.eviction_summary_hook)(&evicted_events)?;
112
113 let mut matrix_checkpoints = BTreeMap::new();
116 let mut planning_checkpoint = None;
117 for event in &evicted_events {
118 if let ThreadEvent::PlanDelta(delta) = event
119 && delta.recovery.is_some()
120 {
121 planning_checkpoint = Some(event);
122 }
123 if let ThreadEvent::MatrixUpdated(snapshot) = event {
124 matrix_checkpoints.insert(snapshot.spec.id.clone(), event);
125 }
126 }
127 for event in decode_events(&remaining) {
128 if let ThreadEvent::PlanDelta(delta) = &event
129 && delta.recovery.is_some()
130 {
131 planning_checkpoint = None;
132 }
133 if let ThreadEvent::MatrixUpdated(snapshot) = event {
134 matrix_checkpoints.remove(&snapshot.spec.id);
135 }
136 }
137 let mut checkpoint_prefix = Vec::new();
138 for event in matrix_checkpoints.values().copied().chain(planning_checkpoint) {
139 serde_json::to_writer(&mut checkpoint_prefix, &VersionedThreadEvent::new((*event).clone()))
140 .map_err(|error| SessionStoreError::io(&self.events_path, std::io::Error::other(error)))?;
141 checkpoint_prefix.push(b'\n');
142 }
143 let checkpoint_bytes = checkpoint_prefix.len() as u64;
144 let checkpoint_count = matrix_checkpoints.len() as u64 + u64::from(planning_checkpoint.is_some());
145 if !checkpoint_prefix.is_empty() {
146 checkpoint_prefix.extend_from_slice(&remaining);
147 remaining = checkpoint_prefix;
148 }
149
150 let new_file_len = u64::try_from(remaining.len()).map_err(|error| {
151 SessionStoreError::io(&self.events_path, std::io::Error::new(std::io::ErrorKind::InvalidData, error))
152 })?;
153 self.manifest_store.write_pending_cap_rewrite(&PendingCapRewrite {
154 previous_file_len,
155 new_file_len,
156 retained_turn_base,
157 })?;
158 let next_offset = self.replace_event_file_contents(&remaining)?;
159 let mut st = self.shared.state.lock().map_err(poison)?;
160 st.apply_cap_eviction(plan, next_offset);
161 for entry in &mut st.index.entries {
162 entry.start_offset += checkpoint_bytes;
163 entry.end_offset += checkpoint_bytes;
164 }
165 st.manifest.event_count += checkpoint_count;
166 self.persist_meta_locked(&mut st)?;
169 self.manifest_store.clear_pending_cap_rewrite()?;
170 Ok(())
171 }
172}