open-standards/eoc/eoc-rs/crates/eoc-memory/src/forget.rs
Transaction Science 68b8a2dbee ingest wave F: 3 crates · op-crypto-extra + smart-byte-acdc + eoc-memory
OpenPay (1 crate, 3751 LOC, 83 tests)
  op-crypto-extra      Extended crypto rails. Stablecoin catalog (USDC, USDT,
                       PYUSD, DAI, RLUSD, EURC, FDUSD — 26 deployments). L2
                       catalog (12 chains incl. Optimism, Arbitrum, Base,
                       zkSync Era, Polygon zkEVM, Linea, Scroll, Starknet).
                       CCTP v2 BurnMessage encode/decode + domain ids +
                       attestation. ERC-4337 UserOperation v0.7 +
                       PackedUserOperation. EIP-7702 set-EOA-code with RLP
                       signing preimage. Uniswap Permit2 single + batch
                       transfer EIP-712 hashes. EIP-2612 ERC-20 permit.
                       BOLT-11 bech32 invoices + LNURL decode. Bitcoin
                       PSBT v2 (BIP-370). Atomic-swap HTLC. Solana SPL /
                       Aleo / Aztec confidential-transfer sketches.

Smart Byte (1 crate, 2304 LOC, 30 tests)
  smart-byte-acdc      Authentic Chained Data Containers (KERI ecosystem).
                       Acdc + AcdcBuilder (targeted + untargeted) with SAID
                       derivation. JSON Schema 2020-12 validator with
                       oneOf partial-disclosure. TEL (RIP/VRT/ISS/REV)
                       with hash-chained verification. IPEX (apply/offer/
                       agree/grant/admit/spurn) chained by SAID. Edge
                       records (I2I/NI2I/DI2I) + EdgeGraph BFS traversal
                       with cycle detection. Selective disclosure with
                       full/none/subset plans + compact packing.
                       CredentialRegistry trait + InMemoryRegistry over
                       TEL. Composes with smart-byte-core + smart-byte-keri-witness.

EOC (1 crate, 1764 LOC, 16 tests)
  eoc-memory           Long-term memory primitives. Memory trait + MemoryKind +
                       EpisodeId. Episodic event log with TimeIndex range
                       queries. RDF-style Triple store with S/P/O indices.
                       Bounded attention-budget Scratchpad with
                       salience-weighted eviction. Rolling SummaryBuffer +
                       FIFO SlidingWindow. Dream-cycle consolidate()
                       (episodic → semantic). Ebbinghaus forgetting curve
                       with reinforcement. Retriever blending cosine
                       similarity + recency + saturating frequency.
                       MemGpt three-tier shape (Main/Recall/Archival).

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-24 17:28:24 -04:00

110 lines
3.4 KiB
Rust

//! Ebbinghaus forgetting curve scorer.
//!
//! Hermann Ebbinghaus' 1885 retention experiment gave us the
//! exponential curve `R = exp(-t / S)` where `t` is elapsed time
//! since the last review and `S` is the memory's stability /
//! strength. Repeated review *increases* `S`; idle time leaves it
//! constant.
//!
//! We implement a small deterministic approximation: each retrieval
//! multiplies `S` by `(1 + reinforcement)`.
use serde::{Deserialize, Serialize};
use crate::error::{MemoryError, MemoryResult};
/// Configuration for the Ebbinghaus decay function.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ForgetConfig {
/// Initial stability `S_0`, in milliseconds. Larger = slower
/// decay. Default 24h = 86_400_000.
pub initial_stability_ms: f64,
/// Reinforcement factor applied on every successful retrieval.
/// Must be `>= 0.0`. Default 1.0 (each review doubles stability).
pub reinforcement: f64,
/// Retention threshold below which an item is considered
/// "forgotten". In `[0.0, 1.0]`. Default 0.2.
pub threshold: f64,
}
impl ForgetConfig {
/// Construct + validate.
pub fn new(initial_stability_ms: f64, reinforcement: f64, threshold: f64) -> MemoryResult<Self> {
if !(initial_stability_ms > 0.0) {
return Err(MemoryError::Config(
"initial_stability_ms must be > 0".into(),
));
}
if reinforcement < 0.0 {
return Err(MemoryError::Config("reinforcement must be >= 0".into()));
}
if !(0.0..=1.0).contains(&threshold) {
return Err(MemoryError::Config("threshold must be in [0,1]".into()));
}
Ok(Self {
initial_stability_ms,
reinforcement,
threshold,
})
}
}
impl Default for ForgetConfig {
fn default() -> Self {
Self {
initial_stability_ms: 86_400_000.0,
reinforcement: 1.0,
threshold: 0.2,
}
}
}
/// Ebbinghaus-curve scorer.
#[derive(Clone, Debug, Default)]
pub struct EbbinghausScorer {
cfg: ForgetConfig,
}
impl EbbinghausScorer {
/// Build a scorer from a [`ForgetConfig`].
#[must_use]
pub fn new(cfg: ForgetConfig) -> Self {
Self { cfg }
}
/// Compute stability after `access_count` reviews.
fn stability(&self, access_count: u32) -> f64 {
let mut s = self.cfg.initial_stability_ms;
// Each review multiplies stability by `(1 + reinforcement)`.
let mul = 1.0 + self.cfg.reinforcement;
for _ in 0..access_count {
s *= mul;
}
s
}
/// Retention `R = exp(-dt / S)` clamped into `[0.0, 1.0]`.
pub fn retention(&self, last_access_ms: u64, now_ms: u64, access_count: u32) -> f64 {
let dt = (now_ms.saturating_sub(last_access_ms)) as f64;
let s = self.stability(access_count);
let r = (-dt / s).exp();
if r < 0.0 {
0.0
} else if r > 1.0 {
1.0
} else {
r
}
}
/// True iff retention is below the configured threshold.
pub fn is_forgotten(&self, last_access_ms: u64, now_ms: u64, access_count: u32) -> bool {
self.retention(last_access_ms, now_ms, access_count) < self.cfg.threshold
}
/// Borrow the underlying config.
#[must_use]
pub fn config(&self) -> &ForgetConfig {
&self.cfg
}
}