#![allow(clippy::cast_possible_truncation)]
use parking_lot::RwLock;
use rustc_hash::FxHashMap;
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum MemoryKind {
Semantic,
Episodic,
Procedural,
}
impl MemoryKind {
const fn tag(self) -> u8 {
match self {
Self::Semantic => 0,
Self::Episodic => 1,
Self::Procedural => 2,
}
}
const fn from_tag(tag: u8) -> Option<Self> {
match tag {
0 => Some(Self::Semantic),
1 => Some(Self::Episodic),
2 => Some(Self::Procedural),
_ => None,
}
}
}
type TtlKey = (MemoryKind, u64);
#[derive(Debug, Clone)]
pub struct TtlEntry {
pub expires_at: u64,
pub created_at: u64,
}
pub struct MemoryTtl {
entries: RwLock<FxHashMap<TtlKey, TtlEntry>>,
}
impl Default for MemoryTtl {
fn default() -> Self {
Self::new()
}
}
impl MemoryTtl {
#[must_use]
pub fn new() -> Self {
Self {
entries: RwLock::new(FxHashMap::default()),
}
}
#[must_use]
pub fn now() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |d| d.as_secs())
}
pub fn set_ttl(&self, kind: MemoryKind, id: u64, ttl_seconds: u64) {
self.set_expiry(kind, id, Self::now().saturating_add(ttl_seconds));
}
pub fn set_expiry(&self, kind: MemoryKind, id: u64, expires_at: u64) {
let entry = TtlEntry {
expires_at,
created_at: Self::now(),
};
self.entries.write().insert((kind, id), entry);
}
pub fn remove(&self, kind: MemoryKind, id: u64) {
self.entries.write().remove(&(kind, id));
}
#[must_use]
pub fn get_expired(&self) -> Vec<TtlKey> {
let now = Self::now();
self.entries
.read()
.iter()
.filter(|(_, entry)| entry.expires_at <= now)
.map(|(&key, _)| key)
.collect()
}
#[must_use]
pub fn expired_count(&self, kind: MemoryKind) -> usize {
let now = Self::now();
self.entries
.read()
.iter()
.filter(|((k, _), entry)| *k == kind && entry.expires_at <= now)
.count()
}
pub fn expire(&self) -> Vec<TtlKey> {
let now = Self::now();
let mut entries = self.entries.write();
let expired: Vec<TtlKey> = entries
.iter()
.filter(|(_, entry)| entry.expires_at <= now)
.map(|(&key, _)| key)
.collect();
for key in &expired {
entries.remove(key);
}
expired
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.read().len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.read().is_empty()
}
#[must_use]
pub fn is_expired(&self, kind: MemoryKind, id: u64) -> bool {
let now = Self::now();
self.entries
.read()
.get(&(kind, id))
.is_some_and(|entry| entry.expires_at <= now)
}
#[must_use]
pub fn get(&self, kind: MemoryKind, id: u64) -> Option<TtlEntry> {
self.entries.read().get(&(kind, id)).cloned()
}
pub fn clear(&self) {
self.entries.write().clear();
}
pub fn replace_from(&self, other: &MemoryTtl) {
let other_entries = other.entries.read();
let mut self_entries = self.entries.write();
self_entries.clear();
for (&key, entry) in other_entries.iter() {
self_entries.insert(key, entry.clone());
}
}
const ENTRY_SIZE: usize = 1 + 8 + 8 + 8;
#[must_use]
pub fn serialize(&self) -> Vec<u8> {
let entries = self.entries.read();
let count = entries.len();
let mut buf = Vec::with_capacity(8 + count * Self::ENTRY_SIZE);
buf.extend_from_slice(&(count as u64).to_le_bytes());
for (&(kind, id), entry) in entries.iter() {
buf.push(kind.tag());
buf.extend_from_slice(&id.to_le_bytes());
buf.extend_from_slice(&entry.expires_at.to_le_bytes());
buf.extend_from_slice(&entry.created_at.to_le_bytes());
}
buf
}
#[must_use]
pub fn deserialize(data: &[u8]) -> Option<Self> {
let count = super::memory_helpers::validate_binary_header(data, Self::ENTRY_SIZE)?;
let mut entries = FxHashMap::default();
entries.reserve(count);
for i in 0..count {
let offset = 8 + i * Self::ENTRY_SIZE;
let kind = MemoryKind::from_tag(data[offset])?;
let id = u64::from_le_bytes(data[offset + 1..offset + 9].try_into().ok()?);
let expires_at = u64::from_le_bytes(data[offset + 9..offset + 17].try_into().ok()?);
let created_at = u64::from_le_bytes(data[offset + 17..offset + 25].try_into().ok()?);
entries.insert(
(kind, id),
TtlEntry {
expires_at,
created_at,
},
);
}
Some(Self {
entries: RwLock::new(entries),
})
}
}
#[derive(Debug, Default)]
pub struct ExpireResult {
pub semantic_expired: usize,
pub episodic_expired: usize,
pub procedural_expired: usize,
pub episodic_consolidated: usize,
pub procedural_evicted: usize,
pub consolidation_truncated: bool,
}
#[derive(Debug, Clone)]
pub struct EvictionConfig {
pub consolidation_age_threshold: u64,
pub min_confidence_threshold: f32,
pub max_entries_per_cycle: usize,
}
impl Default for EvictionConfig {
fn default() -> Self {
Self {
consolidation_age_threshold: 7 * 24 * 60 * 60, min_confidence_threshold: 0.1,
max_entries_per_cycle: 1000,
}
}
}