use anyhow::{Context, Result};
use rusqlite::{Connection, params};
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
#[derive(Clone)]
pub struct GlobalMemoryStore {
conn: Arc<Mutex<Connection>>,
pub db_path: PathBuf,
}
impl std::fmt::Debug for GlobalMemoryStore {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("GlobalMemoryStore")
.field("db_path", &self.db_path)
.finish()
}
}
impl GlobalMemoryStore {
pub fn open(db_path: &Path) -> Result<Self> {
if let Some(parent) = db_path.parent() {
if !parent.exists() {
std::fs::create_dir_all(parent)?;
}
}
let conn = Connection::open(db_path)
.with_context(|| format!("Failed to open global memory database at {:?}", db_path))?;
crate::memory::auto_memory::configure_connection(&conn)?;
let store = Self {
conn: Arc::new(Mutex::new(conn)),
db_path: db_path.to_path_buf(),
};
store.init_schema()?;
Ok(store)
}
fn init_schema(&self) -> Result<()> {
let conn = self.conn.lock().unwrap();
conn.execute_batch(
"CREATE TABLE IF NOT EXISTS global_memories (
id TEXT PRIMARY KEY,
type TEXT NOT NULL,
name TEXT NOT NULL,
description TEXT NOT NULL,
body TEXT NOT NULL,
confidence REAL NOT NULL DEFAULT 1.0,
status TEXT NOT NULL DEFAULT 'active',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
last_seen TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_global_memories_status
ON global_memories(status);
",
)?;
Ok(())
}
pub fn read_index(&self) -> Result<String> {
let conn = self.conn.lock().unwrap();
let mut stmt = conn.prepare(
"SELECT name, type, description, confidence
FROM global_memories
WHERE status = 'active'
ORDER BY type, name",
)?;
let rows: Vec<(String, String, String, f64)> = stmt
.query_map([], |row| {
Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?))
})?
.filter_map(|r| r.ok())
.collect();
if rows.is_empty() {
return Ok(String::new());
}
let mut content = String::from("# Global Memory Index\n\n");
for (name, mem_type, description, confidence) in &rows {
content.push_str(&format!(
"- **{}** (`{}`) — {} _(conf: {:.2})_\n",
name, mem_type, description, confidence
));
}
Ok(content)
}
pub fn write_from_markdown(&self, markdown: &str) -> Result<()> {
let conn = self.conn.lock().unwrap();
conn.execute("DELETE FROM global_memories", [])?;
let now = crate::memory::auto_memory::now_iso();
let mut id_counter = 0u32;
for line in markdown.lines() {
let trimmed = line.trim();
if let Some(rest) = trimmed.strip_prefix("- **") {
if let Some(end_name) = rest.find("**") {
let name = &rest[..end_name];
let after_name = &rest[end_name + 2..];
if let Some(start_type) = after_name.find("(`") {
if let Some(end_type) = after_name[start_type + 2..].find("`)") {
let mem_type = &after_name[start_type + 2..start_type + 2 + end_type];
let description = after_name[start_type + 2 + end_type + 2..]
.trim_start_matches('—')
.trim();
id_counter += 1;
let id = format!("global_{}", id_counter);
conn.execute(
"INSERT OR REPLACE INTO global_memories
(id, type, name, description, body, confidence, status,
created_at, updated_at, last_seen)
VALUES (?1, ?2, ?3, ?4, ?5, 1.0, 'active', ?6, ?6, ?6)",
params![id, mem_type, name, description, description, now,],
)?;
}
}
}
}
}
Ok(())
}
pub fn count_active(&self) -> Result<usize> {
let conn = self.conn.lock().unwrap();
let count: i64 = conn.query_row(
"SELECT COUNT(*) FROM global_memories WHERE status = 'active'",
[],
|row| row.get(0),
)?;
Ok(count as usize)
}
pub fn mark_stale(&self, stale_days: u32) -> Result<usize> {
let conn = self.conn.lock().unwrap();
let now = crate::memory::auto_memory::now_iso();
let cutoff = crate::memory::auto_memory::stale_iso_cutoff(stale_days);
let count = conn.execute(
"UPDATE global_memories
SET status = 'needs_review', updated_at = ?1
WHERE status = 'active'
AND last_seen < ?2",
params![now, cutoff],
)?;
Ok(count)
}
}