201 lines
6.7 KiB
Rust
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)
|
|
}
|
|
}
|