use serde::{Deserialize, Serialize};
use uuid::Uuid;
const RECENCY_HALF_LIFE_HOURS: f64 = 168.0;
const HIT_RATE_SMOOTH: f64 = 5.0;
pub const DEFAULT_EXCERPT_TOKENS: usize = 64;
const MIN_EXCERPT_TOKENS: usize = 16;
pub fn est_tokens(text: &str) -> usize {
text.chars().count().div_ceil(4)
}
fn truncate_chars(text: &str, max_chars: usize) -> String {
text.chars().take(max_chars).collect()
}
pub fn recoverability(age_hours: f64, access_count: u64) -> f32 {
let recency = 0.5f64.powf(age_hours.max(0.0) / RECENCY_HALF_LIFE_HOURS);
let ac = access_count as f64;
let hit_rate = (ac + 1.0) / (ac + 1.0 + HIT_RATE_SMOOTH);
(recency * hit_rate) as f32
}
#[derive(Debug, Clone)]
pub struct RetentionCandidate<'a> {
pub id: Uuid,
pub content: &'a str,
pub access_count: u64,
pub age_hours: f64,
pub retrieval_score: f32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct EvidenceCapsule {
pub id: Uuid,
pub retrieval_key: String,
pub excerpt: String,
pub tokens: usize,
pub recoverability: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RetentionReport {
pub budget_tokens: usize,
pub retained_tokens: usize,
pub capsules: Vec<EvidenceCapsule>,
pub candidates_examined: usize,
pub dropped: usize,
}
pub fn retain_within_budget(
candidates: &[RetentionCandidate<'_>],
budget_tokens: usize,
excerpt_tokens: usize,
) -> RetentionReport {
let mut order: Vec<usize> = (0..candidates.len()).collect();
order.sort_by(|&a, &b| {
let ra = recoverability(candidates[a].age_hours, candidates[a].access_count);
let rb = recoverability(candidates[b].age_hours, candidates[b].access_count);
rb.partial_cmp(&ra)
.unwrap_or(std::cmp::Ordering::Equal)
.then(
candidates[b]
.retrieval_score
.partial_cmp(&candidates[a].retrieval_score)
.unwrap_or(std::cmp::Ordering::Equal),
)
.then(a.cmp(&b))
});
let mut remaining = budget_tokens;
let mut capsules = Vec::new();
let mut retained_tokens = 0usize;
for &i in &order {
let c = &candidates[i];
let key = c.id.to_string();
let key_tokens = est_tokens(&key);
if remaining < key_tokens + MIN_EXCERPT_TOKENS {
break;
}
let excerpt_budget = (remaining - key_tokens).min(excerpt_tokens);
let excerpt = truncate_chars(c.content, excerpt_budget * 4);
let excerpt_cost = est_tokens(&excerpt);
let cost = excerpt_cost + key_tokens;
if cost > remaining {
continue;
}
remaining -= cost;
retained_tokens += cost;
capsules.push(EvidenceCapsule {
id: c.id,
retrieval_key: key,
excerpt,
tokens: cost,
recoverability: recoverability(c.age_hours, c.access_count),
});
}
RetentionReport {
budget_tokens,
retained_tokens,
dropped: candidates.len().saturating_sub(capsules.len()),
candidates_examined: candidates.len(),
capsules,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn cand<'a>(id: Uuid, content: &'a str, access: u64, age_hours: f64) -> RetentionCandidate<'a> {
RetentionCandidate {
id,
content,
access_count: access,
age_hours,
retrieval_score: 1.0,
}
}
#[test]
fn est_tokens_is_ceil_div_four() {
assert_eq!(est_tokens(""), 0);
assert_eq!(est_tokens("abcd"), 1);
assert_eq!(est_tokens("abcde"), 2);
}
#[test]
fn recoverability_rewards_recent_and_frequent() {
let fresh_hot = recoverability(1.0, 50);
let stale_cold = recoverability(720.0, 0);
assert!(fresh_hot > stale_cold);
assert!(recoverability(10.0, 20) > recoverability(10.0, 1));
assert!(recoverability(1.0, 10) > recoverability(500.0, 10));
assert!(recoverability(0.0, 0) > 0.0);
}
#[test]
fn packing_never_exceeds_budget() {
let ids: Vec<Uuid> = (0..20).map(|_| Uuid::now_v7()).collect();
let body = "the salient fact is alpha-bravo-charlie ".repeat(20);
let cands: Vec<RetentionCandidate> = ids
.iter()
.enumerate()
.map(|(i, id)| cand(*id, &body, i as u64, i as f64))
.collect();
let report = retain_within_budget(&cands, 512, DEFAULT_EXCERPT_TOKENS);
assert!(report.retained_tokens <= report.budget_tokens);
assert_eq!(report.candidates_examined, 20);
assert_eq!(report.capsules.len() + report.dropped, 20);
for w in report.capsules.windows(2) {
assert!(w[0].recoverability >= w[1].recoverability);
}
}
#[test]
fn capsules_beat_raw_dumps_on_count_under_budget() {
let ids: Vec<Uuid> = (0..40).map(|_| Uuid::now_v7()).collect();
let chunk = format!("FACT {} ", "x".repeat(600));
let cands: Vec<RetentionCandidate> =
ids.iter().map(|id| cand(*id, &chunk, 1, 1.0)).collect();
let budget = 2048;
let report = retain_within_budget(&cands, budget, DEFAULT_EXCERPT_TOKENS);
let chunk_tokens = est_tokens(&chunk);
let naive_fit = budget / chunk_tokens;
assert!(
report.capsules.len() > naive_fit,
"capsules {} should beat naive {} under {budget} tokens",
report.capsules.len(),
naive_fit
);
assert!(
report
.capsules
.iter()
.all(|c| c.excerpt.starts_with("FACT"))
);
}
#[test]
fn zero_budget_keeps_nothing() {
let id = Uuid::now_v7();
let report =
retain_within_budget(&[cand(id, "anything", 1, 1.0)], 0, DEFAULT_EXCERPT_TOKENS);
assert!(report.capsules.is_empty());
assert_eq!(report.dropped, 1);
assert_eq!(report.retained_tokens, 0);
}
}