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
92/// (Memory tab UI or `cellar memory export` CLI) is responsible for writing it
93/// to disk as `.tar.gz` or `.json`.
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`]. Drives the
150/// Memory tab dashboard and `cellar doctor`'s memory health check.
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}