use std::fs::File; use std::io::{BufRead, BufReader}; use std::path::Path; use serde_json::Value; use crate::openclaw_config::get_openclaw_dir; use crate::session_manager::{SessionMessage, SessionMeta}; use super::utils::{extract_text, parse_timestamp_to_ms, path_basename, truncate_summary}; const PROVIDER_ID: &str = "openclaw"; pub fn scan_sessions() -> Vec { let agents_dir = get_openclaw_dir().join("agents"); if !agents_dir.exists() { return Vec::new(); } let mut sessions = Vec::new(); // Traverse each agent directory let agent_entries = match std::fs::read_dir(&agents_dir) { Ok(entries) => entries, Err(_) => return sessions, }; for agent_entry in agent_entries.flatten() { let agent_path = agent_entry.path(); if !agent_path.is_dir() { continue; } let sessions_dir = agent_path.join("sessions"); if !sessions_dir.is_dir() { continue; } let session_entries = match std::fs::read_dir(&sessions_dir) { Ok(entries) => entries, Err(_) => continue, }; for entry in session_entries.flatten() { let path = entry.path(); if path.extension().and_then(|ext| ext.to_str()) != Some("jsonl") { continue; } // Skip sessions.json index file if path .file_name() .and_then(|n| n.to_str()) .map(|n| n == "sessions.json") .unwrap_or(false) { continue; } if let Some(meta) = parse_session(&path) { sessions.push(meta); } } } sessions } pub fn load_messages(path: &Path) -> Result, String> { let file = File::open(path).map_err(|e| format!("Failed to open session file: {e}"))?; let reader = BufReader::new(file); let mut messages = Vec::new(); for line in reader.lines() { let line = match line { Ok(value) => value, Err(_) => continue, }; let value: Value = match serde_json::from_str(&line) { Ok(parsed) => parsed, Err(_) => continue, }; if value.get("type").and_then(Value::as_str) != Some("message") { continue; } let message = match value.get("message") { Some(msg) => msg, None => continue, }; let raw_role = message .get("role") .and_then(Value::as_str) .unwrap_or("unknown"); // Map OpenClaw roles to our standard roles let role = match raw_role { "toolResult" => "tool".to_string(), other => other.to_string(), }; let content = message.get("content").map(extract_text).unwrap_or_default(); if content.trim().is_empty() { continue; } let ts = value.get("timestamp").and_then(parse_timestamp_to_ms); messages.push(SessionMessage { role, content, ts }); } Ok(messages) } fn parse_session(path: &Path) -> Option { let file = File::open(path).ok()?; let reader = BufReader::new(file); let mut session_id: Option = None; let mut cwd: Option = None; let mut created_at: Option = None; let mut last_active_at: Option = None; let mut summary: Option = None; for line in reader.lines() { let line = match line { Ok(value) => value, Err(_) => continue, }; let value: Value = match serde_json::from_str(&line) { Ok(parsed) => parsed, Err(_) => continue, }; if let Some(ts) = value.get("timestamp").and_then(parse_timestamp_to_ms) { if created_at.is_none() { created_at = Some(ts); } last_active_at = Some(ts); } let event_type = value.get("type").and_then(Value::as_str).unwrap_or(""); if event_type == "session" { if session_id.is_none() { session_id = value .get("id") .and_then(Value::as_str) .map(|s| s.to_string()); } if cwd.is_none() { cwd = value .get("cwd") .and_then(Value::as_str) .map(|s| s.to_string()); } if let Some(ts) = value.get("timestamp").and_then(parse_timestamp_to_ms) { created_at.get_or_insert(ts); } continue; } if event_type != "message" { continue; } // Extract first message content for summary if summary.is_some() { continue; } let message = match value.get("message") { Some(msg) => msg, None => continue, }; let text = message.get("content").map(extract_text).unwrap_or_default(); if text.trim().is_empty() { continue; } summary = Some(text); } // Fall back to filename as session ID let session_id = session_id.or_else(|| { path.file_stem() .and_then(|s| s.to_str()) .map(|s| s.to_string()) }); let session_id = session_id?; let title = cwd .as_deref() .and_then(path_basename) .map(|s| s.to_string()); let summary = summary.map(|text| truncate_summary(&text, 160)); Some(SessionMeta { provider_id: PROVIDER_ID.to_string(), session_id: session_id.clone(), title, summary, project_dir: cwd, created_at, last_active_at, source_path: Some(path.to_string_lossy().to_string()), resume_command: None, // OpenClaw sessions are gateway-managed, no CLI resume }) }