use anyhow::{Context, Result};
use rusqlite::{OptionalExtension, params};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::fmt;
use std::str::FromStr;
use crate::inspect::now_unix;
use crate::store::Store;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MemoryKind {
Fact,
Preference,
Goal,
Constraint,
TaskState,
Observation,
Hypothesis,
}
impl MemoryKind {
pub fn as_str(self) -> &'static str {
match self {
MemoryKind::Fact => "fact",
MemoryKind::Preference => "preference",
MemoryKind::Goal => "goal",
MemoryKind::Constraint => "constraint",
MemoryKind::TaskState => "task_state",
MemoryKind::Observation => "observation",
MemoryKind::Hypothesis => "hypothesis",
}
}
}
impl fmt::Display for MemoryKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl FromStr for MemoryKind {
type Err = String;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s {
"fact" => Ok(MemoryKind::Fact),
"preference" => Ok(MemoryKind::Preference),
"goal" => Ok(MemoryKind::Goal),
"constraint" => Ok(MemoryKind::Constraint),
"task_state" | "task-state" => Ok(MemoryKind::TaskState),
"observation" => Ok(MemoryKind::Observation),
"hypothesis" => Ok(MemoryKind::Hypothesis),
other => Err(format!("unknown memory kind: {other}")),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MemoryStatus {
Active,
Archived,
Superseded,
}
impl MemoryStatus {
pub fn as_str(self) -> &'static str {
match self {
MemoryStatus::Active => "active",
MemoryStatus::Archived => "archived",
MemoryStatus::Superseded => "superseded",
}
}
}
impl fmt::Display for MemoryStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl FromStr for MemoryStatus {
type Err = String;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s {
"active" => Ok(MemoryStatus::Active),
"archived" => Ok(MemoryStatus::Archived),
"superseded" => Ok(MemoryStatus::Superseded),
other => Err(format!("unknown memory status: {other}")),
}
}
}
#[derive(Debug, Clone)]
pub struct AddMemoryOptions {
pub kind: MemoryKind,
pub scope: String,
pub content: String,
pub priority: Option<i64>,
pub urgency: Option<i64>,
pub confidence: Option<f64>,
pub source_refs: Vec<String>,
}
#[derive(Debug, Clone, Default)]
pub struct ListMemoryOptions {
pub kind: Option<MemoryKind>,
pub scope: Option<String>,
pub status: Option<MemoryStatus>,
pub limit: Option<usize>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct MemoryRecord {
pub id: String,
pub kind: MemoryKind,
pub scope: String,
pub content: String,
pub priority: i64,
pub urgency: i64,
pub confidence: f64,
pub status: MemoryStatus,
pub source_refs: Vec<String>,
pub created_at: i64,
pub updated_at: i64,
}
#[derive(Debug, Clone, Serialize)]
pub struct MemoryListReport {
pub records: Vec<MemoryRecord>,
pub total: usize,
}
fn clamp_priority(n: i64) -> i64 {
n.clamp(0, 100)
}
fn clamp_confidence(n: f64) -> f64 {
n.clamp(0.0, 1.0)
}
fn source_refs_json(source_refs: &[String]) -> Result<String> {
serde_json::to_string(source_refs).context("serializing memory source refs")
}
fn make_id(
conn: &rusqlite::Connection,
opts: &AddMemoryOptions,
created_at: i64,
) -> Result<String> {
let existing: i64 =
conn.query_row("SELECT COUNT(*) FROM memory_records", [], |row| row.get(0))?;
let mut hasher = Sha256::new();
hasher.update(opts.kind.as_str());
hasher.update(b"\0");
hasher.update(opts.scope.as_bytes());
hasher.update(b"\0");
hasher.update(opts.content.as_bytes());
hasher.update(b"\0");
hasher.update(created_at.to_string().as_bytes());
hasher.update(b"\0");
hasher.update(existing.to_string().as_bytes());
let digest = hasher.finalize();
Ok(hex::encode(&digest[..16]))
}
pub fn add_memory(store: &Store, opts: AddMemoryOptions) -> Result<MemoryRecord> {
let conn = store.conn();
let created_at = now_unix();
let priority = clamp_priority(opts.priority.unwrap_or(50));
let urgency = clamp_priority(opts.urgency.unwrap_or(0));
let confidence = clamp_confidence(opts.confidence.unwrap_or(1.0));
let refs_json = source_refs_json(&opts.source_refs)?;
let id = make_id(conn, &opts, created_at)?;
conn.execute(
"INSERT INTO memory_records
(id, kind, scope, content, priority, urgency, confidence, status, source_refs_json, created_at, updated_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, 'active', ?8, ?9, ?9)",
params![
id,
opts.kind.as_str(),
opts.scope,
opts.content,
priority,
urgency,
confidence,
refs_json,
created_at
],
)
.context("inserting memory record")?;
get_memory(store, &id)?.ok_or_else(|| anyhow::anyhow!("memory {id} disappeared after insert"))
}
pub fn get_memory(store: &Store, id: &str) -> Result<Option<MemoryRecord>> {
store
.conn()
.query_row(
"SELECT id, kind, scope, content, priority, urgency, confidence, status, source_refs_json, created_at, updated_at
FROM memory_records WHERE id = ?1",
params![id],
row_to_memory,
)
.optional()
.map_err(Into::into)
}
pub fn list_memories(store: &Store, opts: ListMemoryOptions) -> Result<MemoryListReport> {
let kind = opts.kind.map(|k| k.as_str().to_string());
let status = opts.status.map(|s| s.as_str().to_string());
let limit = opts.limit.unwrap_or(50).min(i64::MAX as usize) as i64;
let mut stmt = store.conn().prepare(
"SELECT id, kind, scope, content, priority, urgency, confidence, status, source_refs_json, created_at, updated_at
FROM memory_records
WHERE (?1 IS NULL OR kind = ?1)
AND (?2 IS NULL OR scope = ?2)
AND (?3 IS NULL OR status = ?3)
ORDER BY priority DESC, urgency DESC, updated_at DESC, id ASC
LIMIT ?4",
)?;
let rows = stmt.query_map(params![kind, opts.scope, status, limit], row_to_memory)?;
let records = rows.collect::<std::result::Result<Vec<_>, _>>()?;
let total = records.len();
Ok(MemoryListReport { records, total })
}
pub fn archive_memory(store: &Store, id: &str) -> Result<MemoryRecord> {
let updated_at = now_unix();
let rows = store.conn().execute(
"UPDATE memory_records SET status = 'archived', updated_at = ?1 WHERE id = ?2",
params![updated_at, id],
)?;
if rows == 0 {
anyhow::bail!("no memory with id {id}");
}
get_memory(store, id)?.ok_or_else(|| anyhow::anyhow!("memory {id} disappeared after archive"))
}
fn row_to_memory(row: &rusqlite::Row<'_>) -> rusqlite::Result<MemoryRecord> {
let kind_raw: String = row.get(1)?;
let status_raw: String = row.get(7)?;
let refs_raw: String = row.get(8)?;
let kind = MemoryKind::from_str(&kind_raw).map_err(|e| {
rusqlite::Error::FromSqlConversionFailure(
1,
rusqlite::types::Type::Text,
Box::new(std::io::Error::new(std::io::ErrorKind::InvalidData, e)),
)
})?;
let status = MemoryStatus::from_str(&status_raw).map_err(|e| {
rusqlite::Error::FromSqlConversionFailure(
7,
rusqlite::types::Type::Text,
Box::new(std::io::Error::new(std::io::ErrorKind::InvalidData, e)),
)
})?;
let source_refs = serde_json::from_str(&refs_raw).map_err(|e| {
rusqlite::Error::FromSqlConversionFailure(8, rusqlite::types::Type::Text, Box::new(e))
})?;
Ok(MemoryRecord {
id: row.get(0)?,
kind,
scope: row.get(2)?,
content: row.get(3)?,
priority: row.get(4)?,
urgency: row.get(5)?,
confidence: row.get(6)?,
status,
source_refs,
created_at: row.get(9)?,
updated_at: row.get(10)?,
})
}
pub fn format_record_text(record: &MemoryRecord) -> String {
format!(
"memory {} kind={} scope={} status={} priority={} urgency={} confidence={:.3} updated_at={} content={}",
record.id,
record.kind,
record.scope,
record.status,
record.priority,
record.urgency,
record.confidence,
record.updated_at,
record.content
)
}
pub fn print_record_text(record: &MemoryRecord) {
println!("{}", format_record_text(record));
}
pub fn print_list_text(report: &MemoryListReport) {
for record in &report.records {
println!("{}", format_record_text(record));
}
}
pub fn print_json<T: Serialize>(value: &T) -> Result<()> {
println!("{}", serde_json::to_string_pretty(value)?);
Ok(())
}