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]>
110 lines
3.4 KiB
Rust
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
|
|
}
|
|
}
|