use crate::conversation::message::MessageMetadata; use crate::conversation::Conversation; use crate::session::Session; use anyhow::Result; use chrono::{DateTime, Local, NaiveDateTime, TimeZone, Utc}; use std::fs; use std::io::{self, BufRead}; use std::path::{Path, PathBuf}; use std::time::SystemTime; const MAX_FILE_SIZE: u64 = 50 * 1024 * 1024; pub fn list_sessions(session_dir: &PathBuf) -> Result> { let entries = fs::read_dir(session_dir)? .filter_map(|entry| { let entry = entry.ok()?; let path = entry.path(); if path.extension().is_some_and(|ext| ext == "jsonl") { let name = path.file_stem()?.to_string_lossy().to_string(); Some((name, path)) } else { None } }) .collect::>(); Ok(entries) } pub fn load_session(session_name: &str, session_path: &Path) -> Result { let file = fs::File::open(session_path).map_err(|e| { anyhow::anyhow!( "Failed to open session file {}: {}", session_path.display(), e ) })?; let file_metadata = file.metadata()?; if file_metadata.len() > MAX_FILE_SIZE { return Err(anyhow::anyhow!("Session file too large")); } if file_metadata.len() == 0 { return Err(anyhow::anyhow!("Empty session file")); } let modified_time = file_metadata.modified().unwrap_or(SystemTime::now()); let created_time = file_metadata .created() .unwrap_or_else(|_| parse_session_timestamp(session_name).unwrap_or(modified_time)); let reader = io::BufReader::new(file); let mut lines = reader.lines(); let mut messages = Vec::new(); let mut session = Session { id: session_name.to_string(), ..Default::default() }; if let Some(Ok(line)) = lines.next() { let mut metadata_json: serde_json::Value = serde_json::from_str(&line) .map_err(|_| anyhow::anyhow!("Invalid session metadata JSON"))?; if let Some(obj) = metadata_json.as_object_mut() { obj.entry("id").or_insert(serde_json::json!(session_name)); obj.entry("created_at") .or_insert(serde_json::json!(DateTime::::from(created_time))); obj.entry("updated_at") .or_insert(serde_json::json!(DateTime::::from(modified_time))); obj.entry("extension_data").or_insert(serde_json::json!({})); obj.entry("message_count").or_insert(serde_json::json!(0)); obj.entry("working_dir").or_insert(serde_json::json!("")); if let Some(desc) = obj.get_mut("description") { if let Some(desc_str) = desc.as_str() { *desc = serde_json::json!(desc_str .split_whitespace() .collect::>() .join(" ")); } } } session = serde_json::from_value(metadata_json)?; session.id = session_name.to_string(); } for line in lines.map_while(Result::ok) { if let Ok(mut message_json) = serde_json::from_str::(&line) { if let Some(obj) = message_json.as_object_mut() { obj.entry("metadata") .or_insert(serde_json::to_value(MessageMetadata::default())?); } if let Ok(message) = serde_json::from_value(message_json) { messages.push(message); } } } if !messages.is_empty() { session.conversation = Some(Conversation::new_unvalidated(messages)); } Ok(session) } fn parse_session_timestamp(session_name: &str) -> Option { NaiveDateTime::parse_from_str(session_name, "%Y%m%d_%H%M%S") .ok() .and_then(|dt| Local.from_local_datetime(&dt).single()) .map(SystemTime::from) } #[cfg(test)] mod tests { use super::*; use rmcp::model::Role; use tempfile::TempDir; #[test] fn test_load_legacy_session_without_metadata() { let temp_dir = TempDir::new().unwrap(); let session_path = temp_dir.path().join("20240101_120000.jsonl"); let legacy_content = r#"{"description":"test","id":"20240101_120000","created_at":"2024-01-01T12:00:00Z","updated_at":"2024-01-01T12:00:00Z","extension_data":{},"message_count":0} {"id":"msg1","role":"user","created":1704110400,"content":[{"type":"text","text":"Hello"}]} {"id":"msg2","role":"assistant","created":1704110401,"content":[{"type":"text","text":"Hi there"}]}"#; fs::write(&session_path, legacy_content).unwrap(); let session = load_session("20240101_120000", &session_path).unwrap(); assert_eq!(session.id, "20240101_120000"); let conversation = session.conversation.as_ref().unwrap(); let messages = conversation.messages(); assert_eq!(messages.len(), 2); assert_eq!(messages[0].role, Role::User); assert_eq!(messages[1].role, Role::Assistant); } }