use kimetsu_core::KimetsuResult;
use rusqlite::Connection;
use crate::context::ContextCapsule;
pub const AS_OF_PREDICATE: &str = "julianday(created_at) <= julianday(?1) \
AND (invalidated_at IS NULL OR julianday(invalidated_at) > julianday(?1)) \
AND (valid_from IS NULL OR julianday(valid_from) <= julianday(?1)) \
AND (valid_to IS NULL OR julianday(valid_to) > julianday(?1))";
pub fn as_of_predicate() -> &'static str {
AS_OF_PREDICATE
}
#[derive(Debug, Clone)]
pub struct AsOfMemory {
pub memory_id: String,
pub scope: String,
pub kind: String,
pub text: String,
pub created_at: String,
pub retired_at: Option<String>,
pub retired_reason: Option<String>,
}
pub fn memories_as_of(
conn: &Connection,
as_of: &str,
limit: u32,
) -> KimetsuResult<Vec<AsOfMemory>> {
memories_at(conn, as_of, as_of, limit)
}
pub fn memories_at(
conn: &Connection,
valid_at: &str,
known_at: &str,
limit: u32,
) -> KimetsuResult<Vec<AsOfMemory>> {
let sql = format!(
"SELECT memory_id, scope,
COALESCE((SELECT kind FROM memory_revisions r WHERE r.memory_id=m.memory_id AND julianday(r.known_at)<=julianday(?2) AND julianday(r.effective_at)<=julianday(?1) ORDER BY julianday(r.known_at) DESC,revision_id DESC LIMIT 1),kind),
COALESCE((SELECT text FROM memory_revisions r WHERE r.memory_id=m.memory_id AND julianday(r.known_at)<=julianday(?2) AND julianday(r.effective_at)<=julianday(?1) ORDER BY julianday(r.known_at) DESC,revision_id DESC LIMIT 1),text), created_at,
invalidated_at, invalidated_reason, valid_to, superseded_by
FROM memories m
WHERE julianday(created_at)<=julianday(?2)
AND (invalidated_at IS NULL OR julianday(invalidated_at)>julianday(?2))
AND (valid_from IS NULL OR julianday(valid_from)<=julianday(?1))
AND (valid_to IS NULL OR julianday(valid_to)>julianday(?1))
ORDER BY created_at DESC
{}",
if limit == 0 {
String::new()
} else {
format!("LIMIT {limit}")
}
);
let mut stmt = conn.prepare(&sql)?;
let rows = stmt
.query_map(rusqlite::params![valid_at, known_at], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, String>(1)?,
row.get::<_, String>(2)?,
row.get::<_, String>(3)?,
row.get::<_, String>(4)?,
row.get::<_, Option<String>>(5)?,
row.get::<_, Option<String>>(6)?,
row.get::<_, Option<String>>(7)?,
row.get::<_, Option<String>>(8)?,
))
})?
.collect::<Result<Vec<_>, _>>()?;
Ok(rows
.into_iter()
.map(
|(
memory_id,
scope,
kind,
text,
created_at,
invalidated_at,
invalidated_reason,
valid_to,
superseded_by,
)| {
let (retired_at, retired_reason) = match (invalidated_at, valid_to, superseded_by) {
(Some(at), _, _) => (
Some(at),
Some(invalidated_reason.unwrap_or_else(|| "invalidated".to_string())),
),
(None, Some(until), _) => (Some(until), Some("expired".to_string())),
(None, None, Some(survivor)) => (None, Some(format!("merged into {survivor}"))),
(None, None, None) => (None, None),
};
AsOfMemory {
memory_id,
scope,
kind,
text,
created_at,
retired_at,
retired_reason,
}
},
)
.collect())
}
pub fn as_of_capsules(memories: &[AsOfMemory]) -> Vec<ContextCapsule> {
memories
.iter()
.map(|m| ContextCapsule {
id: String::new(),
kind: "memory".to_string(),
summary: format!("{}:{} - {}", m.scope, m.kind, m.text),
token_estimate: (m.text.len() / 4) as u32 + 8,
expansion_handle: format!("memory:{}", m.memory_id),
provenance: Vec::new(),
confidence: 1.0,
freshness: 0.0,
relevance: 0.0,
scope_weight: 0.0,
score: 0.0,
superseded_hint: false,
rerank_policy_tier: 0,
claim_revision: None,
facts: vec![],
rerank_usefulness: None,
rerank_trust: None,
})
.collect()
}
#[derive(Debug, Clone)]
pub struct BeliefDelta {
pub learned: Vec<AsOfMemory>,
pub retired: Vec<AsOfMemory>,
}
pub fn belief_delta(conn: &Connection, from: &str, to: &str) -> KimetsuResult<BeliefDelta> {
use std::collections::HashSet;
let before = memories_as_of(conn, from, 0)?;
let after = memories_as_of(conn, to, 0)?;
let before_ids: HashSet<_> = before
.iter()
.map(|m| (&m.memory_id, &m.text, &m.kind))
.collect();
let after_ids: HashSet<_> = after
.iter()
.map(|m| (&m.memory_id, &m.text, &m.kind))
.collect();
Ok(BeliefDelta {
learned: after
.iter()
.filter(|m| !before_ids.contains(&(&m.memory_id, &m.text, &m.kind)))
.cloned()
.collect(),
retired: before
.iter()
.filter(|m| !after_ids.contains(&(&m.memory_id, &m.text, &m.kind)))
.cloned()
.collect(),
})
}
#[cfg(test)]
mod tests {
use super::*;
fn conn() -> Connection {
let conn = Connection::open_in_memory().expect("open");
crate::schema::initialize(&conn).expect("schema");
conn
}
#[allow(clippy::too_many_arguments)]
fn insert(
conn: &Connection,
id: &str,
text: &str,
created_at: &str,
invalidated_at: Option<&str>,
valid_from: Option<&str>,
valid_to: Option<&str>,
superseded_by: Option<&str>,
) {
conn.execute(
"INSERT INTO memories
(memory_id, scope, kind, text, normalized_text, confidence,
provenance_snapshot_json, created_at, invalidated_at,
valid_from, valid_to, superseded_by)
VALUES (?1, 'project', 'fact', ?2, ?2, 0.9, '{}', ?3, ?4, ?5, ?6, ?7)",
rusqlite::params![
id,
text,
created_at,
invalidated_at,
valid_from,
valid_to,
superseded_by
],
)
.expect("insert");
}
fn ids(memories: &[AsOfMemory]) -> Vec<&str> {
let mut v: Vec<&str> = memories.iter().map(|m| m.memory_id.as_str()).collect();
v.sort_unstable();
v
}
#[test]
fn a_memory_written_later_is_not_in_the_past_view() {
let c = conn();
insert(
&c,
"early",
"a",
"2026-01-01T00:00:00Z",
None,
None,
None,
None,
);
insert(
&c,
"late",
"b",
"2026-06-01T00:00:00Z",
None,
None,
None,
None,
);
assert_eq!(
ids(&memories_as_of(&c, "2026-03-01T00:00:00Z", 0).unwrap()),
vec!["early"]
);
assert_eq!(
ids(&memories_as_of(&c, "2026-09-01T00:00:00Z", 0).unwrap()),
vec!["early", "late"]
);
}
#[test]
fn a_retracted_memory_is_still_visible_before_its_retraction() {
let c = conn();
insert(
&c,
"retracted",
"the schema is v10",
"2026-01-01T00:00:00Z",
Some("2026-05-01T00:00:00Z"),
None,
None,
None,
);
let before = memories_as_of(&c, "2026-03-01T00:00:00Z", 0).unwrap();
assert_eq!(ids(&before), vec!["retracted"], "believed at the time");
assert_eq!(
before[0].retired_at.as_deref(),
Some("2026-05-01T00:00:00Z"),
"and the view says what became of it"
);
let after = memories_as_of(&c, "2026-07-01T00:00:00Z", 0).unwrap();
assert!(after.is_empty(), "no longer believed: {:?}", ids(&after));
}
#[test]
fn valid_time_is_independent_of_when_it_was_recorded() {
let c = conn();
insert(
&c,
"future-effective",
"the new API lands in March",
"2026-01-01T00:00:00Z",
None,
Some("2026-03-01T00:00:00Z"),
None,
None,
);
assert!(
memories_as_of(&c, "2026-02-01T00:00:00Z", 0)
.unwrap()
.is_empty(),
"recorded, but not yet in effect"
);
assert_eq!(
ids(&memories_as_of(&c, "2026-04-01T00:00:00Z", 0).unwrap()),
vec!["future-effective"]
);
}
#[test]
fn an_expired_memory_drops_out_after_its_valid_to() {
let c = conn();
insert(
&c,
"expired",
"we are on rust 1.85",
"2026-01-01T00:00:00Z",
None,
None,
Some("2026-04-01T00:00:00Z"),
None,
);
assert_eq!(
ids(&memories_as_of(&c, "2026-02-01T00:00:00Z", 0).unwrap()),
vec!["expired"]
);
assert!(
memories_as_of(&c, "2026-05-01T00:00:00Z", 0)
.unwrap()
.is_empty()
);
}
#[test]
fn a_superseded_memory_still_counts_as_a_past_belief() {
let c = conn();
insert(
&c,
"member",
"checkpoint the wal",
"2026-01-01T00:00:00Z",
None,
None,
None,
Some("survivor"),
);
let view = memories_as_of(&c, "2026-03-01T00:00:00Z", 0).unwrap();
assert_eq!(ids(&view), vec!["member"]);
assert_eq!(
view[0].retired_reason.as_deref(),
Some("merged into survivor"),
"and the view explains where it went"
);
}
#[test]
fn belief_delta_reports_what_was_learned_and_retired() {
let c = conn();
insert(
&c,
"kept",
"a",
"2026-01-01T00:00:00Z",
None,
None,
None,
None,
);
insert(
&c,
"dropped",
"b",
"2026-01-01T00:00:00Z",
Some("2026-04-01T00:00:00Z"),
None,
None,
None,
);
insert(
&c,
"added",
"c",
"2026-03-01T00:00:00Z",
None,
None,
None,
None,
);
let delta = belief_delta(&c, "2026-02-01T00:00:00Z", "2026-06-01T00:00:00Z").unwrap();
assert_eq!(ids(&delta.learned), vec!["added"]);
assert_eq!(ids(&delta.retired), vec!["dropped"]);
}
#[test]
fn the_limit_is_respected_and_zero_means_all() {
let c = conn();
for i in 0..5 {
insert(
&c,
&format!("m{i}"),
"x",
&format!("2026-01-0{}T00:00:00Z", i + 1),
None,
None,
None,
None,
);
}
assert_eq!(
memories_as_of(&c, "2026-09-01T00:00:00Z", 0).unwrap().len(),
5
);
assert_eq!(
memories_as_of(&c, "2026-09-01T00:00:00Z", 2).unwrap().len(),
2
);
}
#[test]
fn as_of_capsules_render_the_scope_and_kind_prefix() {
let c = conn();
insert(
&c,
"m",
"checkpoint the wal",
"2026-01-01T00:00:00Z",
None,
None,
None,
None,
);
let capsules = as_of_capsules(&memories_as_of(&c, "2026-02-01T00:00:00Z", 0).unwrap());
assert_eq!(capsules.len(), 1);
assert!(capsules[0].summary.starts_with("project:fact - "));
assert_eq!(capsules[0].expansion_handle, "memory:m");
}
}