1use async_trait::async_trait;
4use chrono::Utc;
5use rusqlite::{OptionalExtension, params};
6use starweaver_core::{RunId, SessionId};
7use starweaver_runtime::AgentStreamRecord;
8use starweaver_stream::{
9 DisplayMessage, ReplayCursor, ReplayError, ReplayResult, ReplayScope, ReplaySnapshot,
10 StreamArchive,
11};
12
13use super::{
14 DisplayReplayWindow, LocalStore,
15 db::insert_raw_stream_records_tx,
16 db::{insert_display_messages_for_run_tx, insert_display_messages_tx},
17};
18use crate::{CliResult, config::CliConfig};
19
20#[derive(Clone, Debug)]
22pub struct LocalStreamArchive {
23 config: CliConfig,
24}
25
26enum ParsedReplayScope<'a> {
27 Run(&'a str),
28 Session(&'a str),
29}
30
31enum DisplayAppendTarget {
32 Run((SessionId, RunId)),
33 Session,
34}
35
36impl LocalStreamArchive {
37 #[must_use]
39 pub const fn new(config: CliConfig) -> Self {
40 Self { config }
41 }
42
43 fn open_store(&self) -> ReplayResult<LocalStore> {
44 LocalStore::open(&self.config).map_err(replay_failed)
45 }
46
47 pub fn replay_display_window(
49 &self,
50 session_id: &str,
51 run_id: Option<&str>,
52 cursor: Option<&ReplayCursor>,
53 ) -> CliResult<DisplayReplayWindow> {
54 LocalStore::open(&self.config)?.replay_display_window(session_id, run_id, cursor)
55 }
56}
57
58#[async_trait]
59impl StreamArchive for LocalStreamArchive {
60 async fn append_raw_records(
61 &self,
62 session_id: &SessionId,
63 run_id: &RunId,
64 records: Vec<AgentStreamRecord>,
65 ) -> ReplayResult<()> {
66 let mut store = self.open_store()?;
67 let run = store
68 .load_run(session_id.as_str(), run_id.as_str())
69 .map_err(replay_failed)?;
70 let tx = store
71 .conn
72 .transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)
73 .map_err(replay_failed)?;
74 insert_raw_stream_records_tx(&tx, &run, &records).map_err(replay_failed)?;
75 tx.commit().map_err(replay_failed)
76 }
77
78 async fn replay_raw_after(
79 &self,
80 session_id: &SessionId,
81 run_id: &RunId,
82 cursor: Option<ReplayCursor>,
83 ) -> ReplayResult<Vec<AgentStreamRecord>> {
84 let scope = ReplayScope::run(run_id.as_str());
85 if let Some(cursor) = cursor.as_ref() {
86 cursor.validate_scope(&scope)?;
87 }
88 let after = cursor.map_or(0, |cursor| cursor.sequence.saturating_add(1));
89 let store = self.open_store()?;
90 let mut stmt = store
91 .conn
92 .prepare(
93 r"
94 SELECT record_json
95 FROM raw_stream_records
96 WHERE session_id = ?1 AND run_id = ?2 AND sequence_no >= ?3
97 ORDER BY sequence_no ASC
98 ",
99 )
100 .map_err(replay_failed)?;
101 let rows = stmt
102 .query_map(
103 params![
104 session_id.as_str(),
105 run_id.as_str(),
106 i64::try_from(after).map_err(replay_failed)?
107 ],
108 |row| row.get::<_, String>(0),
109 )
110 .map_err(replay_failed)?;
111 rows.collect::<Result<Vec<_>, _>>()
112 .map_err(replay_failed)?
113 .into_iter()
114 .map(|json| serde_json::from_str(&json).map_err(replay_failed))
115 .collect()
116 }
117
118 async fn append_display_messages(
119 &self,
120 scope: ReplayScope,
121 messages: Vec<DisplayMessage>,
122 ) -> ReplayResult<()> {
123 if messages.is_empty() {
124 return Ok(());
125 }
126 let mut store = self.open_store()?;
127 let append_target = match parse_scope(&scope)? {
128 ParsedReplayScope::Run(run_id) => {
129 let storage_run_ref = storage_run_ref_for_scope(&store, run_id)?;
130 validate_run_scoped_display_messages(&storage_run_ref.0, &messages)?;
131 DisplayAppendTarget::Run(storage_run_ref)
132 }
133 ParsedReplayScope::Session(session_id) => {
134 validate_session_scoped_display_messages(&store, session_id, &messages)?;
135 DisplayAppendTarget::Session
136 }
137 };
138 let tx = store
139 .conn
140 .transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)
141 .map_err(replay_failed)?;
142 match append_target {
143 DisplayAppendTarget::Run((storage_session_id, storage_run_id)) => {
144 insert_display_messages_for_run_tx(
145 &tx,
146 &storage_session_id,
147 &storage_run_id,
148 &messages,
149 )
150 .map_err(replay_failed)?;
151 }
152 DisplayAppendTarget::Session => {
153 insert_display_messages_tx(&tx, &messages).map_err(replay_failed)?;
154 }
155 }
156 tx.commit().map_err(replay_failed)
157 }
158
159 async fn replay_display_after(
160 &self,
161 scope: &ReplayScope,
162 cursor: Option<ReplayCursor>,
163 ) -> ReplayResult<Vec<DisplayMessage>> {
164 if let Some(cursor) = cursor.as_ref() {
165 cursor.validate_scope(scope)?;
166 }
167 let store = self.open_store()?;
168 match parse_scope(scope)? {
169 ParsedReplayScope::Run(run_id) => {
170 let (session_id, run_id) = storage_run_ref_for_scope(&store, run_id)?;
171 replay_run_display(&store, &session_id, &run_id, cursor.as_ref())
172 }
173 ParsedReplayScope::Session(session_id) => {
174 replay_session_display(&store, session_id, cursor.as_ref())
175 }
176 }
177 }
178
179 async fn append_snapshot(
180 &self,
181 scope: ReplayScope,
182 snapshot: ReplaySnapshot,
183 ) -> ReplayResult<()> {
184 let store = self.open_store()?;
185 store
186 .conn
187 .execute(
188 "INSERT OR REPLACE INTO replay_snapshots (scope, snapshot_json, updated_at)
189 VALUES (?1, ?2, ?3)",
190 params![
191 scope.as_str(),
192 serde_json::to_string(&snapshot).map_err(replay_failed)?,
193 Utc::now().to_rfc3339()
194 ],
195 )
196 .map_err(replay_failed)?;
197 Ok(())
198 }
199
200 async fn latest_snapshot(&self, scope: &ReplayScope) -> ReplayResult<Option<ReplaySnapshot>> {
201 let store = self.open_store()?;
202 store
203 .conn
204 .query_row(
205 "SELECT snapshot_json FROM replay_snapshots WHERE scope = ?1",
206 params![scope.as_str()],
207 |row| row.get::<_, String>(0),
208 )
209 .optional()
210 .map_err(replay_failed)?
211 .map(|json| serde_json::from_str(&json).map_err(replay_failed))
212 .transpose()
213 }
214
215 async fn cursor_range(
216 &self,
217 scope: &ReplayScope,
218 ) -> ReplayResult<Option<(ReplayCursor, ReplayCursor)>> {
219 let store = self.open_store()?;
220 match parse_scope(scope)? {
221 ParsedReplayScope::Run(run_id) => {
222 let (session_id, run_id) = storage_run_ref_for_scope(&store, run_id)?;
223 run_cursor_range(&store, scope, &session_id, &run_id)
224 }
225 ParsedReplayScope::Session(session_id) => {
226 session_cursor_range(&store, scope, session_id)
227 }
228 }
229 }
230}
231
232fn storage_run_ref_for_scope(store: &LocalStore, run_id: &str) -> ReplayResult<(SessionId, RunId)> {
233 let mut stmt = store
234 .conn
235 .prepare("SELECT session_id FROM runs WHERE run_id = ?1 ORDER BY updated_at DESC LIMIT 2")
236 .map_err(replay_failed)?;
237 let session_ids = stmt
238 .query_map(params![run_id], |row| row.get::<_, String>(0))
239 .map_err(replay_failed)?
240 .collect::<Result<Vec<_>, _>>()
241 .map_err(replay_failed)?;
242 match session_ids.as_slice() {
243 [] => Err(ReplayError::NotFound(format!("run:{run_id}"))),
244 [session_id] => Ok((
245 SessionId::from_string(session_id.clone()),
246 RunId::from_string(run_id.to_string()),
247 )),
248 _ => Err(ReplayError::Failed(format!(
249 "run scope run:{run_id} is ambiguous across multiple sessions"
250 ))),
251 }
252}
253
254fn validate_run_scoped_display_messages(
255 storage_session_id: &SessionId,
256 messages: &[DisplayMessage],
257) -> ReplayResult<()> {
258 for (index, message) in messages.iter().enumerate() {
259 if message.session_id.as_str() != storage_session_id.as_str() {
260 return Err(ReplayError::Failed(format!(
261 "display message at index {index} has session_id {}, but run scope belongs to session_id {}",
262 message.session_id.as_str(),
263 storage_session_id.as_str()
264 )));
265 }
266 }
267 Ok(())
268}
269
270fn validate_session_scoped_display_messages(
271 store: &LocalStore,
272 session_id: &str,
273 messages: &[DisplayMessage],
274) -> ReplayResult<()> {
275 for (index, message) in messages.iter().enumerate() {
276 if message.session_id.as_str() != session_id {
277 return Err(ReplayError::Failed(format!(
278 "display message at index {index} has session_id {}, but session scope is session:{session_id}",
279 message.session_id.as_str()
280 )));
281 }
282 let run_exists = store
283 .conn
284 .query_row(
285 "SELECT EXISTS(SELECT 1 FROM runs WHERE session_id = ?1 AND run_id = ?2)",
286 params![session_id, message.run_id.as_str()],
287 |row| row.get::<_, i64>(0),
288 )
289 .map_err(replay_failed)?
290 != 0;
291 if !run_exists {
292 return Err(ReplayError::Failed(format!(
293 "display message at index {index} has run_id {}, which is not a run in session scope session:{session_id}",
294 message.run_id.as_str()
295 )));
296 }
297 }
298 Ok(())
299}
300
301fn replay_run_display(
302 store: &LocalStore,
303 session_id: &SessionId,
304 run_id: &RunId,
305 cursor: Option<&ReplayCursor>,
306) -> ReplayResult<Vec<DisplayMessage>> {
307 let after = cursor.map_or(0, |cursor| cursor.sequence.saturating_add(1));
308 let mut stmt = store
309 .conn
310 .prepare(
311 r"
312 SELECT message_json
313 FROM display_messages
314 WHERE session_id = ?1 AND run_id = ?2 AND sequence_no >= ?3
315 ORDER BY sequence_no ASC
316 ",
317 )
318 .map_err(replay_failed)?;
319 let rows = stmt
320 .query_map(
321 params![
322 session_id.as_str(),
323 run_id.as_str(),
324 i64::try_from(after).map_err(replay_failed)?
325 ],
326 |row| row.get::<_, String>(0),
327 )
328 .map_err(replay_failed)?;
329 collect_display_messages(rows)
330}
331
332fn replay_session_display(
333 store: &LocalStore,
334 session_id: &str,
335 cursor: Option<&ReplayCursor>,
336) -> ReplayResult<Vec<DisplayMessage>> {
337 let after = cursor.map_or(0, |cursor| cursor.sequence.saturating_add(1));
338 let mut stmt = store
339 .conn
340 .prepare(
341 r"
342 SELECT dm.message_json
343 FROM display_messages dm
344 JOIN runs r ON r.session_id = dm.session_id AND r.run_id = dm.run_id
345 WHERE dm.session_id = ?1
346 ORDER BY r.sequence_no ASC, dm.sequence_no ASC
347 ",
348 )
349 .map_err(replay_failed)?;
350 let rows = stmt
351 .query_map(params![session_id], |row| row.get::<_, String>(0))
352 .map_err(replay_failed)?;
353 let messages = collect_display_messages(rows)?;
354 Ok(messages
355 .into_iter()
356 .enumerate()
357 .filter_map(|(sequence, message)| (sequence >= after).then_some(message))
358 .collect())
359}
360
361fn collect_display_messages(
362 rows: rusqlite::MappedRows<'_, impl FnMut(&rusqlite::Row<'_>) -> rusqlite::Result<String>>,
363) -> ReplayResult<Vec<DisplayMessage>> {
364 rows.collect::<Result<Vec<_>, _>>()
365 .map_err(replay_failed)?
366 .into_iter()
367 .map(|json| serde_json::from_str(&json).map_err(replay_failed))
368 .collect()
369}
370
371fn run_cursor_range(
372 store: &LocalStore,
373 scope: &ReplayScope,
374 session_id: &SessionId,
375 run_id: &RunId,
376) -> ReplayResult<Option<(ReplayCursor, ReplayCursor)>> {
377 let range = store
378 .conn
379 .query_row(
380 "SELECT MIN(sequence_no), MAX(sequence_no) FROM display_messages WHERE session_id = ?1 AND run_id = ?2",
381 params![session_id.as_str(), run_id.as_str()],
382 |row| Ok((row.get::<_, Option<i64>>(0)?, row.get::<_, Option<i64>>(1)?)),
383 )
384 .map_err(replay_failed)?;
385 let (Some(first), Some(last)) = range else {
386 return Ok(None);
387 };
388 Ok(Some((
389 ReplayCursor::new(
390 scope.clone(),
391 usize::try_from(first).map_err(replay_failed)?,
392 ),
393 ReplayCursor::new(scope.clone(), usize::try_from(last).map_err(replay_failed)?),
394 )))
395}
396
397fn session_cursor_range(
398 store: &LocalStore,
399 scope: &ReplayScope,
400 session_id: &str,
401) -> ReplayResult<Option<(ReplayCursor, ReplayCursor)>> {
402 let count = store
403 .conn
404 .query_row(
405 "SELECT COUNT(*) FROM display_messages WHERE session_id = ?1",
406 params![session_id],
407 |row| row.get::<_, i64>(0),
408 )
409 .map_err(replay_failed)?;
410 let count = usize::try_from(count).map_err(replay_failed)?;
411 if count == 0 {
412 return Ok(None);
413 }
414 Ok(Some((
415 ReplayCursor::new(scope.clone(), 0),
416 ReplayCursor::new(scope.clone(), count.saturating_sub(1)),
417 )))
418}
419
420fn parse_scope(scope: &ReplayScope) -> ReplayResult<ParsedReplayScope<'_>> {
421 if let Some(run_id) = scope.as_str().strip_prefix("run:") {
422 return Ok(ParsedReplayScope::Run(run_id));
423 }
424 if let Some(session_id) = scope.as_str().strip_prefix("session:") {
425 return Ok(ParsedReplayScope::Session(session_id));
426 }
427 Err(ReplayError::InvalidCursor(format!(
428 "unsupported replay scope {}",
429 scope.as_str()
430 )))
431}
432
433fn replay_failed(error: impl std::fmt::Display) -> ReplayError {
434 ReplayError::Failed(error.to_string())
435}