use crate::models::*; use crate::store::{Store, read_json_file, write_json_atomic}; use crate::search::MemoryIndex; use std::collections::{HashMap, HashSet}; use std::fs; use std::path::PathBuf; use std::sync::RwLock; use std::time::{Duration, SystemTime}; pub struct MemoryState { pub base_dir: PathBuf, pub master_path: PathBuf, pub session_graph: RwLock, pub master_cache: RwLock<(KnowledgeGraph, SystemTime)>, pub search_index: RwLock, pub ledger: Store>, pub sticky: Store>, pub tasks: Store>, pub snippets: Store>, pub adrs: Store>, pub prefs: Store>, pub error_fixes: Store>, pub pinned_files: Store>, pub session_summaries: Store>, pub handoff_memos: Store>, pub env_fingerprints: Store>, pub env_requirements: Store>, pub milestones: Store>, pub environments: Store>, pub pr_checklists: Store>, pub tech_debts: Store>, pub gates: Store>, pub context_workspaces: Store>, } impl MemoryState { fn master_mtime(&self) -> SystemTime { fs::metadata(&self.master_path) .and_then(|m| m.modified()) .unwrap_or(SystemTime::UNIX_EPOCH) } pub fn unique_items(input: Vec) -> Vec { let mut keys = HashSet::new(); let mut list = Vec::new(); for entry in input { if keys.insert(entry.clone()) { list.push(entry); } } list } pub fn merge_graphs(dest: &mut KnowledgeGraph, src: &KnowledgeGraph) { for (name, src_ent) in &src.entities { let dest_ent = dest .entities .entry(name.clone()) .or_insert_with(|| src_ent.clone()); if dest_ent.name == src_ent.name { dest_ent.observations.extend(src_ent.observations.clone()); dest_ent.observations = Self::unique_items(dest_ent.observations.clone()); } } dest.relations.extend(src.relations.clone()); dest.relations = Self::unique_items(dest.relations.clone()); } pub fn read_master_cached(&self) -> KnowledgeGraph { let current_mtime = self.master_mtime(); { let lock = self.master_cache.read().unwrap(); if lock.1 == current_mtime { return lock.0.clone(); } } let mut lock = self.master_cache.write().unwrap(); let new_mtime = self.master_mtime(); if lock.1 != new_mtime { lock.0 = read_json_file(&self.master_path); lock.1 = new_mtime; } lock.0.clone() } pub fn get_full_graph(&self) -> KnowledgeGraph { let mut master = self.read_master_cached(); let wal_path = self.base_dir.join("wal.jsonl"); if let Ok(content) = std::fs::read_to_string(&wal_path) { for line in content.lines() { if let Ok(d) = serde_json::from_str::(line) { Self::merge_graphs(&mut master, &d); } } } let session_graph = self.session_graph.read().unwrap(); Self::merge_graphs(&mut master, &session_graph); master } pub fn write_to_local_delta(&self, update_fn: F) { let mut session_graph = self.session_graph.write().unwrap(); update_fn(&mut session_graph); let wal_path = self.base_dir.join("wal.jsonl"); if let Ok(payload) = serde_json::to_string(&*session_graph) { if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(&wal_path) { use std::io::Write; let _ = writeln!(file, "{}", payload); } } } pub fn apply_sync_write(&self, update_fn: F) { let lock_path = self.base_dir.join("master.lock"); let mut attempts = 0; loop { if fs::OpenOptions::new() .create_new(true) .write(true) .open(&lock_path) .is_ok() { break; } if attempts > 100 { let _ = fs::remove_file(&lock_path); } attempts += 1; std::thread::sleep(Duration::from_millis(50)); } let mut master = self.get_full_graph(); let wal_path = self.base_dir.join("wal.jsonl"); let _ = fs::remove_file(&wal_path); *self.session_graph.write().unwrap() = KnowledgeGraph::default(); update_fn(&mut master); let _ = write_json_atomic(&self.master_path, &master); let mut cache_lock = self.master_cache.write().unwrap(); cache_lock.0 = master; cache_lock.1 = self.master_mtime(); let _ = fs::remove_file(&lock_path); } pub fn rebuild_index(&self) { if let Ok(new_idx) = MemoryIndex::new() { let full = self.get_full_graph(); for (_, e) in &full.entities { let _ = new_idx.index_entity(e); } for t in self.tasks.read() { let _ = new_idx.index_task(&t); } for s in self.snippets.read() { let _ = new_idx.index_snippet(&s); } for a in self.adrs.read() { let _ = new_idx.index_adr(&a); } if let Ok(mut w) = self.search_index.write() { *w = new_idx; } } } }