// session.rs — Session state and transcript info // // Session: per-session state (seen set, state directory) across hook // invocations. Created from hook JSON input or POC_SESSION_ID env var. // // TranscriptInfo: cached metadata about the current session's transcript // file — size, path, compaction offset. Read once, used by all callers. use std::collections::HashSet; use std::fs; use std::path::PathBuf; pub struct HookSession { pub session_id: String, pub transcript_path: String, pub hook_event: String, pub state_dir: PathBuf, } impl HookSession { fn sessions_dir() -> PathBuf { let dir = dirs::home_dir().unwrap_or_default().join(".consciousness/sessions"); fs::create_dir_all(&dir).ok(); dir } pub fn from_json(input: &str) -> Option { let state_dir = Self::sessions_dir(); let json: serde_json::Value = serde_json::from_str(input).ok()?; let session_id = json["session_id"].as_str().unwrap_or("").to_string(); if session_id.is_empty() { return None; } let transcript_path = json["transcript_path"].as_str().unwrap_or("").to_string(); let hook_event = json["hook_event_name"].as_str().unwrap_or("").to_string(); Some(HookSession { session_id, transcript_path, hook_event, state_dir }) } pub fn path(&self, prefix: &str) -> PathBuf { self.state_dir.join(format!("{}-{}", prefix, self.session_id)) } /// Construct directly from fields. pub fn from_fields(session_id: String, transcript_path: String, hook_event: String) -> Self { HookSession { state_dir: Self::sessions_dir(), session_id, transcript_path, hook_event, } } /// Load from a session ID string pub fn from_id(session_id: String) -> Option { if session_id.is_empty() { return None; } let state_dir = Self::sessions_dir(); Some(HookSession { session_id, transcript_path: String::new(), hook_event: String::new(), state_dir, }) } /// Load from POC_SESSION_ID environment variable pub fn from_env() -> Option { Self::from_id(std::env::var("POC_SESSION_ID").ok()?) } /// Get the seen set for this session pub fn seen(&self) -> HashSet { super::claude::hook::load_seen(&self.state_dir, &self.session_id) } /// Get transcript metadata, resolving the path if needed. pub fn transcript(&self) -> TranscriptInfo { if !self.transcript_path.is_empty() { return TranscriptInfo::from_path(&self.transcript_path); } // Find transcript by session ID in projects dir let projects = crate::config::get().projects_dir.clone(); if let Ok(dirs) = fs::read_dir(&projects) { for dir in dirs.filter_map(|e| e.ok()) { let path = dir.path().join(format!("{}.jsonl", self.session_id)); if path.exists() { return TranscriptInfo::from_path(&path.to_string_lossy()); } } } TranscriptInfo::empty() } } /// Cached transcript metadata — read once, use everywhere. pub struct TranscriptInfo { pub path: String, pub size: u64, } impl TranscriptInfo { pub fn from_path(path: &str) -> Self { let size = fs::metadata(path).map(|m| m.len()).unwrap_or(0); TranscriptInfo { path: path.to_string(), size, } } pub fn empty() -> Self { TranscriptInfo { path: String::new(), size: 0, } } pub fn exists(&self) -> bool { !self.path.is_empty() && self.size > 0 } }