use serde::{Deserialize, Serialize};
use ulid::Ulid;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum Grain {
#[default]
Turn,
ToolCall,
Node,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum Strength {
#[default]
Weak,
Strong,
StrongNegative,
}
pub const WEIGHT_WEAK: f64 = 1.0;
pub const WEIGHT_STRONG: f64 = 3.0;
pub const WEIGHT_STRONG_NEGATIVE: f64 = -3.0;
pub const BOOST_MIDPOINT: f64 = 0.0;
pub const BOOST_SCALE: f64 = 3.0;
#[deprecated(note = "F43: superseded by BOOST_MIDPOINT / BOOST_SCALE; the \
k * ln(1 + a) curve made the prior inert in production")]
pub const BOOST_K: f32 = 0.1;
pub const BOOST_CAP: f32 = 0.30;
pub const DEFAULT_DECAY: f64 = 0.5;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RefSignal {
pub id: Ulid,
pub grain: Grain,
pub strength: Strength,
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ActivationRecord {
pub n: u32,
pub weighted: f64,
pub first_ref_wall: u64,
pub last_ref_wall: u64,
pub last_grain: Grain,
pub last_strength: Strength,
pub v: u8,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub archived_at: Option<u64>,
}
impl Default for ActivationRecord {
fn default() -> Self {
Self {
n: 0,
weighted: 0.0,
first_ref_wall: 0,
last_ref_wall: 0,
last_grain: Grain::default(),
last_strength: Strength::default(),
v: 1,
archived_at: None,
}
}
}
impl ActivationRecord {
pub fn apply(&mut self, s: &RefSignal, now: u64) {
if self.n == 0 {
self.first_ref_wall = now;
}
self.n = self.n.saturating_add(1);
self.weighted = (self.weighted + weight_for(s.strength)).max(0.0);
self.last_ref_wall = now;
self.last_grain = s.grain;
self.last_strength = s.strength;
}
pub fn activation(&self, now: u64, decay: f64) -> f64 {
let elapsed = (now.saturating_sub(self.last_ref_wall)).max(1) as f64;
let sum = self.weighted * elapsed.powf(-decay);
sum.ln()
}
pub fn is_archived(&self) -> bool {
self.archived_at.is_some()
}
}
fn weight_for(s: Strength) -> f64 {
match s {
Strength::Weak => WEIGHT_WEAK,
Strength::Strong => WEIGHT_STRONG,
Strength::StrongNegative => WEIGHT_STRONG_NEGATIVE,
}
}
pub fn boost_prior(activation: f64) -> f32 {
if activation.is_nan() {
return 0.0;
}
let z = (activation - BOOST_MIDPOINT) / BOOST_SCALE;
let logistic = 1.0 / (1.0 + (-z).exp());
((BOOST_CAP as f64) * logistic).clamp(0.0, BOOST_CAP as f64) as f32
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_record_has_zeroed_fields() {
let r = ActivationRecord::default();
assert_eq!(r.n, 0);
assert_eq!(r.weighted, 0.0);
assert_eq!(r.v, 1);
}
#[test]
fn boost_prior_reserves_zero_for_never_referenced() {
assert_eq!(boost_prior(f64::NEG_INFINITY), 0.0, "never referenced");
assert!(boost_prior(-5.0) > 0.0, "a stale but real memory is not nothing");
assert_eq!(
boost_prior(BOOST_MIDPOINT),
BOOST_CAP / 2.0,
"the midpoint earns exactly half the cap"
);
assert!(boost_prior(-5.0) < boost_prior(BOOST_MIDPOINT), "strictly increasing");
}
fn prior_ladder(weighted: f64) -> Vec<(u64, f32)> {
[10u64, 60, 600, 3_600, 21_600, 86_400, 604_800]
.iter()
.map(|&secs| {
let r = ActivationRecord {
n: 1,
weighted,
first_ref_wall: 0,
last_ref_wall: 0,
last_grain: Grain::default(),
last_strength: Strength::default(),
v: 1,
archived_at: None,
};
(secs, boost_prior(r.activation(secs, DEFAULT_DECAY)))
})
.collect()
}
#[test]
fn f43_a_well_used_memory_still_earns_a_prior_after_an_hour() {
let ladder = prior_ladder(30.0); let (_, at_one_hour) = ladder[3];
assert!(
at_one_hour > 0.0,
"10 strong refs one hour old must still carry a prior; ladder = {ladder:?}"
);
}
#[test]
fn f43_usage_history_is_discriminating_at_realistic_ages() {
for secs in [3_600u64, 86_400, 604_800] {
let heavy = prior_ladder(150.0);
let light = prior_ladder(3.0);
let h = heavy.iter().find(|(s, _)| *s == secs).unwrap().1;
let l = light.iter().find(|(s, _)| *s == secs).unwrap().1;
assert!(h > l, "at {secs}s: 50 strong refs ({h}) must outrank 1 strong ref ({l})");
}
}
#[test]
fn f43_prior_decays_strictly_and_never_flatlines_to_zero() {
let ladder = prior_ladder(30.0);
for w in ladder.windows(2) {
let ((s0, p0), (s1, p1)) = (w[0], w[1]);
assert!(
p1 < p0,
"prior must strictly decrease {s0}s -> {s1}s, got {p0} -> {p1}; ladder = {ladder:?}"
);
assert!(p1 > 0.0, "prior flatlined to zero at {s1}s; ladder = {ladder:?}");
}
}
#[test]
fn f43_a_never_referenced_memory_earns_exactly_zero() {
let ladder = prior_ladder(0.0);
for (secs, p) in ladder {
assert_eq!(p, 0.0, "never-referenced memory got {p} at {secs}s");
}
}
#[test]
fn f43_a_nan_activation_yields_zero_not_nan() {
let p = boost_prior(f64::NAN);
assert!(p.is_finite(), "NaN activation produced {p}");
assert_eq!(p, 0.0);
}
#[test]
fn boost_prior_never_exceeds_cap() {
for a in [0.1, 1.0, 10.0, 1_000.0, 1_000_000.0] {
assert!(boost_prior(a) <= BOOST_CAP);
assert!(boost_prior(a) >= 0.0);
}
}
#[test]
fn strong_negative_weight_and_floor() {
let mut r = ActivationRecord::default();
r.apply(&RefSignal { id: Ulid::nil(), grain: Grain::Turn, strength: Strength::Weak }, 100);
assert_eq!(r.weighted, WEIGHT_WEAK);
r.apply(
&RefSignal { id: Ulid::nil(), grain: Grain::Turn, strength: Strength::StrongNegative },
200,
);
assert_eq!(r.weighted, 0.0, "1.0 + (-3.0) must floor at 0.0, not go negative");
assert_eq!(r.n, 2);
assert_eq!(r.last_strength, Strength::StrongNegative);
assert_eq!(WEIGHT_STRONG_NEGATIVE, -3.0);
let json = serde_json::to_string(&r.last_strength).unwrap();
assert_eq!(json, "\"strong_negative\"");
let back: Strength = serde_json::from_str(&json).unwrap();
assert_eq!(back, Strength::StrongNegative);
}
#[test]
fn old_activation_record_without_strong_negative_still_decodes() {
let raw = r#"{"n":1,"weighted":3.0,"first_ref_wall":1,"last_ref_wall":1,"last_grain":"turn","last_strength":"strong","v":1}"#;
let record: ActivationRecord = serde_json::from_str(raw).unwrap();
assert_eq!(record.last_strength, Strength::Strong);
assert_eq!(record.weighted, 3.0);
}
#[test]
fn weak_and_strong_apply_math_unchanged() {
let mut r = ActivationRecord::default();
r.apply(&RefSignal { id: Ulid::nil(), grain: Grain::Turn, strength: Strength::Weak }, 1);
assert_eq!(r.weighted, 1.0);
r.apply(&RefSignal { id: Ulid::nil(), grain: Grain::Turn, strength: Strength::Strong }, 2);
assert_eq!(r.weighted, 4.0);
}
#[test]
fn old_activation_record_without_archived_at_still_decodes() {
let raw = r#"{"n":1,"weighted":3.0,"first_ref_wall":1,"last_ref_wall":1,"last_grain":"turn","last_strength":"strong","v":1}"#;
let record: ActivationRecord = serde_json::from_str(raw).unwrap();
assert_eq!(record.archived_at, None);
assert!(!record.is_archived());
}
#[test]
fn archived_at_skip_serializing_when_none_and_round_trips_when_set() {
let live = ActivationRecord::default();
assert!(!live.is_archived());
let live_json = serde_json::to_string(&live).unwrap();
assert!(
!live_json.contains("archived_at"),
"unarchived record must not emit archived_at: {live_json}"
);
let archived =
ActivationRecord { archived_at: Some(1_700_000_000), ..ActivationRecord::default() };
assert!(archived.is_archived());
let archived_json = serde_json::to_string(&archived).unwrap();
assert!(
archived_json.contains("\"archived_at\":1700000000"),
"archived record must serialize archived_at: {archived_json}"
);
let back: ActivationRecord = serde_json::from_str(&archived_json).unwrap();
assert_eq!(back.archived_at, Some(1_700_000_000));
assert!(back.is_archived());
}
#[test]
fn is_archived_true_false() {
let mut r = ActivationRecord::default();
assert!(!r.is_archived());
r.archived_at = Some(42);
assert!(r.is_archived());
r.archived_at = None;
assert!(!r.is_archived());
}
}