treeship_core/session/
event_log.rs1use std::io::{BufRead, Write};
7use std::path::{Path, PathBuf};
8use std::sync::atomic::{AtomicU64, Ordering};
9
10use crate::session::event::SessionEvent;
11
12#[derive(Debug)]
14pub enum EventLogError {
15 Io(std::io::Error),
16 Json(serde_json::Error),
17}
18
19impl std::fmt::Display for EventLogError {
20 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21 match self {
22 Self::Io(e) => write!(f, "event log io: {e}"),
23 Self::Json(e) => write!(f, "event log json: {e}"),
24 }
25 }
26}
27
28impl std::error::Error for EventLogError {}
29impl From<std::io::Error> for EventLogError {
30 fn from(e: std::io::Error) -> Self { Self::Io(e) }
31}
32impl From<serde_json::Error> for EventLogError {
33 fn from(e: serde_json::Error) -> Self { Self::Json(e) }
34}
35
36pub struct EventLog {
38 path: PathBuf,
39 sequence: AtomicU64,
40}
41
42impl EventLog {
43 pub fn open(session_dir: &Path) -> Result<Self, EventLogError> {
48 std::fs::create_dir_all(session_dir)?;
49 let path = session_dir.join("events.jsonl");
50
51 let sequence = if path.exists() {
53 let file = std::fs::File::open(&path)?;
54 let reader = std::io::BufReader::new(file);
55 let count = reader.lines().filter(|l| l.is_ok()).count() as u64;
56 AtomicU64::new(count)
57 } else {
58 AtomicU64::new(0)
59 };
60
61 Ok(Self { path, sequence })
62 }
63
64 pub fn append(&self, event: &mut SessionEvent) -> Result<(), EventLogError> {
68 event.sequence_no = self.sequence.fetch_add(1, Ordering::SeqCst);
69
70 let mut line = serde_json::to_vec(event)?;
71 line.push(b'\n');
72
73 let mut file = std::fs::OpenOptions::new()
74 .create(true)
75 .append(true)
76 .open(&self.path)?;
77 file.write_all(&line)?;
78 file.flush()?;
79
80 Ok(())
81 }
82
83 pub fn read_all(&self) -> Result<Vec<SessionEvent>, EventLogError> {
85 if !self.path.exists() {
86 return Ok(Vec::new());
87 }
88 let file = std::fs::File::open(&self.path)?;
89 let reader = std::io::BufReader::new(file);
90 let mut events = Vec::new();
91 for line in reader.lines() {
92 let line = line?;
93 if line.trim().is_empty() {
94 continue;
95 }
96 let event: SessionEvent = serde_json::from_str(&line)?;
97 events.push(event);
98 }
99 Ok(events)
100 }
101
102 pub fn event_count(&self) -> u64 {
104 self.sequence.load(Ordering::SeqCst)
105 }
106
107 pub fn path(&self) -> &Path {
109 &self.path
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116 use crate::session::event::*;
117
118 fn make_event(session_id: &str, event_type: EventType) -> SessionEvent {
119 SessionEvent {
120 session_id: session_id.into(),
121 event_id: generate_event_id(),
122 timestamp: "2026-04-05T08:00:00Z".into(),
123 sequence_no: 0,
124 trace_id: generate_trace_id(),
125 span_id: generate_span_id(),
126 parent_span_id: None,
127 agent_id: "agent://test".into(),
128 agent_instance_id: "ai_test_1".into(),
129 agent_name: "test-agent".into(),
130 agent_role: None,
131 host_id: "host_test".into(),
132 tool_runtime_id: None,
133 event_type,
134 artifact_ref: None,
135 meta: None,
136 }
137 }
138
139 #[test]
140 fn append_and_read_back() {
141 let dir = std::env::temp_dir().join(format!("treeship-evtlog-test-{}", rand::random::<u32>()));
142 let log = EventLog::open(&dir).unwrap();
143
144 let mut e1 = make_event("ssn_001", EventType::SessionStarted);
145 let mut e2 = make_event("ssn_001", EventType::AgentStarted {
146 parent_agent_instance_id: None,
147 });
148
149 log.append(&mut e1).unwrap();
150 log.append(&mut e2).unwrap();
151
152 assert_eq!(log.event_count(), 2);
153 assert_eq!(e1.sequence_no, 0);
154 assert_eq!(e2.sequence_no, 1);
155
156 let events = log.read_all().unwrap();
157 assert_eq!(events.len(), 2);
158 assert_eq!(events[0].sequence_no, 0);
159 assert_eq!(events[1].sequence_no, 1);
160
161 let _ = std::fs::remove_dir_all(&dir);
162 }
163
164 #[test]
165 fn reopen_preserves_sequence() {
166 let dir = std::env::temp_dir().join(format!("treeship-evtlog-reopen-{}", rand::random::<u32>()));
167
168 {
169 let log = EventLog::open(&dir).unwrap();
170 let mut e = make_event("ssn_001", EventType::SessionStarted);
171 log.append(&mut e).unwrap();
172 }
173
174 let log = EventLog::open(&dir).unwrap();
176 assert_eq!(log.event_count(), 1);
177
178 let mut e2 = make_event("ssn_001", EventType::AgentStarted {
179 parent_agent_instance_id: None,
180 });
181 log.append(&mut e2).unwrap();
182 assert_eq!(e2.sequence_no, 1);
183
184 let _ = std::fs::remove_dir_all(&dir);
185 }
186}