use crate::error::Result;
use crate::store::strengths;
use crate::types::*;
use rusqlite::Connection;
const DEFAULT_DECAY_FACTOR: f32 = 0.95;
const ARCHIVE_STORAGE_THRESHOLD: f32 = 0.1;
const ARCHIVE_RETRIEVAL_THRESHOLD: f32 = 0.05;
pub fn forget(conn: &Connection) -> Result<ForgettingReport> {
let mut report = ForgettingReport {
nodes_decayed: strengths::decay_all_retrieval(conn, DEFAULT_DECAY_FACTOR)? as u32,
..Default::default()
};
let archivable =
strengths::find_archivable(conn, ARCHIVE_STORAGE_THRESHOLD, ARCHIVE_RETRIEVAL_THRESHOLD)?;
for node in &archivable {
match node {
NodeRef::Episode(id) => {
crate::store::episodic::delete_episodes(conn, &[*id])?;
}
NodeRef::Semantic(id) => {
crate::store::semantic::delete_node(conn, *id)?;
}
NodeRef::Preference(_) => {
continue;
}
NodeRef::Category(_) => {
continue;
}
}
conn.execute(
"DELETE FROM node_strengths WHERE node_type = ?1 AND node_id = ?2",
rusqlite::params![node.type_str(), node.id()],
)?;
report.nodes_archived += 1;
}
Ok(report)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::schema::open_memory_db;
use crate::store::{episodic, strengths};
#[test]
fn test_forget_empty_db() {
let conn = open_memory_db().unwrap();
let report = forget(&conn).unwrap();
assert_eq!(report.nodes_decayed, 0);
assert_eq!(report.nodes_archived, 0);
}
#[test]
fn test_decay_reduces_retrieval_strength() {
let conn = open_memory_db().unwrap();
let node = NodeRef::Episode(EpisodeId(1));
episodic::store_episode(
&conn,
&NewEpisode {
content: "test".to_string(),
role: Role::User,
session_id: "s1".to_string(),
timestamp: 1000,
context: EpisodeContext::default(),
embedding: None,
},
)
.unwrap();
strengths::init_strength(&conn, node).unwrap();
let before = strengths::get_strength(&conn, node).unwrap();
forget(&conn).unwrap();
let after = strengths::get_strength(&conn, node).unwrap();
assert!(after.retrieval_strength < before.retrieval_strength);
}
#[test]
fn test_archive_semantic_node() {
use crate::store::semantic;
let conn = open_memory_db().unwrap();
let node_id = semantic::store_semantic_node(
&conn,
&crate::types::NewSemanticNode {
content: "archivable knowledge".to_string(),
node_type: crate::types::SemanticType::Fact,
confidence: 0.5,
source_episodes: vec![],
embedding: None,
},
)
.unwrap();
let node = NodeRef::Semantic(node_id);
strengths::init_strength(&conn, node).unwrap();
conn.execute(
"UPDATE node_strengths SET storage_strength = 0.05, retrieval_strength = 0.01
WHERE node_type = 'semantic' AND node_id = ?1",
[node_id.0],
)
.unwrap();
let before = semantic::get_semantic_node(&conn, node_id);
assert!(before.is_ok(), "semantic node should exist before forget");
let report = forget(&conn).unwrap();
assert_eq!(report.nodes_archived, 1);
let after = semantic::get_semantic_node(&conn, node_id);
assert!(
after.is_err(),
"semantic node should be deleted after archive"
);
}
#[test]
fn test_forget_skips_preferences_and_categories() {
let conn = open_memory_db().unwrap();
conn.execute(
"INSERT INTO node_strengths (node_type, node_id, storage_strength, retrieval_strength, access_count, last_accessed)
VALUES ('preference', 1, 0.01, 0.001, 1, 1000)",
[],
)
.unwrap();
conn.execute(
"INSERT INTO node_strengths (node_type, node_id, storage_strength, retrieval_strength, access_count, last_accessed)
VALUES ('category', 1, 0.01, 0.001, 1, 1000)",
[],
)
.unwrap();
let report = forget(&conn).unwrap();
assert_eq!(report.nodes_archived, 0);
let pref_count: i64 = conn
.query_row(
"SELECT COUNT(*) FROM node_strengths WHERE node_type = 'preference'",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(
pref_count, 1,
"preference strength record should still exist"
);
let cat_count: i64 = conn
.query_row(
"SELECT COUNT(*) FROM node_strengths WHERE node_type = 'category'",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(cat_count, 1, "category strength record should still exist");
}
#[test]
fn test_archive_episode_node() {
let conn = open_memory_db().unwrap();
episodic::store_episode(
&conn,
&NewEpisode {
content: "soon to be forgotten".to_string(),
role: crate::types::Role::User,
session_id: "s1".to_string(),
timestamp: 1000,
context: crate::types::EpisodeContext::default(),
embedding: None,
},
)
.unwrap();
let ep_id = EpisodeId(conn.last_insert_rowid());
let node = NodeRef::Episode(ep_id);
strengths::init_strength(&conn, node).unwrap();
conn.execute(
"UPDATE node_strengths SET storage_strength = 0.05, retrieval_strength = 0.01
WHERE node_type = 'episode' AND node_id = ?1",
[ep_id.0],
)
.unwrap();
let count_before: i64 = conn
.query_row(
"SELECT COUNT(*) FROM episodes WHERE id = ?1",
[ep_id.0],
|r| r.get(0),
)
.unwrap();
assert_eq!(count_before, 1);
let report = forget(&conn).unwrap();
assert_eq!(report.nodes_archived, 1, "episode should be archived");
let count_after: i64 = conn
.query_row(
"SELECT COUNT(*) FROM episodes WHERE id = ?1",
[ep_id.0],
|r| r.get(0),
)
.unwrap();
assert_eq!(count_after, 0, "episode should be deleted after archiving");
let strength_count: i64 = conn
.query_row(
"SELECT COUNT(*) FROM node_strengths WHERE node_type = 'episode' AND node_id = ?1",
[ep_id.0],
|r| r.get(0),
)
.unwrap();
assert_eq!(
strength_count, 0,
"strength record should be removed after archive"
);
}
#[test]
fn test_forget_nodes_decayed_count() {
let conn = open_memory_db().unwrap();
let node = NodeRef::Episode(EpisodeId(1));
episodic::store_episode(
&conn,
&NewEpisode {
content: "content".to_string(),
role: crate::types::Role::User,
session_id: "s1".to_string(),
timestamp: 1000,
context: crate::types::EpisodeContext::default(),
embedding: None,
},
)
.unwrap();
strengths::init_strength(&conn, node).unwrap();
let report = forget(&conn).unwrap();
assert_eq!(
report.nodes_decayed, 1,
"one strength row should have been decayed"
);
}
}