Files
dirigent/crates/dirigent_archivist/src/backends/jsonl/meta.rs
T
2026-05-08 01:59:04 +02:00

201 lines
6.7 KiB
Rust

//! `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<MetaEventRecord>,
) -> 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::<SessionMetadata>(&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<Vec<MetaEventRecord>> {
let events_path = self.paths.events_path(scroll_id);
// Read events from events.jsonl
let mut events: Vec<MetaEventRecord> = 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<Uuid>,
) -> 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<Vec<SessionMetadata>> {
// 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::<SessionMetadata>(&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<Option<SessionMetadata>> {
// 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)
}
}