use std::collections::{BTreeSet, HashMap};
#[cfg(feature = "local-embed")]
use anyhow::anyhow;
use anyhow::{Context, Result};
use hifitime::Epoch;
use triblespace::core::metadata;
use triblespace::core::query::TriblePattern;
use triblespace::core::repo::BlobStoreGet;
use triblespace::macros::{find, pattern};
use triblespace::prelude::blobencodings::{RawBytes, UTF8String};
use triblespace::prelude::inlineencodings::{Handle, NsTAIInterval};
use triblespace::prelude::*;
use triblespace_search::bm25::BM25Builder;
use triblespace_search::tokens::hash_tokens;
#[cfg(feature = "local-embed")]
use crate::nomic;
#[cfg(feature = "local-embed")]
use crate::schemas::embeddings::{self, Embedding768};
use crate::schemas::memory::{ctx, KIND_CHUNK_ID};
pub fn chunk_summary_handle<P: TriblePattern>(
space: &P,
id: Id,
) -> Option<Inline<Handle<UTF8String>>> {
find!(h: Inline<Handle<UTF8String>>, pattern!(space, [{ id @ ctx::summary: ?h }])).min()
}
pub fn chunk_image_handle<P: TriblePattern>(space: &P, id: Id) -> Option<Inline<Handle<RawBytes>>> {
find!(h: Inline<Handle<RawBytes>>, pattern!(space, [{ id @ ctx::image: ?h }])).min()
}
pub fn summary_handles<P: TriblePattern>(space: &P) -> HashMap<Id, Inline<Handle<UTF8String>>> {
let mut handles: HashMap<Id, Inline<Handle<UTF8String>>> = HashMap::new();
for (id, handle) in find!(
(id: Id, handle: Inline<Handle<UTF8String>>),
pattern!(space, [{ ?id @ ctx::summary: ?handle }])
) {
handles
.entry(id)
.and_modify(|current| {
if handle < *current {
*current = handle;
}
})
.or_insert(handle);
}
handles
}
pub fn chunk_span_str<P: TriblePattern>(space: &P, id: Id) -> String {
match (chunk_start_at(space, id), chunk_end_at(space, id)) {
(Some(s), Some(e)) => format_time_range(epoch_from_interval(s), epoch_end_from_interval(e)),
_ => "?".to_string(),
}
}
pub fn chunk_lens_handle<P: TriblePattern>(
space: &P,
id: Id,
) -> Option<Inline<Handle<UTF8String>>> {
find!(h: Inline<Handle<UTF8String>>, pattern!(space, [{ id @ ctx::lens: ?h }])).min()
}
pub fn chunk_start_at<P: TriblePattern>(space: &P, id: Id) -> Option<Inline<NsTAIInterval>> {
find!(v: Inline<NsTAIInterval>, pattern!(space, [{ id @ ctx::start_at: ?v }])).min()
}
pub fn chunk_end_at<P: TriblePattern>(space: &P, id: Id) -> Option<Inline<NsTAIInterval>> {
find!(v: Inline<NsTAIInterval>, pattern!(space, [{ id @ ctx::end_at: ?v }])).max()
}
pub fn chunk_about_archive_message<P: TriblePattern>(space: &P, id: Id) -> Option<Id> {
find!(v: Id, pattern!(space, [{ id @ ctx::about_archive_message: ?v }])).min()
}
pub fn chunk_aliases<P: TriblePattern>(space: &P, id: Id) -> Vec<Id> {
find!(v: Id, pattern!(space, [{ id @ metadata::anchor: ?v }]))
.collect::<BTreeSet<_>>()
.into_iter()
.collect()
}
pub fn all_chunk_ids<P: TriblePattern>(space: &P) -> Vec<Id> {
find!(id: Id, pattern!(space, [{ ?id @ metadata::tag: &KIND_CHUNK_ID }]))
.collect::<BTreeSet<_>>()
.into_iter()
.collect()
}
pub fn chunk_references<P: TriblePattern>(space: &P, id: Id) -> Vec<Id> {
let mut children: Vec<Id> =
find!(c: Id, pattern!(space, [{ id @ ctx::reference: ?c }])).collect();
children.sort_by_key(|child_id| {
chunk_start_at(space, *child_id)
.map(interval_key)
.unwrap_or(i128::MAX)
});
children.dedup();
children
}
pub fn chunk_about_exec_result<P: TriblePattern>(space: &P, id: Id) -> Option<Id> {
find!(v: Id, pattern!(space, [{ id @ ctx::about_exec_result: ?v }])).min()
}
pub fn chunk_observed_at<P: TriblePattern>(space: &P, id: Id) -> Vec<Inline<NsTAIInterval>> {
find!(v: Inline<NsTAIInterval>, pattern!(space, [{ id @ metadata::created_at: ?v }]))
.collect::<BTreeSet<_>>()
.into_iter()
.collect()
}
#[cfg(feature = "local-embed")]
pub fn chunk_embedding_handle<P: TriblePattern>(
embeddings_space: &P,
id: Id,
) -> Result<Option<Inline<Handle<Embedding768>>>> {
let handles: BTreeSet<_> = find!(
h: Inline<Handle<Embedding768>>,
pattern!(embeddings_space, [{ id @ embeddings::attr::embedding: ?h }])
)
.collect();
Ok(handles.first().copied())
}
pub fn format_time_range(start: Epoch, end: Epoch) -> String {
let (y1, m1, d1, h1, mi1, s1, _) = start.to_gregorian_tai();
let (y2, m2, d2, h2, mi2, s2, _) = end.to_gregorian_tai();
format!(
"{y1:04}-{m1:02}-{d1:02}T{h1:02}:{mi1:02}:{s1:02}..{y2:04}-{m2:02}-{d2:02}T{h2:02}:{mi2:02}:{s2:02}"
)
}
pub fn fmt_epoch(e: Epoch) -> String {
let (y, m, d, h, mi, s, _) = e.to_gregorian_tai();
format!("{y:04}-{m:02}-{d:02}T{h:02}:{mi:02}:{s:02}")
}
pub fn epoch_from_interval(interval: Inline<NsTAIInterval>) -> Epoch {
let (lower, _): (Epoch, Epoch) = interval.try_from_inline().unwrap();
lower
}
pub fn epoch_end_from_interval(interval: Inline<NsTAIInterval>) -> Epoch {
let (_, upper): (Epoch, Epoch) = interval.try_from_inline().unwrap();
upper
}
pub fn interval_key(interval: Inline<NsTAIInterval>) -> i128 {
let (lower, _): (Epoch, Epoch) = interval.try_from_inline().unwrap();
lower.to_tai_duration().total_nanoseconds()
}
pub fn key_to_epoch(key: i128) -> Epoch {
Epoch::from_tai_duration(hifitime::Duration::from_total_nanoseconds(key))
}
#[cfg(feature = "local-embed")]
pub fn l2_normalize(mut v: Vec<f32>) -> Vec<f32> {
let n = v.iter().map(|x| x * x).sum::<f32>().sqrt();
if n > 0.0 {
for x in &mut v {
*x /= n;
}
}
v
}
pub fn collect_chunk_spans<P: TriblePattern>(space: &P) -> Vec<(i128, i128, Id)> {
let mut spans: Vec<_> = find!(
(id: Id, start: Inline<NsTAIInterval>, end: Inline<NsTAIInterval>),
pattern!(space, [{
?id @ metadata::tag: &KIND_CHUNK_ID,
ctx::start_at: ?start,
ctx::end_at: ?end,
}])
)
.filter(|(id, _, _)| chunk_lens_handle(space, *id).is_none())
.map(|(id, start, end)| (interval_key(start), interval_key(end), id))
.filter(|(start, end, _)| start <= end)
.collect();
spans.sort_unstable();
spans.dedup();
let content_of = |id: Id| -> Option<[u8; 32]> {
chunk_summary_handle(space, id)
.map(|h| h.raw)
.or_else(|| chunk_image_handle(space, id).map(|h| h.raw))
};
let respanned: BTreeSet<Id> = find!(
(newer: Id, older: Id),
pattern!(space, [{
?newer @ metadata::tag: &KIND_CHUNK_ID,
metadata::supersedes: ?older,
}])
)
.filter(|(newer, older)| {
let newer = content_of(*newer);
newer.is_some() && newer == content_of(*older)
})
.map(|(_, older)| older)
.collect();
if !respanned.is_empty() {
spans.retain(|(_, _, id)| !respanned.contains(id));
}
spans
}
pub fn context_chunk_cost<B: BlobStoreGet, P: TriblePattern>(
ws: &B,
space: &P,
spans: &[(i128, i128, Id)],
cache: &mut [Option<usize>],
i: usize,
) -> Result<usize> {
if let Some(c) = cache[i] {
return Ok(c);
}
let (start, end, id) = spans[i];
let range = format_time_range(key_to_epoch(start), key_to_epoch(end));
let framing = 1usize
.saturating_add(range.chars().count())
.saturating_add(1);
let body = match chunk_summary_handle(space, id) {
Some(handle) => {
let summary: View<str> = ws.get(handle).context("read chunk summary")?;
Some(summary.trim_end().chars().count())
}
None if chunk_image_handle(space, id).is_some() => {
Some(format!("[image memory @ {range}]").chars().count())
}
None => None,
};
let c = body.map_or(framing, |chars| {
framing.saturating_add(chars).saturating_add(1)
});
cache[i] = Some(c);
Ok(c)
}
pub const DEFAULT_SIM_THRESHOLD: f32 = 0.55;
pub fn lexical_relevance_scores<B: BlobStoreGet, P: TriblePattern>(
space: &P,
reader: &B,
query: &str,
) -> Result<HashMap<Id, f32>> {
let mut builder = BM25Builder::new();
for chunk in all_chunk_ids(space) {
let Some(handle) = chunk_summary_handle(space, chunk) else {
continue;
};
let summary: View<str> = reader
.get(handle)
.with_context(|| format!("read Memory chunk {chunk:x} for lexical search"))?;
builder.insert(chunk, hash_tokens(summary.as_ref()));
}
Ok(builder
.build()
.query_multi(&hash_tokens(query))
.into_iter()
.filter_map(|(doc, score)| Some((doc.try_from_inline().ok()?, score)))
.collect())
}
pub fn about_relevance_scores<B, P, E>(
space: &P,
embeddings_space: &E,
reader: &B,
query: &str,
) -> Result<HashMap<Id, f32>>
where
B: BlobStoreGet,
P: TriblePattern,
E: TriblePattern,
{
#[cfg(feature = "local-embed")]
{
if let Some(scores) = semantic_about_scores(space, embeddings_space, reader, query)? {
return Ok(scores);
}
}
#[cfg(not(feature = "local-embed"))]
let _ = embeddings_space;
lexical_relevance_scores(space, reader, query)
}
#[cfg(feature = "local-embed")]
pub fn semantic_about_scores<B, P, E>(
space: &P,
embeddings_space: &E,
reader: &B,
query: &str,
) -> Result<Option<HashMap<Id, f32>>>
where
B: BlobStoreGet,
P: TriblePattern,
E: TriblePattern,
{
let mut handles: Vec<(Id, Inline<Handle<Embedding768>>)> = Vec::new();
for chunk in all_chunk_ids(space) {
if let Some(h) = chunk_embedding_handle(embeddings_space, chunk)? {
handles.push((chunk, h));
}
}
if handles.is_empty() {
return Ok(None);
}
eprintln!("memory: loading nomic-embed-text for --about (once)…");
let emb = nomic::load_text_embedder()?;
let qv = l2_normalize(
emb.embed_query(query)
.map_err(|e| anyhow!("embed query: {e:?}"))?,
);
let mut scores = HashMap::new();
for (chunk, h) in handles {
let v: View<[f32]> = reader
.get(h)
.map_err(|e| anyhow!("read embedding: {e:?}"))?;
let cos: f32 = qv.iter().zip(v.as_ref().iter()).map(|(a, b)| a * b).sum();
scores.insert(chunk, cos.max(0.0));
}
Ok(Some(scores))
}
pub fn eligibility_scores<B, P, E>(
space: &P,
embeddings_space: &E,
reader: &B,
query: &str,
universe: &[Id],
) -> Result<(HashMap<Id, f32>, Vec<Id>)>
where
B: BlobStoreGet,
P: TriblePattern,
E: TriblePattern,
{
#[cfg(feature = "local-embed")]
{
if let Some(res) =
semantic_eligibility_scores(space, embeddings_space, reader, query, universe)?
{
return Ok(res);
}
}
#[cfg(not(feature = "local-embed"))]
let _ = embeddings_space;
let raw = lexical_relevance_scores(space, reader, query)?;
let max = raw.values().copied().fold(0.0_f32, f32::max).max(1e-6);
let scores = universe
.iter()
.map(|&id| (id, raw.get(&id).copied().map(|s| s / max).unwrap_or(0.0)))
.collect();
Ok((scores, Vec::new()))
}
#[cfg(feature = "local-embed")]
pub fn semantic_eligibility_scores<B, P, E>(
space: &P,
embeddings_space: &E,
reader: &B,
query: &str,
universe: &[Id],
) -> Result<Option<(HashMap<Id, f32>, Vec<Id>)>>
where
B: BlobStoreGet,
P: TriblePattern,
E: TriblePattern,
{
let mut embedded: Vec<(Id, Inline<Handle<Embedding768>>)> = Vec::new();
let mut unembedded: Vec<Id> = Vec::new();
for &chunk in universe {
match chunk_embedding_handle(embeddings_space, chunk)? {
Some(h) => embedded.push((chunk, h)),
None => unembedded.push(chunk),
}
}
if embedded.is_empty() {
return Ok(None);
}
eprintln!("memory: loading nomic-embed-text for --filter/--remove (once)…");
let emb = nomic::load_text_embedder()?;
let qv = l2_normalize(
emb.embed_query(query)
.map_err(|e| anyhow!("embed query: {e:?}"))?,
);
let mut scores = HashMap::new();
for (chunk, h) in embedded {
let v: View<[f32]> = reader
.get(h)
.map_err(|e| anyhow!("read embedding: {e:?}"))?;
let cos: f32 = qv.iter().zip(v.as_ref().iter()).map(|(a, b)| a * b).sum();
scores.insert(chunk, cos.max(0.0));
}
let lexical = lexical_relevance_scores(space, reader, query)?;
let lexical_max = lexical.values().copied().fold(0.0_f32, f32::max).max(1e-6);
let mut unscorable = Vec::new();
for chunk in unembedded {
if chunk_summary_handle(space, chunk).is_some() {
scores.insert(
chunk,
lexical
.get(&chunk)
.copied()
.map(|score| score / lexical_max)
.unwrap_or(0.0),
);
} else {
unscorable.push(chunk);
}
}
Ok(Some((scores, unscorable)))
}
pub struct CoverOpts {
pub budget_chars: usize,
pub chunk_overhead: usize,
pub about: Option<String>,
pub filter: Option<String>,
pub remove: Option<String>,
pub sim_threshold: f32,
}
impl CoverOpts {
pub fn plain(budget_chars: usize) -> Self {
CoverOpts {
budget_chars,
chunk_overhead: 0,
about: None,
filter: None,
remove: None,
sim_threshold: DEFAULT_SIM_THRESHOLD,
}
}
}
const MOMENT_NS: i128 = (crate::memory::MOMENT_SECONDS * 1_000_000_000.0) as i128;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct DensitySample {
pub time_ns: f64,
pub time_per_char_ns: f64,
}
#[derive(Clone, Copy, Debug)]
pub struct DensityGradient {
life_ns: f64,
space: usize,
rate: f64,
}
impl DensityGradient {
pub fn new(life_ns: i128, space: usize) -> Self {
let life_ns = life_ns.max(MOMENT_NS) as f64;
let space = space.max(1);
let moment = MOMENT_NS as f64;
let rate = (life_ns / moment).ln_1p() / space as f64;
Self {
life_ns,
space,
rate,
}
}
pub fn sample(&self, cursor: usize) -> DensitySample {
let cursor = cursor.min(self.space) as f64;
let behind = self.space as f64 - cursor;
let exponent = self.rate * behind;
let moment = MOMENT_NS as f64;
let age = moment * exponent.exp_m1();
DensitySample {
time_ns: (self.life_ns - age).clamp(0.0, self.life_ns),
time_per_char_ns: moment * self.rate * exponent.exp(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RecollectionCut {
pub cover: Vec<usize>,
pub used: usize,
}
pub fn select_recollection_cut(
spans: &[(i128, i128, Id)],
costs: &[usize],
eligible: &[bool],
budget: usize,
) -> RecollectionCut {
assert_eq!(spans.len(), costs.len());
assert_eq!(spans.len(), eligible.len());
let (Some(earliest), Some(latest)) = (
spans.iter().map(|span| span.0).min(),
spans.iter().map(|span| span.1).max(),
) else {
return RecollectionCut {
cover: Vec::new(),
used: 0,
};
};
if budget == 0 {
return RecollectionCut {
cover: Vec::new(),
used: 0,
};
}
let gradient = DensityGradient::new(latest.saturating_sub(earliest), budget);
let mut selected = vec![false; spans.len()];
let mut cover = Vec::new();
let mut used = 0usize;
while used < budget {
let ideal_start = gradient.sample(used).time_ns;
let mut best: Option<(f64, Id, usize)> = None;
for (i, &(start, end, id)) in spans.iter().enumerate() {
let cost = costs[i];
if selected[i] || !eligible[i] || cost == 0 {
continue;
}
let width = end.saturating_sub(start).max(MOMENT_NS) as f64;
let actual_start = start.saturating_sub(earliest) as f64;
let actual_end = actual_start + width;
let ideal_end = gradient.sample(used.saturating_add(cost)).time_ns;
let start_error = actual_start - ideal_start;
let end_error = actual_end - ideal_end;
let score = start_error.mul_add(start_error, end_error * end_error);
let candidate = (score, id, i);
if best.as_ref().is_none_or(|current| {
candidate
.0
.total_cmp(¤t.0)
.then(candidate.1.cmp(¤t.1))
.is_lt()
}) {
best = Some(candidate);
}
}
let Some((_, _, pick)) = best else {
break;
};
let next = used.saturating_add(costs[pick]);
if next > budget {
break;
}
selected[pick] = true;
cover.push(pick);
used = next;
}
RecollectionCut { cover, used }
}
pub fn silent_life_quarters(spans: &[(i128, i128, Id)], cover: &[usize]) -> Vec<(i128, i128)> {
let (Some(earliest), Some(latest)) = (
spans.iter().map(|span| span.0).min(),
spans.iter().map(|span| span.1).max(),
) else {
return Vec::new();
};
let life = latest.saturating_sub(earliest);
if life <= 0 {
return Vec::new();
}
let mut selected: Vec<(i128, i128)> = cover
.iter()
.map(|&i| {
let span = spans
.get(i)
.unwrap_or_else(|| panic!("cover index {i} is outside {} spans", spans.len()));
(span.0.max(earliest), span.1.min(latest))
})
.filter(|(start, end)| end > start)
.collect();
selected.sort_unstable();
let threshold = life / 4;
let mut silent = Vec::new();
let mut cursor = earliest;
for (start, end) in selected {
if start > cursor && start.saturating_sub(cursor) > threshold {
silent.push((cursor, start));
}
cursor = cursor.max(end);
if cursor >= latest {
break;
}
}
if latest.saturating_sub(cursor) > threshold {
silent.push((cursor, latest));
}
silent
}
fn recollection_classes(
raw_spans: &[(i128, i128, Id)],
) -> (Vec<(i128, i128, Id)>, Vec<Vec<usize>>) {
let mut order: Vec<usize> = (0..raw_spans.len()).collect();
order.sort_by(|&a, &b| {
raw_spans[a]
.0
.cmp(&raw_spans[b].0)
.then(raw_spans[a].1.cmp(&raw_spans[b].1))
.then(raw_spans[a].2.cmp(&raw_spans[b].2))
});
let mut spans = Vec::new();
let mut members: Vec<Vec<usize>> = Vec::new();
for raw in order {
let (start, end, id) = raw_spans[raw];
if spans
.last()
.is_some_and(|&(class_start, class_end, _)| class_start == start && class_end == end)
{
members.last_mut().expect("class exists").push(raw);
} else {
spans.push((start, end, id));
members.push(vec![raw]);
}
}
(spans, members)
}
fn recollection_class_cost<B: BlobStoreGet, P: TriblePattern>(
reader: &B,
space: &P,
raw_spans: &[(i128, i128, Id)],
raw_costs: &mut [Option<usize>],
classes: &[Vec<usize>],
class_costs: &mut [Option<usize>],
class: usize,
) -> Result<usize> {
if let Some(cost) = class_costs[class] {
return Ok(cost);
}
let mut cost = 0;
for &raw in &classes[class] {
cost = cost.max(context_chunk_cost(
reader, space, raw_spans, raw_costs, raw,
)?);
}
class_costs[class] = Some(cost);
Ok(cost)
}
fn recollection_class_observations<P: TriblePattern>(
space: &P,
raw_spans: &[(i128, i128, Id)],
classes: &[Vec<usize>],
) -> (Vec<i128>, usize) {
let mut first_by_id: HashMap<Id, i128> = HashMap::new();
for (id, observed) in find!(
(id: Id, observed: Inline<NsTAIInterval>),
pattern!(space, [{ ?id @ metadata::created_at: ?observed }])
) {
let observed = interval_key(observed);
first_by_id
.entry(id)
.and_modify(|first| *first = (*first).min(observed))
.or_insert(observed);
}
let mut missing = 0usize;
let observations = classes
.iter()
.map(|members| {
let mut observed = None;
for &raw in members {
if let Some(at) = first_by_id.get(&raw_spans[raw].2).copied() {
observed = Some(observed.map_or(at, |first: i128| first.min(at)));
}
}
observed.unwrap_or_else(|| {
missing += 1;
members
.iter()
.map(|&raw| raw_spans[raw].1)
.min()
.unwrap_or(0)
})
})
.collect();
(observations, missing)
}
fn select_recollection(
raw_spans: &[(i128, i128, Id)],
members: &[usize],
about_scores: Option<&HashMap<Id, f32>>,
eligible: &[bool],
) -> usize {
let has_eligible = members.iter().any(|&raw| eligible[raw]);
members
.iter()
.copied()
.filter(|&raw| !has_eligible || eligible[raw])
.max_by(|&a, &b| {
let a_score = about_scores
.and_then(|scores| scores.get(&raw_spans[a].2))
.copied()
.unwrap_or(0.0);
let b_score = about_scores
.and_then(|scores| scores.get(&raw_spans[b].2))
.copied()
.unwrap_or(0.0);
a_score
.total_cmp(&b_score)
.then_with(|| raw_spans[b].2.cmp(&raw_spans[a].2))
})
.expect("a recollection class is never empty")
}
#[derive(Clone, Debug)]
pub struct ReplayRow {
pub observed_now: i128,
pub semantic_now: i128,
pub chunks: usize,
pub used: usize,
pub silent_life_quarters: Vec<(i128, i128)>,
pub kept: usize,
pub prev_used: usize,
pub first: Option<(i128, i128)>,
pub changed_at: Option<(i128, i128)>,
pub unobserved_classes: usize,
}
pub const REPLAY_QUANTUM_NS: i128 = (1i128 << 14) * 1_000_000_000;
pub fn replay_cover<B: BlobStoreGet, P: TriblePattern>(
space: &P,
ws: &B,
budget_chars: usize,
chunk_overhead: usize,
steps: usize,
step_units: i128,
) -> Result<Vec<ReplayRow>> {
let raw_spans = collect_chunk_spans(space);
let (spans, classes) = recollection_classes(&raw_spans);
let (observed_at, unobserved_classes) =
recollection_class_observations(space, &raw_spans, &classes);
let mut rows = Vec::new();
let Some(latest_observation) = observed_at.iter().copied().max() else {
return Ok(rows);
};
let mut raw_costs: Vec<Option<usize>> = vec![None; raw_spans.len()];
let mut class_costs: Vec<Option<usize>> = vec![None; spans.len()];
let mut costs = Vec::with_capacity(spans.len());
for i in 0..spans.len() {
costs.push(
recollection_class_cost(
ws,
space,
&raw_spans,
&mut raw_costs,
&classes,
&mut class_costs,
i,
)?
.saturating_add(chunk_overhead),
);
}
let step_ns = step_units.max(1) * REPLAY_QUANTUM_NS;
let mut previous: Vec<usize> = Vec::new();
let mut prev_used = 0usize;
for k in (0..=steps).rev() {
let point = latest_observation - (k as i128) * step_ns;
let mut map: Vec<usize> = Vec::new();
let mut sub: Vec<(i128, i128, Id)> = Vec::new();
let mut sub_costs = Vec::new();
for (i, s) in spans.iter().enumerate() {
if observed_at[i] <= point {
map.push(i);
sub.push(*s);
sub_costs.push(costs[i]);
}
}
let Some(semantic_now) = sub.iter().map(|s| s.1).max() else {
continue;
};
let cut = select_recollection_cut(&sub, &sub_costs, &vec![true; sub.len()], budget_chars);
let silent_life_quarters = silent_life_quarters(&sub, &cut.cover);
let cover: Vec<usize> = cut.cover.iter().map(|&j| map[j]).collect();
let mut kept = 0usize;
let mut changed_at = None;
for (n, (a, b)) in cover.iter().zip(previous.iter()).enumerate() {
if a != b {
changed_at = Some((spans[*a].0, spans[*a].1));
break;
}
kept = kept.saturating_add(costs[*a]);
if n + 1 == previous.len() && cover.len() > previous.len() {
changed_at = Some((spans[cover[n + 1]].0, spans[cover[n + 1]].1));
}
}
if changed_at.is_none() && previous.is_empty() {
changed_at = cover.first().map(|&i| (spans[i].0, spans[i].1));
}
rows.push(ReplayRow {
observed_now: point,
semantic_now,
chunks: cover.len(),
used: cut.used,
silent_life_quarters,
kept,
prev_used,
first: cover.first().map(|&i| (spans[i].0, spans[i].1)),
changed_at,
unobserved_classes,
});
previous = cover;
prev_used = cut.used;
}
Ok(rows)
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CoverReport {
pub text: String,
pub diagnostics: Vec<String>,
}
fn unscorable_warning(label: &str, unscorable: &[Id]) -> Option<String> {
if unscorable.is_empty() {
return None;
}
let ids: Vec<String> = unscorable.iter().map(|id| format!("{id:x}")).collect();
Some(format!(
"memory: {} unembedded chunk(s) not scorable for {label} — kept (fail-open); \
run `memory embed` to make them filterable: {}",
unscorable.len(),
ids.join(", ")
))
}
pub fn render_cover<B, P, E>(
space: &P,
embeddings_space: &E,
reader: &B,
opts: &CoverOpts,
) -> Result<String>
where
B: BlobStoreGet,
P: TriblePattern,
E: TriblePattern,
{
let report = render_cover_report(space, embeddings_space, reader, opts)?;
for diagnostic in report.diagnostics {
eprintln!("{diagnostic}");
}
Ok(report.text)
}
pub fn render_cover_report<B, P, E>(
space: &P,
embeddings_space: &E,
reader: &B,
opts: &CoverOpts,
) -> Result<CoverReport>
where
B: BlobStoreGet,
P: TriblePattern,
E: TriblePattern,
{
use std::fmt::Write as _;
let budget_chars = opts.budget_chars;
let chunk_overhead = opts.chunk_overhead;
let about = opts.about.as_deref();
let filter_q = opts.filter.as_deref();
let remove_q = opts.remove.as_deref();
let sim_threshold = opts.sim_threshold;
let mut diagnostics = Vec::new();
let mut out = String::new();
let mut raw_spans = collect_chunk_spans(space);
let handles = summary_handles(space);
let mut unreadable: Vec<String> = Vec::new();
raw_spans.retain(|&(start, end, id)| match handles.get(&id) {
Some(&handle) => match reader.get::<View<str>, UTF8String>(handle) {
Ok(_) => true,
Err(error) => {
unreadable.push(format!(
"{} ({error})",
format_time_range(key_to_epoch(start), key_to_epoch(end))
));
false
}
},
None => true,
});
if !unreadable.is_empty() {
diagnostics.push(format!(
"memory context — {} memory(ies) skipped, summary not readable in this snapshot (usually still replicating from the machine that wrote it): {}",
unreadable.len(),
unreadable.join(", ")
));
}
if raw_spans.is_empty() {
writeln!(out, "no memory chunks")?;
return Ok(CoverReport {
text: out,
diagnostics,
});
}
let (spans, classes) = recollection_classes(&raw_spans);
if spans.is_empty() {
diagnostics.push("memory context — 0 chunk(s)".to_owned());
return Ok(CoverReport {
text: String::new(),
diagnostics,
});
}
let n = spans.len();
let universe: Vec<Id> = raw_spans.iter().map(|s| s.2).collect();
let filter_elig = match filter_q {
Some(q) => Some(eligibility_scores(
space,
embeddings_space,
reader,
q,
&universe,
)?),
None => None,
};
let remove_elig = match remove_q {
Some(q) => Some(eligibility_scores(
space,
embeddings_space,
reader,
q,
&universe,
)?),
None => None,
};
for (label, elig) in [("--filter", &filter_elig), ("--remove", &remove_elig)] {
if let Some((_, unscorable)) = elig {
if let Some(warning) = unscorable_warning(label, unscorable) {
diagnostics.push(warning);
}
}
}
let eligible_id = |id: Id| -> bool {
if let Some((scores, _)) = &filter_elig {
if let Some(v) = scores.get(&id) {
if *v <= sim_threshold {
return false;
}
}
}
if let Some((scores, _)) = &remove_elig {
if let Some(v) = scores.get(&id) {
if *v > sim_threshold {
return false;
}
}
}
true
};
let member_eligible: Vec<bool> = raw_spans.iter().map(|span| eligible_id(span.2)).collect();
let class_eligible: Vec<bool> = classes
.iter()
.map(|members| members.iter().any(|&raw| member_eligible[raw]))
.collect();
let about_scores = if classes.iter().any(|class| class.len() > 1) {
about
.map(|query| about_relevance_scores(space, embeddings_space, reader, query))
.transpose()?
} else {
None
};
let representatives: Vec<usize> = classes
.iter()
.map(|members| {
select_recollection(&raw_spans, members, about_scores.as_ref(), &member_eligible)
})
.collect();
let mut raw_costs: Vec<Option<usize>> = vec![None; raw_spans.len()];
let mut class_costs: Vec<Option<usize>> = vec![None; n];
let mut costs = Vec::with_capacity(n);
for i in 0..n {
costs.push(
recollection_class_cost(
reader,
space,
&raw_spans,
&mut raw_costs,
&classes,
&mut class_costs,
i,
)?
.saturating_add(chunk_overhead),
);
}
let cut = select_recollection_cut(&spans, &costs, &class_eligible, budget_chars);
let used = cut.used;
let cover = cut.cover;
let mode = {
let mut parts = vec![match about {
Some(q) => format!("recollections about \"{q}\" within equal spans"),
None => "density-shaped recollection".to_string(),
}];
if let Some(q) = filter_q {
parts.push(format!("filtered to \"{q}\""));
}
if let Some(q) = remove_q {
parts.push(format!("excluding \"{q}\""));
}
format!("greedy SPACE order; {}", parts.join("; "))
};
for &i in &cover {
let (s, e, _) = spans[i];
let id = raw_spans[representatives[i]].2;
writeln!(out)?;
writeln!(
out,
"{}",
format_time_range(key_to_epoch(s), key_to_epoch(e)),
)?;
if let Some(handle) = chunk_summary_handle(space, id) {
let summary: View<str> = reader.get(handle).context("read chunk summary")?;
writeln!(out, "{}", summary.trim_end())?;
} else if chunk_image_handle(space, id).is_some() {
let range = format_time_range(key_to_epoch(s), key_to_epoch(e));
writeln!(out, "[image memory @ {range}]")?;
}
}
let fill = if budget_chars == 0 {
0.0
} else {
100.0 * used as f64 / budget_chars as f64
};
diagnostics.push(format!(
"memory context — {} of {} eligible memories recalled, ~{} of {} characters ({fill:.1}% full; {mode})",
cover.len(),
class_eligible.iter().filter(|&&yes| yes).count(),
used,
budget_chars,
));
Ok(CoverReport {
text: out,
diagnostics,
})
}
#[cfg(test)]
mod recollection_tests {
use super::*;
use triblespace::macros::id_hex;
const A: Id = id_hex!("C1000000000000000000000000000001");
const B: Id = id_hex!("C1000000000000000000000000000002");
const C: Id = id_hex!("C1000000000000000000000000000003");
#[test]
fn fail_open_diagnostic_names_the_gate_and_exact_unscorable_ids() {
assert_eq!(unscorable_warning("--remove", &[]), None);
for gate in ["--filter", "--remove"] {
assert_eq!(
unscorable_warning(gate, &[B, A]).unwrap(),
format!("memory: 2 unembedded chunk(s) not scorable for {gate} — kept (fail-open); run `memory embed` to make them filterable: {B:x}, {A:x}")
);
}
}
#[test]
fn a_memory_whose_summary_has_not_arrived_is_skipped_and_named() {
use triblespace::core::blob::MemoryBlobStore;
use triblespace::core::repo::SnapshotSource;
let point = |seconds: f64| {
let epoch = Epoch::from_tai_seconds(seconds);
(epoch, epoch).try_to_inline().unwrap()
};
let mut blobs = MemoryBlobStore::new();
let here = blobs.insert("the summary that arrived".to_owned().to_blob());
let elsewhere = "a summary still in flight"
.to_owned()
.to_blob()
.get_handle();
let mut facts = entity! {
ExclusiveId::force_ref(&A) @
metadata::tag: &KIND_CHUNK_ID,
ctx::summary: here,
ctx::start_at: point(0.0),
ctx::end_at: point(100.0),
};
facts += entity! {
ExclusiveId::force_ref(&B) @
metadata::tag: &KIND_CHUNK_ID,
ctx::summary: elsewhere,
ctx::start_at: point(100.0),
ctx::end_at: point(200.0),
};
let reader = blobs.snapshot().expect("in-memory snapshot is infallible");
let report = render_cover_report(
facts.facts(),
&TribleSet::new(),
&reader,
&CoverOpts::plain(10_000),
)
.expect("a missing summary must not fail the render");
assert!(report.text.contains("the summary that arrived"));
let skipped = format_time_range(
key_to_epoch(interval_key(point(100.0))),
key_to_epoch(interval_key(point(200.0))),
);
assert!(
!report.text.contains(&skipped),
"the unreadable memory must not be emitted: {}",
report.text
);
let named = report.diagnostics.iter().any(|line| {
line.contains("1 memory(ies) skipped") && line.contains(&skipped)
});
assert!(named, "diagnostics: {:?}", report.diagnostics);
}
#[test]
fn exact_span_is_the_structural_equivalence_class() {
let spans = vec![(10, 20, C), (10, 21, B), (10, 20, A)];
let (structural, classes) = recollection_classes(&spans);
assert_eq!(
structural,
vec![(10, 20, A), (10, 21, B)],
"only exact endpoint equality collapses, and the structural id is stable"
);
let member_ids: Vec<Vec<Id>> = classes
.iter()
.map(|class| class.iter().map(|&raw| spans[raw].2).collect())
.collect();
assert_eq!(member_ids, vec![vec![A, C], vec![B]]);
}
#[test]
fn span_projection_keeps_additive_typed_observations() {
let point = |seconds: f64| {
let epoch = Epoch::from_tai_seconds(seconds);
(epoch, epoch).try_to_inline().unwrap()
};
let start_0 = point(0.0);
let start_10 = point(10.0);
let end_20 = point(20.0);
let end_30 = point(30.0);
let expected = vec![
(interval_key(start_0), interval_key(end_20), A),
(interval_key(start_0), interval_key(end_30), A),
(interval_key(start_10), interval_key(end_20), A),
(interval_key(start_10), interval_key(end_30), A),
];
let facts = entity! {
ExclusiveId::force_ref(&A) @
metadata::tag: &KIND_CHUNK_ID,
ctx::start_at: start_0,
ctx::start_at: start_10,
ctx::end_at: end_20,
ctx::end_at: end_30,
};
assert_eq!(collect_chunk_spans(facts.facts()), expected);
}
#[test]
fn recollection_classes_ignore_input_order() {
let original = [(0, 100, C), (0, 100, A), (10, 20, B)];
for permutation in [
[0usize, 1, 2],
[0, 2, 1],
[1, 0, 2],
[1, 2, 0],
[2, 0, 1],
[2, 1, 0],
] {
let raw: Vec<_> = permutation.into_iter().map(|i| original[i]).collect();
let (spans, classes) = recollection_classes(&raw);
assert_eq!(spans, vec![(0, 100, A), (10, 20, B)]);
let ids: Vec<Vec<Id>> = classes
.iter()
.map(|members| members.iter().map(|&i| raw[i].2).collect())
.collect();
assert_eq!(ids, vec![vec![A, C], vec![B]]);
}
}
#[test]
fn contextual_selection_stays_inside_one_class_and_respects_eligibility() {
let spans = vec![(0, 10, A), (0, 10, B), (0, 10, C)];
let members = vec![0, 1, 2];
let scores = HashMap::from([(A, 0.1), (B, 0.8), (C, 0.5)]);
assert_eq!(
select_recollection(&spans, &members, Some(&scores), &[true; 3]),
1,
"the most relevant equal-span recollection wins"
);
assert_eq!(
select_recollection(&spans, &members, Some(&scores), &[true, false, true]),
2,
"context cannot select an ineligible recollection"
);
assert_eq!(
select_recollection(&spans, &members, None, &[true; 3]),
0,
"without context the least intrinsic id is deterministic"
);
}
#[cfg(feature = "local-embed")]
#[test]
fn competing_shared_embedding_observations_are_arbitrated_deterministically() {
let chunk = Id::new([0x61; 16]).unwrap();
let mut fragment = Fragment::empty();
let first = fragment.put::<Embedding768, _>(vec![0.0; 768]);
let second = fragment.put::<Embedding768, _>(vec![1.0; 768]);
fragment += entity! {
triblespace::core::id::ExclusiveId::force_ref(&chunk) @
embeddings::attr::embedding: first,
embeddings::attr::embedding: second,
};
let selected = chunk_embedding_handle(fragment.facts(), chunk)
.unwrap()
.expect("one additive observation is selected");
assert_eq!(selected, first.min(second));
}
fn ids(n: usize) -> Vec<Id> {
(1..=n)
.map(|k| {
Id::new(u128::to_be_bytes(
0xC1000000000000000000000000000000 + k as u128,
))
.unwrap()
})
.collect()
}
#[test]
fn gradient_integrates_the_life_and_gets_finer_toward_now() {
let life = 1023 * MOMENT_NS;
let space = 10_000;
let gradient = DensityGradient::new(life, space);
let old = gradient.sample(0);
let young = gradient.sample(space);
assert!(old.time_ns.abs() < life as f64 * 1e-12);
assert!((young.time_ns - life as f64).abs() < life as f64 * 1e-12);
assert!(old.time_per_char_ns > young.time_per_char_ns);
let mut integral = 0.0;
for cursor in 0..space {
let a = gradient.sample(cursor).time_per_char_ns;
let b = gradient.sample(cursor + 1).time_per_char_ns;
integral += (a + b) / 2.0;
}
let relative_error = (integral - life as f64).abs() / life as f64;
assert!(relative_error < 1e-6, "{relative_error}");
}
#[test]
fn recollection_is_deliberately_lossy() {
let id = ids(101);
let life = 1000 * MOMENT_NS;
let mut spans = vec![(0, life, id[0])];
let mut costs = vec![10usize];
for i in 0..100 {
let start = i as i128 * 10 * MOMENT_NS;
spans.push((start, start + 10 * MOMENT_NS, id[i + 1]));
costs.push(10);
}
let cut = select_recollection_cut(&spans, &costs, &[true; 101], 50);
assert_eq!(cut.used, 50);
assert_eq!(cut.cover.len(), 5);
assert!(
cut.cover.len() < spans.len(),
"the unselected memories remain losslessly journaled, not forced into active recall"
);
}
#[test]
fn greedy_projection_ignores_candidate_input_order() {
let original = [
(0, 100 * MOMENT_NS, A),
(0, 50 * MOMENT_NS, B),
(50 * MOMENT_NS, 100 * MOMENT_NS, C),
];
let mut expected = None;
for permutation in [
[0usize, 1, 2],
[0, 2, 1],
[1, 0, 2],
[1, 2, 0],
[2, 0, 1],
[2, 1, 0],
] {
let spans: Vec<_> = permutation.into_iter().map(|i| original[i]).collect();
let cut = select_recollection_cut(&spans, &[5; 3], &[true; 3], 10);
let ids: Vec<_> = cut.cover.iter().map(|&i| spans[i].2).collect();
if let Some(expected) = &expected {
assert_eq!(&ids, expected);
} else {
expected = Some(ids);
}
}
}
#[test]
fn greedy_space_order_is_not_chronologically_repaired() {
let spans = vec![
(0, 80 * MOMENT_NS, A),
(80 * MOMENT_NS, 100 * MOMENT_NS, B),
(50 * MOMENT_NS, 60 * MOMENT_NS, C),
];
let cut = select_recollection_cut(&spans, &[5; 3], &[true; 3], 15);
let selected: Vec<_> = cut.cover.iter().map(|&i| spans[i].2).collect();
assert_eq!(selected, vec![A, B, C]);
assert!(
spans[cut.cover[2]].0 < spans[cut.cover[1]].0,
"the final fallback stays in its selected SPACE slot"
);
}
#[test]
fn candidate_length_defines_one_ideal_temporal_slot() {
let id = ids(5);
let life = 1000 * MOMENT_NS;
let budget = 101;
let cost = 10;
let gradient = DensityGradient::new(life, budget);
let ideal_end = gradient.sample(cost).time_ns.round() as i128;
let shift = ideal_end / 4;
let spans = vec![
(0, 0, id[0]),
(life, life, id[1]),
(0, ideal_end, id[2]),
(shift, ideal_end + shift, id[3]),
(ideal_end / 4, ideal_end * 3 / 4, id[4]),
];
let cut = select_recollection_cut(
&spans,
&[1, 1, cost, cost, cost],
&[false, false, true, true, true],
budget,
);
assert_eq!(cut.cover.first(), Some(&2));
}
#[test]
fn sampled_grain_gets_finer_toward_the_present() {
let id = ids(17);
let life = 4096 * MOMENT_NS;
let mut spans = vec![(0, life, id[0])];
let mut costs = vec![1usize];
for level in 0..8 {
let width = (1i128 << (11 - level)) * MOMENT_NS;
let old_start = (1i128 << level) * MOMENT_NS;
let young_end = life - (1i128 << level) * MOMENT_NS;
spans.push((old_start, old_start + width, id[1 + level * 2]));
spans.push((young_end - width, young_end, id[2 + level * 2]));
costs.extend([8, 8]);
}
let cut = select_recollection_cut(&spans, &costs, &[true; 17], 49);
let selected: Vec<_> = cut.cover.iter().copied().filter(|&i| i != 0).collect();
assert!(selected.len() >= 2, "{selected:?}");
let oldest = selected
.iter()
.min_by_key(|&&i| spans[i].0 + (spans[i].1 - spans[i].0) / 2)
.copied()
.unwrap();
let youngest = selected
.iter()
.max_by_key(|&&i| spans[i].0 + (spans[i].1 - spans[i].0) / 2)
.copied()
.unwrap();
let old_width = spans[oldest].1 - spans[oldest].0;
let young_width = spans[youngest].1 - spans[youngest].0;
assert!(
old_width > young_width,
"old {oldest} width {old_width}, young {youngest} width {young_width}, selected {selected:?}"
);
}
#[test]
fn first_best_sample_that_does_not_fit_ends_the_walk() {
let id = ids(1);
let life = 100 * MOMENT_NS;
let spans = vec![(0, life, id[0])];
let cut = select_recollection_cut(&spans, &[11], &[true], 10);
assert!(cut.cover.is_empty());
assert_eq!(cut.used, 0);
}
#[test]
fn silent_era_detection_is_observation_not_selection() {
let id = ids(4);
let spans = vec![
(0, 100 * MOMENT_NS, id[0]),
(0, 20 * MOMENT_NS, id[1]),
(10 * MOMENT_NS, 25 * MOMENT_NS, id[2]),
(60 * MOMENT_NS, 100 * MOMENT_NS, id[3]),
];
assert_eq!(
silent_life_quarters(&spans, &[1, 2, 3]),
vec![(25 * MOMENT_NS, 60 * MOMENT_NS)]
);
assert_eq!(
silent_life_quarters(&spans, &[]),
vec![(0, 100 * MOMENT_NS)]
);
assert!(
silent_life_quarters(&spans, &[0]).is_empty(),
"a broad selected arc makes the instrument quiet; it is not forced into selection"
);
}
}