Skip to main content

cel_memory/
ops.rs

1//! Operational types — eviction, access logging, export bundles, reports.
2//!
3//! These are the supporting types for [`crate::MemoryProvider`]'s maintenance
4//! and audit surface (`delete`, `delete_matching`, `purge_all`, `export`,
5//! `run_aging_sweep`, `re_embed_all`, `record_access`, `stats`).
6
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10
11use crate::chunk::MemoryChunk;
12use crate::query::MemoryPredicate;
13use crate::session::MemorySession;
14
15/// Why a chunk was evicted. Recorded in `memory_eviction_log` and reported in
16/// [`AgingReport`].
17#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
18#[serde(rename_all = "snake_case")]
19pub enum EvictionReason {
20    /// The user explicitly requested deletion.
21    UserDelete,
22    /// The chunk aged past its per-kind retention horizon.
23    Aging,
24    /// The chunk's importance fell below the eviction threshold during a
25    /// storage-cap sweep.
26    LowImportance,
27    /// A `redact_memory` rule matched the chunk and elected to remove it.
28    RedactRule,
29    /// Storage cap exceeded; lowest-importance unpinned non-correction non-fire
30    /// chunks were evicted oldest-first.
31    StorageCap,
32    /// A `purge_all` call by the user wiped everything.
33    PurgeAll,
34}
35
36/// One entry in the eviction audit log.
37#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
38pub struct EvictionEntry {
39    /// When the eviction happened.
40    pub ts: DateTime<Utc>,
41    /// ID of the evicted chunk.
42    pub chunk_id: String,
43    /// Why it was evicted.
44    pub reason: EvictionReason,
45    /// Optional structured context.
46    #[serde(default)]
47    pub metadata: Value,
48}
49
50/// One entry in the retrieval access log. Used for relevance learning and the
51/// recall@k benchmark.
52#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
53pub struct AccessEntry {
54    /// When the access happened.
55    pub ts: DateTime<Utc>,
56    /// ID of the chunk that was returned.
57    pub chunk_id: String,
58    /// Caller string used by the consuming subsystem (`"agent"`, `"compiler"`,
59    /// `"audit"`, `"user"`).
60    pub retrieved_by: String,
61    /// Stable hash of the retrieval query — group rows produced by the same
62    /// query call.
63    pub query_hash: String,
64    /// Position in the returned result list (0-indexed).
65    pub rank: usize,
66    /// True if the agent went on to cite or act on this chunk.
67    pub used: bool,
68}
69
70/// Filter for [`crate::MemoryProvider::export`].
71#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
72pub struct ExportFilter {
73    /// Optional predicate over chunks. `None` includes everything.
74    #[serde(default)]
75    pub predicate: Option<MemoryPredicate>,
76    /// Include the eviction log in the bundle.
77    #[serde(default)]
78    pub include_eviction_log: bool,
79    /// Include the access log in the bundle.
80    #[serde(default)]
81    pub include_access_log: bool,
82    /// Include closed sessions whose chunks are included.
83    #[serde(default = "default_true")]
84    pub include_sessions: bool,
85}
86
87fn default_true() -> bool {
88    true
89}
90
91/// Result of [`crate::MemoryProvider::export`]. JSON-serializable; the caller is
92/// responsible for writing it to disk as `.tar.gz`, `.json`, or another archive
93/// format.
94#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
95pub struct ExportBundle {
96    /// Exported chunks.
97    pub chunks: Vec<MemoryChunk>,
98    /// Sessions referenced by the chunks (when `include_sessions=true`).
99    #[serde(default)]
100    pub sessions: Vec<MemorySession>,
101    /// Eviction log entries (when `include_eviction_log=true`).
102    #[serde(default)]
103    pub evictions: Vec<EvictionEntry>,
104    /// Access log entries (when `include_access_log=true`).
105    #[serde(default)]
106    pub accesses: Vec<AccessEntry>,
107}
108
109/// Report returned by [`crate::MemoryProvider::purge_all`].
110#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
111pub struct PurgeReport {
112    /// Number of chunks deleted.
113    pub chunks_deleted: usize,
114    /// Number of sessions deleted.
115    pub sessions_deleted: usize,
116    /// Number of access-log entries deleted.
117    pub access_log_deleted: usize,
118    /// Number of eviction-log entries deleted.
119    pub eviction_log_deleted: usize,
120}
121
122/// Report returned by [`crate::MemoryProvider::run_aging_sweep`].
123#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
124pub struct AgingReport {
125    /// Chunks transitioned from session tier to long-term tier.
126    pub tier_promoted: usize,
127    /// Chunks deleted due to aging or storage cap.
128    pub deleted: usize,
129    /// Total bytes reclaimed (best-effort estimate by the storage layer).
130    pub bytes_reclaimed: u64,
131    /// Per-reason deletion counts.
132    #[serde(default)]
133    pub deletions_by_reason: Vec<(EvictionReason, usize)>,
134}
135
136/// Report returned by [`crate::MemoryProvider::re_embed_all`].
137#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
138pub struct ReEmbedReport {
139    /// Total chunks targeted.
140    pub total: usize,
141    /// Chunks successfully re-embedded.
142    pub succeeded: usize,
143    /// Chunks that failed (model error, content too large, etc.).
144    pub failed: usize,
145    /// Wall-clock time spent, in milliseconds.
146    pub elapsed_ms: u64,
147}
148
149/// Summary statistics returned by [`crate::MemoryProvider::stats`]. Useful for
150/// dashboards, health checks, and storage monitoring.
151#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
152pub struct MemoryStats {
153    /// Total chunks across both tiers.
154    pub total_chunks: usize,
155    /// Chunks in the `session` tier.
156    pub session_chunks: usize,
157    /// Chunks in the `long_term` tier.
158    pub long_term_chunks: usize,
159    /// Total sessions (open + closed).
160    pub total_sessions: usize,
161    /// Currently open sessions.
162    pub open_sessions: usize,
163    /// On-disk database size in bytes (best-effort).
164    pub db_bytes: u64,
165    /// Name of the embedding model in use, if any.
166    pub embedding_model: Option<String>,
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172    use serde_json::json;
173
174    #[test]
175    fn eviction_reason_serializes_snake_case() {
176        assert_eq!(
177            serde_json::to_value(EvictionReason::StorageCap).unwrap(),
178            json!("storage_cap")
179        );
180        assert_eq!(
181            serde_json::to_value(EvictionReason::UserDelete).unwrap(),
182            json!("user_delete")
183        );
184    }
185
186    #[test]
187    fn export_filter_defaults_include_sessions() {
188        let raw = "{}";
189        let f: ExportFilter = serde_json::from_str(raw).unwrap();
190        assert!(f.include_sessions);
191        assert!(!f.include_access_log);
192        assert!(!f.include_eviction_log);
193    }
194
195    #[test]
196    fn stats_round_trip() {
197        let s = MemoryStats {
198            total_chunks: 100,
199            session_chunks: 30,
200            long_term_chunks: 70,
201            total_sessions: 5,
202            open_sessions: 1,
203            db_bytes: 1024 * 1024,
204            embedding_model: Some("bge-small-en-v1.5".into()),
205        };
206        let raw = serde_json::to_string(&s).unwrap();
207        let back: MemoryStats = serde_json::from_str(&raw).unwrap();
208        assert_eq!(s, back);
209    }
210}