use std::fs;
use std::path::PathBuf;
use kimetsu_core::KimetsuResult;
use kimetsu_core::ids::RunId;
use kimetsu_core::memory::{MemoryKind, MemoryScope, normalize_memory_text};
use kimetsu_core::paths::{user_brain_db_path, user_brain_enabled, user_kimetsu_dir};
use rusqlite::{Connection, OpenFlags, OptionalExtension};
use time::OffsetDateTime;
use ulid::Ulid;
use crate::conflict;
use crate::embeddings;
use crate::project::MemoryRow;
use crate::redact;
use crate::schema;
pub fn open_user_brain() -> KimetsuResult<Option<Connection>> {
if !user_brain_enabled() {
return Ok(None);
}
let Some(dir) = user_kimetsu_dir() else {
return Ok(None);
};
fs::create_dir_all(&dir)?;
let db_path = dir.join("brain.db");
let conn = Connection::open(&db_path)?;
schema::initialize(&conn)?;
Ok(Some(conn))
}
pub fn open_user_brain_readonly() -> KimetsuResult<Option<Connection>> {
if !user_brain_enabled() {
return Ok(None);
}
let Some(db_path) = user_brain_db_path() else {
return Ok(None);
};
if !db_path.exists() {
return Ok(None);
}
let conn = Connection::open_with_flags(&db_path, OpenFlags::SQLITE_OPEN_READ_ONLY)?;
schema::validate(&conn)?;
Ok(Some(conn))
}
pub fn user_brain_path() -> Option<PathBuf> {
user_brain_db_path()
}
pub fn add_user_memory(
conn: &Connection,
kind: MemoryKind,
text: &str,
confidence: f32,
) -> KimetsuResult<String> {
let redaction = redact::redact_secrets(text);
if redaction.was_redacted() {
eprintln!("kimetsu-brain (user): {}", redaction.summary());
}
let text = redaction.text.as_str();
let normalized = normalize_memory_text(text);
let existing: Option<String> = conn
.query_row(
"
SELECT memory_id FROM memories
WHERE scope = ?1 AND kind = ?2 AND normalized_text = ?3
AND invalidated_at IS NULL
LIMIT 1
",
rusqlite::params!["global_user".to_string(), kind.to_string(), &normalized],
|row| row.get::<_, String>(0),
)
.optional()?;
if let Some(existing_id) = existing {
return Ok(existing_id);
}
let memory_id = Ulid::new().to_string();
let created_at = OffsetDateTime::now_utc()
.format(&time::format_description::well_known::Rfc3339)
.map_err(|e| format!("timestamp format: {e}"))?;
let provenance = serde_json::json!({
"source": "user_brain",
"run_id": RunId::new().to_string(),
"text": text,
})
.to_string();
conn.execute(
"
INSERT INTO memories (
memory_id, scope, kind, text, normalized_text,
confidence, provenance_snapshot_json, created_at,
use_count, usefulness_score
)
VALUES (?1, 'global_user', ?2, ?3, ?4, ?5, ?6, ?7, 0, 0.0)
",
rusqlite::params![
memory_id,
kind.to_string(),
text,
normalized,
confidence,
provenance,
created_at,
],
)?;
conn.execute(
"
INSERT INTO memories_fts (memory_id, text, kind, scope)
VALUES (?1, ?2, ?3, 'global_user')
",
rusqlite::params![memory_id, text, kind.to_string()],
)?;
let embedder = embeddings::open_default_embedder();
embeddings::embed_and_persist(conn, &memory_id, text, embedder)?;
let conflicts = conflict::detect_and_record(
conn,
&memory_id,
&MemoryScope::GlobalUser,
&kind.to_string(),
text,
embedder,
);
if conflicts > 0 {
eprintln!(
"kimetsu-brain (user): memory {memory_id} conflicts with {conflicts} existing memor{} (run `kimetsu brain memory conflicts` to review)",
if conflicts == 1 { "y" } else { "ies" }
);
}
Ok(memory_id)
}
pub fn list_user_memories(conn: &Connection) -> KimetsuResult<Vec<MemoryRow>> {
let mut stmt = conn.prepare(
"
SELECT memory_id, scope, kind, text, confidence, use_count, usefulness_score
FROM memories
WHERE invalidated_at IS NULL
ORDER BY created_at DESC
LIMIT 100
",
)?;
let rows = stmt.query_map([], |row| {
Ok(MemoryRow {
memory_id: row.get(0)?,
scope: row.get(1)?,
kind: row.get(2)?,
text: row.get(3)?,
confidence: row.get(4)?,
use_count: row.get(5)?,
usefulness_score: row.get::<_, f64>(6)? as f32,
})
})?;
let mut memories = Vec::new();
for row in rows {
memories.push(row?);
}
Ok(memories)
}
#[doc(hidden)]
pub fn test_env_lock() -> &'static std::sync::Mutex<()> {
static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
&LOCK
}
#[doc(hidden)]
pub fn with_user_brain_disabled<R>(f: impl FnOnce() -> R) -> R {
let _guard = test_env_lock().lock().unwrap_or_else(|p| p.into_inner());
let prev_enabled = std::env::var("KIMETSU_USER_BRAIN").ok();
let prev_dir = std::env::var("KIMETSU_USER_BRAIN_DIR").ok();
unsafe {
std::env::set_var("KIMETSU_USER_BRAIN", "0");
std::env::remove_var("KIMETSU_USER_BRAIN_DIR");
}
let out = f();
unsafe {
match prev_enabled {
Some(v) => std::env::set_var("KIMETSU_USER_BRAIN", v),
None => std::env::remove_var("KIMETSU_USER_BRAIN"),
}
match prev_dir {
Some(v) => std::env::set_var("KIMETSU_USER_BRAIN_DIR", v),
None => std::env::remove_var("KIMETSU_USER_BRAIN_DIR"),
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn with_user_brain_at(dir: &std::path::Path, f: impl FnOnce()) {
let _guard = test_env_lock().lock().unwrap_or_else(|p| p.into_inner());
let prev_dir = std::env::var("KIMETSU_USER_BRAIN_DIR").ok();
let prev_enabled = std::env::var("KIMETSU_USER_BRAIN").ok();
unsafe {
std::env::set_var("KIMETSU_USER_BRAIN_DIR", dir);
std::env::remove_var("KIMETSU_USER_BRAIN");
}
f();
unsafe {
match prev_dir {
Some(v) => std::env::set_var("KIMETSU_USER_BRAIN_DIR", v),
None => std::env::remove_var("KIMETSU_USER_BRAIN_DIR"),
}
match prev_enabled {
Some(v) => std::env::set_var("KIMETSU_USER_BRAIN", v),
None => std::env::remove_var("KIMETSU_USER_BRAIN"),
}
}
}
#[test]
fn open_user_brain_creates_db_on_first_call() {
let tmp = tempdir_in_test("kimetsu-user-brain-1");
with_user_brain_at(&tmp, || {
let conn = open_user_brain()
.expect("open ok")
.expect("user brain enabled");
let count: i64 = conn
.query_row("SELECT COUNT(*) FROM memories", [], |row| row.get(0))
.expect("query memories");
assert_eq!(count, 0);
assert!(tmp.join("brain.db").exists());
});
}
#[test]
fn open_user_brain_returns_none_when_disabled() {
let tmp = tempdir_in_test("kimetsu-user-brain-2");
let _guard = test_env_lock().lock().unwrap_or_else(|p| p.into_inner());
let prev_enabled = std::env::var("KIMETSU_USER_BRAIN").ok();
let prev_dir = std::env::var("KIMETSU_USER_BRAIN_DIR").ok();
unsafe {
std::env::set_var("KIMETSU_USER_BRAIN", "0");
std::env::set_var("KIMETSU_USER_BRAIN_DIR", &tmp);
}
let result = open_user_brain().expect("open ok");
assert!(result.is_none(), "disabled should short-circuit to None");
assert!(!tmp.join("brain.db").exists());
unsafe {
match prev_dir {
Some(v) => std::env::set_var("KIMETSU_USER_BRAIN_DIR", v),
None => std::env::remove_var("KIMETSU_USER_BRAIN_DIR"),
}
match prev_enabled {
Some(v) => std::env::set_var("KIMETSU_USER_BRAIN", v),
None => std::env::remove_var("KIMETSU_USER_BRAIN"),
}
}
}
#[test]
fn open_user_brain_readonly_returns_none_before_first_write() {
let tmp = tempdir_in_test("kimetsu-user-brain-3");
with_user_brain_at(&tmp, || {
let result = open_user_brain_readonly().expect("open ok");
assert!(result.is_none(), "missing file -> None for readonly path");
});
}
#[test]
fn add_user_memory_persists_and_dedups() {
let tmp = tempdir_in_test("kimetsu-user-brain-4");
with_user_brain_at(&tmp, || {
let conn = open_user_brain().expect("open").expect("enabled");
let first =
add_user_memory(&conn, MemoryKind::Preference, "use thiserror", 1.0).expect("add");
let second = add_user_memory(&conn, MemoryKind::Preference, " use thiserror ", 1.0)
.expect("add normalized dup");
assert_eq!(first, second, "normalized-text dedup must hit");
let rows = list_user_memories(&conn).expect("list");
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].text, "use thiserror");
assert_eq!(rows[0].scope, "global_user");
});
}
#[test]
fn user_brain_path_resolves_from_override_env() {
let tmp = tempdir_in_test("kimetsu-user-brain-5");
with_user_brain_at(&tmp, || {
let path = user_brain_path().expect("path");
assert!(path.starts_with(&tmp));
assert!(path.ends_with("brain.db"));
});
}
fn tempdir_in_test(prefix: &str) -> std::path::PathBuf {
let dir = std::env::temp_dir().join(format!("{prefix}-{}", Ulid::new()));
std::fs::create_dir_all(&dir).expect("mkdir");
dir
}
}