1use std::collections::{BTreeMap, HashMap, VecDeque};
4use std::fs::File;
5use std::io::{BufRead, Read, Seek, SeekFrom, Write};
6use std::path::{Path, PathBuf};
7use std::sync::atomic::{AtomicBool, Ordering};
8use std::sync::{Arc, Mutex, OnceLock, Weak};
9
10use chrono::Utc;
11use serde::{Deserialize, Serialize};
12use vtcode_commons::VtCodePaths;
13use vtcode_exec_events::{EVENT_SCHEMA_VERSION, ThreadEvent, ThreadItemDetails, VersionedThreadEvent};
14
15use crate::error::SessionStoreError;
16
17pub(crate) const SESSION_LOCK_FILE: &str = "session.lock";
19use crate::manifest::{ManifestStore, PendingCapRewrite};
20use crate::session_dir;
21
22mod append;
23mod eviction;
24mod manifest;
25mod scan;
26
27use eviction::default_eviction_summary_hook;
28pub use manifest::{SessionManifest, TurnIndex, TurnIndexEntry};
29use scan::{
30 count_persisted_event_records, decode_events, infer_scan_turn_base, requires_full_lifecycle_validation,
31 valid_lifecycle_payload,
32};
33
34pub const DEFAULT_MAX_EVENTS: usize = 10_000;
37
38const MAX_WRITE_BUFFER_BYTES: usize = 64 * 1024;
41
42pub type EvictionSummaryHook = Arc<dyn Fn(&[ThreadEvent]) -> Result<(), SessionStoreError> + Send + Sync>;
49
50#[derive(Debug, Deserialize)]
56struct VersionedEventKind<'a> {
57 #[serde(rename = "schema_version", borrow)]
58 _schema_version: &'a str,
59 #[serde(borrow)]
60 event: EventKind<'a>,
61}
62
63#[derive(Debug, Deserialize)]
64struct EventKind<'a> {
65 #[serde(rename = "type", borrow)]
66 kind: &'a str,
67}
68
69#[derive(Serialize)]
76struct BorrowedVersionedEvent<'a> {
77 schema_version: &'a str,
78 event: &'a ThreadEvent,
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86enum LifecycleKind {
87 ThreadStarted,
88 ThreadCompleted,
89 TurnStarted,
90 TurnCompleted,
91 TurnFailed,
92 Other,
93}
94
95impl LifecycleKind {
96 #[inline]
98 fn from_event(event: &ThreadEvent) -> Self {
99 match event {
100 ThreadEvent::ThreadStarted(_) => Self::ThreadStarted,
101 ThreadEvent::ThreadCompleted(_) => Self::ThreadCompleted,
102 ThreadEvent::TurnStarted(_) => Self::TurnStarted,
103 ThreadEvent::TurnCompleted(_) => Self::TurnCompleted,
104 ThreadEvent::TurnFailed(_) => Self::TurnFailed,
105 _ => Self::Other,
106 }
107 }
108
109 #[inline]
111 fn from_kind(kind: &str) -> Self {
112 match kind {
113 "thread.started" => Self::ThreadStarted,
114 "thread.completed" => Self::ThreadCompleted,
115 "turn.started" => Self::TurnStarted,
116 "turn.completed" => Self::TurnCompleted,
117 "turn.failed" => Self::TurnFailed,
118 _ => Self::Other,
119 }
120 }
121}
122
123struct LogState {
126 manifest: SessionManifest,
127 index: TurnIndex,
128 in_turn: bool,
132 next_offset: u64,
136 write_buf: Vec<u8>,
139}
140
141#[derive(Debug, Clone, Copy)]
142struct CapEvictionPlan {
143 truncate_offset: u64,
144 evicted_event_count: u64,
145 evicted_turn_count: usize,
146}
147
148impl LogState {
149 fn new(session_id: &str) -> Self {
150 Self {
151 manifest: SessionManifest::new(session_id),
152 index: TurnIndex::default(),
153 in_turn: false,
154 next_offset: 0,
155 write_buf: Vec::with_capacity(65536),
156 }
157 }
158
159 fn serialize_event(&mut self, event: &ThreadEvent) -> Result<(u64, u64), SessionStoreError> {
168 let start = self.next_offset;
169 let buf_len_before = self.write_buf.len();
170 if let Err(err) = serde_json::to_writer(
171 &mut self.write_buf,
172 &BorrowedVersionedEvent { schema_version: EVENT_SCHEMA_VERSION, event },
173 ) {
174 self.write_buf.truncate(buf_len_before);
175 return Err(err.into());
176 }
177 self.write_buf.push(b'\n');
178 let written = self.write_buf.len() - buf_len_before;
179 let end = start + written as u64;
180 self.next_offset = end;
181 Ok((start, end))
182 }
183
184 fn apply_lifecycle_event(&mut self, kind: LifecycleKind, start: u64, end: u64) -> bool {
197 let is_boundary = match kind {
198 LifecycleKind::ThreadStarted => {
199 self.manifest.status = "active".to_string();
200 false
201 }
202 LifecycleKind::ThreadCompleted => {
203 self.manifest.status = "completed".to_string();
204 true
205 }
206 LifecycleKind::TurnStarted => {
207 self.manifest.status = "active".to_string();
208 self.in_turn = true;
209 let n = self.manifest.turn_count + 1;
210 self.index.entries.push_back(TurnIndexEntry {
211 turn_number: n,
212 start_offset: start,
213 end_offset: end,
214 event_count: 1,
215 ts: now_rfc3339(),
216 });
217 false
218 }
219 LifecycleKind::TurnCompleted | LifecycleKind::TurnFailed => {
220 if self.in_turn {
221 if let Some(entry) = self.index.entries.back_mut() {
222 entry.end_offset = end;
223 entry.event_count += 1;
224 self.manifest.turn_count = self.manifest.turn_count.max(entry.turn_number);
229 }
230 self.in_turn = false;
231 }
232 true
233 }
234 LifecycleKind::Other => {
235 if self.in_turn
236 && let Some(entry) = self.index.entries.back_mut()
237 {
238 entry.end_offset = end;
239 entry.event_count += 1;
240 }
241 false
242 }
243 };
244 self.manifest.in_turn = Some(self.in_turn);
249 is_boundary
250 }
251
252 fn plan_cap_eviction(&self, max_events: usize) -> Option<CapEvictionPlan> {
259 if max_events == 0 || self.manifest.event_count <= max_events as u64 {
260 return None;
261 }
262 let mut evicted_event_count = 0u64;
263 let mut truncate_offset = 0u64;
264 let mut evicted_turn_count = 0;
265 for (entry_index, oldest) in self.index.entries.iter().enumerate() {
266 if self.in_turn && entry_index + 1 == self.index.entries.len() {
273 break;
274 }
275 if self.manifest.event_count.saturating_sub(evicted_event_count) <= max_events as u64 {
276 break;
277 }
278 truncate_offset = oldest.end_offset;
279 evicted_event_count += oldest.event_count;
280 evicted_turn_count += 1;
281 }
282 if truncate_offset == 0 || evicted_turn_count == 0 {
283 None
284 } else {
285 Some(CapEvictionPlan {
286 truncate_offset,
287 evicted_event_count,
288 evicted_turn_count,
289 })
290 }
291 }
292
293 fn apply_cap_eviction(&mut self, plan: CapEvictionPlan, next_offset: u64) {
294 for _ in 0..plan.evicted_turn_count {
295 let _ = self.index.entries.pop_front();
296 }
297 for entry in &mut self.index.entries {
298 entry.start_offset = entry.start_offset.saturating_sub(plan.truncate_offset);
299 entry.end_offset = entry.end_offset.saturating_sub(plan.truncate_offset);
300 }
301 self.next_offset = next_offset;
302 self.manifest.event_count = self.manifest.event_count.saturating_sub(plan.evicted_event_count);
303 self.manifest.retained_turn_base = Some(self.retained_turn_base_after_eviction(0));
304 }
305
306 fn retained_turn_base_after_eviction(&self, evicted_turn_count: usize) -> u64 {
307 self.index
308 .entries
309 .get(evicted_turn_count)
310 .map(|entry| entry.turn_number)
311 .or_else(|| self.manifest.turn_count.checked_add(1))
312 .unwrap_or(1)
313 .max(1)
314 }
315}
316
317struct SessionShared {
325 file: Mutex<Option<File>>,
326 state: Mutex<LogState>,
327 eviction_lock: Mutex<()>,
328 initialized: AtomicBool,
329 liveness_lock: Mutex<Option<File>>,
336}
337
338#[derive(Debug, Clone, Copy, PartialEq, Eq)]
340enum OpenMode {
341 Shared,
344 Exclusive,
348}
349
350fn shared_session(
355 events_path: &Path,
356 session_id: &str,
357 mode: OpenMode,
358) -> Result<Arc<SessionShared>, SessionStoreError> {
359 static SESSION_SHARED: OnceLock<Mutex<HashMap<PathBuf, Weak<SessionShared>>>> = OnceLock::new();
360
361 let key = events_path
362 .parent()
363 .and_then(|parent| vtcode_commons::paths::canonicalize(parent).ok())
364 .and_then(|parent| events_path.file_name().map(|name| parent.join(name)))
365 .unwrap_or_else(|| events_path.to_path_buf());
366 let registry = SESSION_SHARED.get_or_init(|| Mutex::new(HashMap::new()));
367 let mut shared_by_path = match registry.lock() {
368 Ok(locks) => locks,
369 Err(poisoned) => poisoned.into_inner(),
370 };
371 shared_by_path.retain(|_, shared| shared.strong_count() > 0);
375 if let Some(shared) = shared_by_path.get(&key).and_then(Weak::upgrade) {
376 if mode == OpenMode::Exclusive {
380 shared.ensure_exclusive_liveness(events_path, session_id)?;
381 }
382 return Ok(shared);
383 }
384
385 let file = VtCodePaths::open_private_append_file(events_path)
386 .map_err(|error| SessionStoreError::io(events_path.to_path_buf(), std::io::Error::other(error)))?;
387 let liveness_lock = match mode {
388 OpenMode::Shared => acquire_liveness_lock(events_path, session_id),
389 OpenMode::Exclusive => acquire_liveness_lock_strict(events_path, session_id)?,
390 };
391 let shared = Arc::new(SessionShared {
392 file: Mutex::new(Some(file)),
393 state: Mutex::new(LogState::new(session_id)),
394 eviction_lock: Mutex::new(()),
395 initialized: AtomicBool::new(false),
396 liveness_lock: Mutex::new(liveness_lock),
397 });
398 shared_by_path.insert(key, Arc::downgrade(&shared));
399 Ok(shared)
400}
401
402fn acquire_liveness_lock(events_path: &Path, session_id: &str) -> Option<File> {
411 acquire_liveness_lock_strict(events_path, session_id).unwrap_or(None)
412}
413
414fn acquire_liveness_lock_strict(events_path: &Path, session_id: &str) -> Result<Option<File>, SessionStoreError> {
421 let Some(dir) = events_path.parent() else {
422 return Ok(None);
423 };
424 let lock_path = dir.join(SESSION_LOCK_FILE);
425 if VtCodePaths::write_private_file_atomic_if_absent(&lock_path, b"").is_err() {
426 return Ok(None);
427 }
428 let file = match std::fs::OpenOptions::new().read(true).write(true).open(&lock_path) {
429 Ok(file) => file,
430 Err(_) => return Ok(None),
431 };
432 match file.try_lock().map_err(std::io::Error::from) {
433 Ok(()) => Ok(Some(file)),
434 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
435 Err(SessionStoreError::SessionBusy { session: session_id.to_owned() })
436 }
437 Err(_) => Ok(None),
438 }
439}
440
441impl SessionShared {
442 fn ensure_exclusive_liveness(&self, events_path: &Path, session_id: &str) -> Result<(), SessionStoreError> {
446 let mut slot = self.liveness_lock.lock().map_err(poison)?;
447 if slot.is_none() {
448 *slot = acquire_liveness_lock_strict(events_path, session_id)?;
449 }
450 Ok(())
451 }
452}
453
454pub struct SessionEventLog {
460 events_path: PathBuf,
461 manifest_store: ManifestStore,
462 shared: Arc<SessionShared>,
463 max_events: usize,
464 eviction_summary_hook: EvictionSummaryHook,
465}
466
467impl SessionEventLog {
468 pub(crate) fn open(workspace: &Path, session_id: &str, max_events: usize) -> Result<Self, SessionStoreError> {
471 let dir = session_dir(workspace, session_id);
472 let hook = default_eviction_summary_hook(dir.join(crate::DERIVED_DIR), session_id.to_string());
473 Self::open_inner(workspace, session_id, max_events, hook, OpenMode::Shared)
474 }
475
476 pub(crate) fn open_exclusive(
481 workspace: &Path,
482 session_id: &str,
483 max_events: usize,
484 ) -> Result<Self, SessionStoreError> {
485 let dir = session_dir(workspace, session_id);
486 let hook = default_eviction_summary_hook(dir.join(crate::DERIVED_DIR), session_id.to_string());
487 Self::open_inner(workspace, session_id, max_events, hook, OpenMode::Exclusive)
488 }
489
490 pub fn open_with_eviction_summary(
496 workspace: &Path,
497 session_id: &str,
498 max_events: usize,
499 eviction_summary_hook: EvictionSummaryHook,
500 ) -> Result<Self, SessionStoreError> {
501 Self::open_inner(workspace, session_id, max_events, eviction_summary_hook, OpenMode::Shared)
502 }
503
504 fn open_inner(
505 workspace: &Path,
506 session_id: &str,
507 max_events: usize,
508 eviction_summary_hook: EvictionSummaryHook,
509 mode: OpenMode,
510 ) -> Result<Self, SessionStoreError> {
511 let dir = session_dir(workspace, session_id);
512 crate::ensure_private_directory(&crate::sessions_root(workspace))?;
513 crate::ensure_private_directory(&dir)?;
514 crate::ensure_private_directory(&dir.join(crate::DERIVED_DIR))?;
515 crate::ensure_private_directory(&dir.join("index"))?;
516 let events_path = dir.join("events.jsonl");
517 let manifest_store = ManifestStore::new(dir.clone());
518 let pending_rewrite = manifest_store.load_pending_cap_rewrite()?;
519 let shared = shared_session(&events_path, session_id, mode)?;
520 let log = Self {
521 events_path: events_path.clone(),
522 manifest_store,
523 shared,
524 max_events,
525 eviction_summary_hook,
526 };
527 let _eviction_guard = log.shared.eviction_lock.lock().map_err(poison)?;
528 if !log.shared.initialized.load(Ordering::Acquire) {
531 let manifest_opt = log.manifest_store.load_manifest()?;
532 let index_opt = log.manifest_store.load_turn_index()?;
533 let file_len = log.event_file_metadata_len()?;
534 let pending_rewrite_matches_file = pending_rewrite.as_ref().is_some_and(|pending| {
535 pending.new_file_len == file_len && pending.new_file_len < pending.previous_file_len
536 });
537 match (&manifest_opt, &index_opt) {
538 (Some(manifest), Some(index))
539 if !pending_rewrite_matches_file
540 && manifest.in_turn.is_some()
541 && manifest.persisted_file_len == Some(file_len)
542 && index.is_valid_for_file(file_len)
543 && index.is_consistent_with_manifest(manifest) =>
544 {
545 let mut st = log.shared.state.lock().map_err(poison)?;
546 st.in_turn = manifest.in_turn.unwrap_or(false);
547 st.manifest = manifest.clone();
548 st.index = index.clone();
549 st.next_offset = file_len;
550 }
551 _ => {
552 let scan_turn_base = infer_scan_turn_base(
553 manifest_opt.as_ref(),
554 index_opt.as_ref(),
555 pending_rewrite.as_ref().filter(|_| pending_rewrite_matches_file),
556 file_len,
557 );
558 {
559 let mut st = log.shared.state.lock().map_err(poison)?;
560 if let Some(previous) = manifest_opt.as_ref() {
561 st.manifest = previous.clone();
562 }
563 st.manifest.turn_count = scan_turn_base.saturating_sub(1);
567 st.manifest.event_count = 0;
568 st.manifest.status = "active".to_string();
569 st.manifest.in_turn = Some(false);
570 st.manifest.retained_turn_base = Some(scan_turn_base);
571 st.index = TurnIndex::default();
572 st.in_turn = false;
573 }
574 log.scan()?;
575 let mut st = log.shared.state.lock().map_err(poison)?;
576 st.next_offset = file_len;
577 log.persist_meta_locked(&mut st)?;
578 }
579 }
580 if pending_rewrite.is_some() {
581 log.manifest_store.clear_pending_cap_rewrite()?;
587 }
588 log.shared.initialized.store(true, Ordering::Release);
589 }
590 drop(_eviction_guard);
591 Ok(log)
592 }
593
594 pub(crate) fn reconstruct_turn(&self, turn: u64) -> Result<Vec<ThreadEvent>, SessionStoreError> {
596 let _eviction_guard = self.shared.eviction_lock.lock().map_err(poison)?;
600 let entry = {
601 let st = self.shared.state.lock().map_err(poison)?;
602 st.index
603 .entries
604 .iter()
605 .find(|e| e.turn_number == turn)
606 .cloned()
607 .ok_or(SessionStoreError::TurnNotFound { session: st.manifest.session_id.clone(), turn })?
608 };
609 {
610 let mut st = self.shared.state.lock().map_err(poison)?;
611 self.flush_write_buf_locked(&mut st)?;
612 }
613 let buf = {
614 let mut file_slot = self.shared.file.lock().map_err(poison)?;
615 let file = file_slot.as_mut().ok_or_else(|| self.event_file_unavailable())?;
616 file.seek(SeekFrom::Start(entry.start_offset))
617 .map_err(|e| SessionStoreError::io(&self.events_path, e))?;
618 let len = usize::try_from(entry.end_offset.checked_sub(entry.start_offset).ok_or_else(|| {
619 SessionStoreError::io(
620 &self.events_path,
621 std::io::Error::new(std::io::ErrorKind::InvalidData, "turn index offsets are out of order"),
622 )
623 })?)
624 .map_err(|error| {
625 SessionStoreError::io(&self.events_path, std::io::Error::new(std::io::ErrorKind::InvalidData, error))
626 })?;
627 let mut buf = vec![0u8; len];
628 file.read_exact(&mut buf)
629 .map_err(|e| SessionStoreError::io(&self.events_path, e))?;
630 buf
631 };
632 let text = String::from_utf8_lossy(&buf);
633 let mut events = Vec::new();
634 for line in text.lines() {
635 let line = line.trim();
636 if line.is_empty() {
637 continue;
638 }
639 let v: VersionedThreadEvent = match serde_json::from_str(line) {
645 Ok(v) => v,
646 Err(_) => continue,
647 };
648 events.push(v.into_event());
649 }
650 Ok(events)
651 }
652
653 #[must_use]
655 pub(crate) fn turn_count(&self) -> u64 {
656 self.shared.state.lock().map_err(poison).map_or(0, |s| s.manifest.turn_count)
657 }
658
659 #[must_use]
661 pub fn event_count(&self) -> u64 {
662 self.shared.state.lock().map_err(poison).map_or(0, |s| s.manifest.event_count)
663 }
664
665 pub fn visit_snapshot<F>(&self, mut visitor: F) -> Result<SessionManifest, SessionStoreError>
669 where
670 F: FnMut(u64, &[u8]),
671 {
672 use std::io::{BufRead, BufReader};
673 let _eviction_guard = self.shared.eviction_lock.lock().map_err(poison)?;
674 let mut st = self.shared.state.lock().map_err(poison)?;
675 self.persist_meta_locked(&mut st)?;
676 let mut file_slot = self.shared.file.lock().map_err(poison)?;
677 let file = file_slot.as_mut().ok_or_else(|| self.event_file_unavailable())?;
678 file.seek(SeekFrom::Start(0))
679 .map_err(|e| SessionStoreError::io(&self.events_path, e))?;
680 let mut reader = BufReader::new(file.take(st.next_offset));
681 let mut offset = 0;
682 let mut line = Vec::new();
683 loop {
684 line.clear();
685 let length = reader
686 .read_until(b'\n', &mut line)
687 .map_err(|e| SessionStoreError::io(&self.events_path, e))?;
688 if length == 0 {
689 break;
690 }
691 visitor(offset, &line);
692 offset += length as u64;
693 }
694 Ok(st.manifest.clone())
695 }
696
697 pub fn flush(&self) -> Result<(), SessionStoreError> {
699 let _eviction_guard = self.shared.eviction_lock.lock().map_err(poison)?;
700 let mut st = self.shared.state.lock().map_err(poison)?;
701 self.persist_meta_locked(&mut st)
702 }
703
704 #[must_use]
706 pub fn manifest(&self) -> SessionManifest {
707 self.shared
708 .state
709 .lock()
710 .map_err(poison)
711 .map(|s| s.manifest.clone())
712 .unwrap_or_else(|_| SessionManifest::new(""))
713 }
714
715 #[must_use]
717 pub fn turn_index(&self) -> TurnIndex {
718 self.shared
719 .state
720 .lock()
721 .map_err(poison)
722 .map(|s| s.index.clone())
723 .unwrap_or_default()
724 }
725
726 pub(crate) fn complete(&self) -> Result<(), SessionStoreError> {
732 let _eviction_guard = self.shared.eviction_lock.lock().map_err(poison)?;
733 let mut st = self.shared.state.lock().map_err(poison)?;
734 st.manifest.updated_at = now_rfc3339();
735 self.persist_meta_locked(&mut st)
736 }
737
738 fn scan(&self) -> Result<(), SessionStoreError> {
744 let mut st = self.shared.state.lock().map_err(poison)?;
745 let file = self
746 .shared
747 .file
748 .lock()
749 .map_err(poison)?
750 .as_ref()
751 .ok_or_else(|| self.event_file_unavailable())?
752 .try_clone()
753 .map_err(|e| SessionStoreError::io(&self.events_path, e))?;
754 let mut reader = std::io::BufReader::new(file);
755 reader
756 .seek(SeekFrom::Start(0))
757 .map_err(|e| SessionStoreError::io(&self.events_path, e))?;
758 let mut buf = Vec::new();
759 let mut pos = 0u64;
760 let mut first_ts: Option<String> = None;
761 loop {
762 buf.clear();
763 let n = reader
764 .read_until(b'\n', &mut buf)
765 .map_err(|e| SessionStoreError::io(&self.events_path, e))?;
766 if n == 0 {
767 break;
768 }
769 let line_end = pos + n as u64;
770 let trimmed = std::str::from_utf8(&buf).unwrap_or("").trim();
771 if !trimmed.is_empty()
772 && let Ok(v) = serde_json::from_str::<VersionedEventKind<'_>>(trimmed)
773 {
774 let kind = v.event.kind;
775 if requires_full_lifecycle_validation(kind) && !valid_lifecycle_payload(kind, trimmed) {
776 pos = line_end;
777 continue;
778 }
779 st.manifest.event_count += 1;
780 if kind == "thread.started" && first_ts.is_none() {
783 first_ts = Some(now_rfc3339());
784 }
785 st.apply_lifecycle_event(LifecycleKind::from_kind(kind), pos, line_end);
788 }
789 pos = line_end;
790 }
791 st.manifest.in_turn = Some(st.in_turn);
795 if let Some(ts) = first_ts
796 && st.manifest.created_at.is_empty()
797 {
798 st.manifest.created_at = ts;
799 }
800 Ok(())
801 }
802
803 fn persist_meta_locked(&self, st: &mut LogState) -> Result<(), SessionStoreError> {
804 self.flush_write_buf_locked(st)?;
805 st.manifest.persisted_file_len = Some(st.next_offset);
810 self.manifest_store.write_turn_index(&st.index)?;
815 self.manifest_store.write_manifest(&st.manifest)?;
816 Ok(())
817 }
818
819 fn flush_write_buf_locked(&self, st: &mut LogState) -> Result<(), SessionStoreError> {
821 if st.write_buf.is_empty() {
822 return Ok(());
823 }
824 let mut file_slot = self.shared.file.lock().map_err(poison)?;
825 let file = file_slot.as_mut().ok_or_else(|| self.event_file_unavailable())?;
826 let previous_len = file.metadata().map_err(|e| SessionStoreError::io(&self.events_path, e))?.len();
827 if let Err(error) = file.write_all(&st.write_buf) {
828 if file.set_len(previous_len).is_err() {
829 st.write_buf.clear();
830 }
831 return Err(SessionStoreError::io(&self.events_path, error));
832 }
833 if let Err(error) = file.sync_data() {
834 st.write_buf.clear();
835 return Err(SessionStoreError::io(&self.events_path, error));
836 }
837 st.write_buf.clear();
838 Ok(())
839 }
840
841 fn event_file_metadata_len(&self) -> Result<u64, SessionStoreError> {
842 let file_slot = self.shared.file.lock().map_err(poison)?;
843 file_slot
844 .as_ref()
845 .ok_or_else(|| self.event_file_unavailable())?
846 .metadata()
847 .map(|metadata| metadata.len())
848 .map_err(|error| SessionStoreError::io(&self.events_path, error))
849 }
850
851 fn open_event_file(&self) -> Result<File, SessionStoreError> {
852 VtCodePaths::open_private_append_file(&self.events_path)
853 .map_err(|error| SessionStoreError::io(&self.events_path, std::io::Error::other(error)))
854 }
855
856 fn replace_event_file_contents(&self, contents: &[u8]) -> Result<u64, SessionStoreError> {
857 let old_file = {
858 let mut file_slot = self.shared.file.lock().map_err(poison)?;
859 file_slot.take().ok_or_else(|| self.event_file_unavailable())?
860 };
861 drop(old_file);
862
863 if let Err(error) = VtCodePaths::write_private_file_atomic(&self.events_path, contents)
864 .map_err(|error| SessionStoreError::io(&self.events_path, std::io::Error::other(error)))
865 {
866 let restored = self.open_event_file();
867 if let Ok(file) = restored {
868 let mut file_slot = self.shared.file.lock().map_err(poison)?;
869 *file_slot = Some(file);
870 return Err(error);
871 }
872 return Err(SessionStoreError::io(
873 &self.events_path,
874 std::io::Error::other(format!("{error}; failed to restore event log handle")),
875 ));
876 }
877
878 let replacement = self.open_event_file()?;
879 let next_offset = replacement
880 .metadata()
881 .map_err(|error| SessionStoreError::io(&self.events_path, error))?
882 .len();
883 let mut file_slot = self.shared.file.lock().map_err(poison)?;
884 *file_slot = Some(replacement);
885 Ok(next_offset)
886 }
887
888 fn event_file_unavailable(&self) -> SessionStoreError {
889 SessionStoreError::io(&self.events_path, std::io::Error::other("event log file is unavailable"))
890 }
891}
892
893impl Drop for SessionEventLog {
894 fn drop(&mut self) {
895 if let Ok(_eviction_guard) = self.shared.eviction_lock.lock()
896 && let Ok(mut st) = self.shared.state.lock()
897 {
898 let _ = self.flush_write_buf_locked(&mut st);
904 }
905 }
906}
907
908fn poison<T>(_e: std::sync::PoisonError<T>) -> SessionStoreError {
910 SessionStoreError::Io {
911 path: PathBuf::new(),
912 source: std::io::Error::other("session store lock poisoned"),
913 }
914}
915
916fn now_rfc3339() -> String {
917 Utc::now().to_rfc3339()
918}
919
920#[cfg(test)]
921mod borrowed_envelope_tests;
922#[cfg(test)]
923mod cap_eviction_tests;
924#[cfg(test)]
925mod lifecycle_state_machine_tests;