//! `MetaEventsBackend` impl for `JsonlBackend`. use async_trait::async_trait; use chrono::Utc; use uuid::Uuid; use crate::backend::MetaEventsBackend; use crate::backends::jsonl::backend::JsonlBackend; use crate::error::{ArchivistError, Result}; use crate::storage::{append_ndjson, read_json, read_ndjson, write_json}; use crate::types::{ MetaEventRecord, SessionCompleteness, SessionKind, SessionMetadata, }; #[async_trait] impl MetaEventsBackend for JsonlBackend { async fn append_meta_events( &self, scroll_id: Uuid, events: Vec, ) -> Result<()> { // Ensure session directory exists self.paths.ensure_dirs(scroll_id).await?; // Append each event to events.jsonl let events_path = self.paths.events_path(scroll_id); for event in &events { append_ndjson(&events_path, event).await?; } // Update session.json timestamp let session_json_path = self.paths.session_json(scroll_id); let now = Utc::now(); let session_metadata = match read_json::(&session_json_path).await { Ok(mut metadata) => { metadata.updated_at = now; metadata } Err(_) => { // session.json doesn't exist, this shouldn't happen for meta sessions // but we'll handle it gracefully tracing::warn!( scroll_id = %scroll_id, "session.json missing when appending meta events, creating minimal metadata" ); SessionMetadata { version: 1, scroll_id, created_at: now, updated_at: now, title: None, connector_uid: scroll_id, // Use scroll_id as placeholder native_session_id: None, agent_id: None, parent_scroll_id: None, continuation: None, tags: Vec::new(), metadata: serde_json::json!({}), no_update: false, kind: SessionKind::AcpConnection, acp_client_id: None, is_connected: None, current_session_id: None, models: None, modes: None, config_options: None, completeness: SessionCompleteness::default(), matrix_room_id: None, matrix_sharing_active: false, matrix_shared_at: None, is_subagent: false, subagent_type: None, spawning_tool_use_id: None, } } }; write_json(&session_json_path, &session_metadata).await?; Ok(()) } async fn get_meta_events(&self, scroll_id: Uuid) -> Result> { let events_path = self.paths.events_path(scroll_id); // Read events from events.jsonl let mut events: Vec = read_ndjson(&events_path) .await .unwrap_or_else(|_| Vec::new()); // Sort by timestamp then event_id for stable ordering events.sort_by(|a, b| { a.ts.cmp(&b.ts).then_with(|| a.event_id.cmp(&b.event_id)) }); Ok(events) } async fn update_meta_session_status( &self, scroll_id: Uuid, is_connected: bool, current_session_id: Option, ) -> Result<()> { // Load existing session metadata let session_json_path = self.paths.session_json(scroll_id); let mut session_metadata: SessionMetadata = match read_json(&session_json_path).await { Ok(metadata) => metadata, Err(e) if e.kind() == std::io::ErrorKind::NotFound => { return Err(ArchivistError::SessionUnknown(scroll_id)); } Err(e) => return Err(e.into()), }; // Update connection status fields session_metadata.is_connected = Some(is_connected); session_metadata.current_session_id = current_session_id; session_metadata.updated_at = Utc::now(); // Write updated metadata back to disk write_json(&session_json_path, &session_metadata).await?; tracing::info!( scroll_id = %scroll_id, is_connected = %is_connected, current_session_id = ?current_session_id, "Updated meta session status" ); Ok(()) } async fn list_meta_sessions(&self) -> Result> { // Scan .contexts/ directory for all session.json files let contexts_dir = self.paths.root().join(".contexts"); if !contexts_dir.exists() { return Ok(Vec::new()); } let mut meta_sessions = Vec::new(); // Read all session directories let mut entries = tokio::fs::read_dir(&contexts_dir).await?; while let Some(entry) = entries.next_entry().await? { if !entry.file_type().await?.is_dir() { continue; } let session_json_path = entry.path().join("session.json"); // Try to read session.json match read_json::(&session_json_path).await { Ok(metadata) => { // Filter to only AcpConnection sessions if metadata.kind == SessionKind::AcpConnection { meta_sessions.push(metadata); } } Err(e) if e.kind() == std::io::ErrorKind::NotFound => { // Skip missing session files continue; } Err(e) => { tracing::warn!( path = ?session_json_path, error = %e, "Failed to read session.json while listing meta sessions" ); continue; } } } // Sort by updated_at descending (newest first) meta_sessions.sort_by(|a, b| b.updated_at.cmp(&a.updated_at)); Ok(meta_sessions) } async fn find_meta_session_by_client( &self, client_id: &str, ) -> Result> { // Use list_meta_sessions and filter by acp_client_id let meta_sessions = self.list_meta_sessions().await?; let result = meta_sessions .into_iter() .find(|session| { session.acp_client_id.as_deref() == Some(client_id) }); Ok(result) } }