Skip to main content

axon/
session_scope.rs

1//! Session Scoping — namespaced session isolation for AxonServer.
2//!
3//! Wraps multiple `SessionStore` instances keyed by scope name, providing
4//! isolation between flows, daemons, and the global server context.
5//!
6//! Scopes:
7//!   - `"global"` — default scope for server-wide state
8//!   - `"flow:<name>"` — per-flow session isolation
9//!   - `"daemon:<name>"` — per-daemon session isolation
10//!   - Custom string — any arbitrary namespace
11//!
12//! Each scope gets its own ephemeral memory and persistent store.
13
14use std::collections::HashMap;
15use crate::session_store::{SessionStore, MemoryEntry};
16
17// ── Scoped key ───────────────────────────────────────────────────────────
18
19/// The default scope name.
20pub const DEFAULT_SCOPE: &str = "global";
21
22/// Build a scope name for a flow.
23pub fn flow_scope(flow_name: &str) -> String {
24    format!("flow:{}", flow_name)
25}
26
27/// Build a scope name for a daemon.
28pub fn daemon_scope(daemon_name: &str) -> String {
29    format!("daemon:{}", daemon_name)
30}
31
32// ── Manager ──────────────────────────────────────────────────────────────
33
34/// Manages multiple SessionStore instances, one per scope.
35#[derive(Debug)]
36pub struct ScopedSessionManager {
37    /// Base directory for persistent stores.
38    base_path: String,
39    /// Map of scope name → SessionStore.
40    scopes: HashMap<String, SessionStore>,
41}
42
43impl ScopedSessionManager {
44    /// Create a new scoped session manager.
45    /// `base_path` is used as the root for deriving per-scope store file paths.
46    pub fn new(base_path: &str) -> Self {
47        let mut mgr = ScopedSessionManager {
48            base_path: base_path.to_string(),
49            scopes: HashMap::new(),
50        };
51        // Pre-create the global scope
52        mgr.ensure_scope(DEFAULT_SCOPE);
53        mgr
54    }
55
56    /// Get or create the SessionStore for a scope.
57    fn ensure_scope(&mut self, scope: &str) -> &mut SessionStore {
58        if !self.scopes.contains_key(scope) {
59            let source = format!("{}__{}", self.base_path, scope.replace(':', "_"));
60            let store = SessionStore::new(&source);
61            self.scopes.insert(scope.to_string(), store);
62        }
63        self.scopes.get_mut(scope).unwrap()
64    }
65
66    /// Get the SessionStore for a scope (read-only). Returns None if scope doesn't exist.
67    fn get_scope(&self, scope: &str) -> Option<&SessionStore> {
68        self.scopes.get(scope)
69    }
70
71    // ── Delegated operations ─────────────────────────────────────────────
72
73    /// Remember a value in a scope's ephemeral memory.
74    pub fn remember(&mut self, scope: &str, key: &str, value: &str, source_step: &str) {
75        self.ensure_scope(scope).remember(key, value, source_step);
76    }
77
78    /// Recall a value from a scope's ephemeral memory.
79    pub fn recall(&mut self, scope: &str, key: &str) -> Option<MemoryEntry> {
80        self.ensure_scope(scope).recall(key).cloned()
81    }
82
83    /// Persist a value in a scope's file-backed store.
84    pub fn persist(&mut self, scope: &str, key: &str, value: &str, source_step: &str) {
85        self.ensure_scope(scope).persist(key, value, source_step);
86    }
87
88    /// Retrieve a value from a scope's file-backed store.
89    pub fn retrieve(&mut self, scope: &str, key: &str) -> Option<MemoryEntry> {
90        self.ensure_scope(scope).retrieve(key).cloned()
91    }
92
93    /// Query entries in a scope's store.
94    pub fn query(&mut self, scope: &str, query: &str) -> Vec<MemoryEntry> {
95        self.ensure_scope(scope)
96            .retrieve_query(query)
97            .into_iter()
98            .cloned()
99            .collect()
100    }
101
102    /// Mutate an entry in a scope's store.
103    pub fn mutate(&mut self, scope: &str, key: &str, new_value: &str, source_step: &str) -> bool {
104        self.ensure_scope(scope).mutate(key, new_value, source_step)
105    }
106
107    /// Purge an entry from a scope's store.
108    pub fn purge(&mut self, scope: &str, key: &str) -> bool {
109        self.ensure_scope(scope).purge(key)
110    }
111
112    /// Flush a scope's persistent store to disk.
113    pub fn flush(&mut self, scope: &str) -> Result<(), String> {
114        self.ensure_scope(scope).flush()
115    }
116
117    /// Flush all scopes to disk.
118    pub fn flush_all(&mut self) -> Vec<(String, Result<(), String>)> {
119        let scope_names: Vec<String> = self.scopes.keys().cloned().collect();
120        let mut results = Vec::new();
121        for name in scope_names {
122            let result = self.scopes.get(&name).map(|s| s.flush()).unwrap_or(Ok(()));
123            results.push((name, result));
124        }
125        results
126    }
127
128    // ── Introspection ────────────────────────────────────────────────────
129
130    /// List all scope names.
131    pub fn list_scopes(&self) -> Vec<&str> {
132        let mut names: Vec<&str> = self.scopes.keys().map(|s| s.as_str()).collect();
133        names.sort();
134        names
135    }
136
137    /// Number of active scopes.
138    pub fn scope_count(&self) -> usize {
139        self.scopes.len()
140    }
141
142    /// Memory count for a scope (0 if scope doesn't exist).
143    pub fn memory_count(&self, scope: &str) -> usize {
144        self.get_scope(scope).map(|s| s.memory_count()).unwrap_or(0)
145    }
146
147    /// Store count for a scope (0 if scope doesn't exist).
148    pub fn store_count(&self, scope: &str) -> usize {
149        self.get_scope(scope).map(|s| s.store_count()).unwrap_or(0)
150    }
151
152    /// Total memory count across all scopes.
153    pub fn total_memory_count(&self) -> usize {
154        self.scopes.values().map(|s| s.memory_count()).sum()
155    }
156
157    /// Total store count across all scopes.
158    pub fn total_store_count(&self) -> usize {
159        self.scopes.values().map(|s| s.store_count()).sum()
160    }
161
162    /// List all entries in a scope (ephemeral + persistent).
163    pub fn list_entries(&mut self, scope: &str) -> Vec<ScopedEntry> {
164        let store = self.ensure_scope(scope);
165        let mut entries = Vec::new();
166
167        for entry in store.memory_entries() {
168            entries.push(ScopedEntry {
169                scope: scope.to_string(),
170                layer: "memory".to_string(),
171                key: entry.key.clone(),
172                value: entry.value.clone(),
173                timestamp: entry.timestamp,
174                source_step: entry.source_step.clone(),
175            });
176        }
177
178        // For persistent entries, we iterate the retrieve_query with empty to get all
179        // But retrieve_query needs a query string; use a broad match
180        // Instead, we'll check store_count and query
181        // Actually, let's just expose what we can
182        entries
183    }
184
185    /// Summary of all scopes for display/API.
186    pub fn summary(&self) -> Vec<ScopeSummary> {
187        let mut result = Vec::new();
188        for (name, store) in &self.scopes {
189            result.push(ScopeSummary {
190                scope: name.clone(),
191                memory_count: store.memory_count(),
192                store_count: store.store_count(),
193            });
194        }
195        result.sort_by(|a, b| a.scope.cmp(&b.scope));
196        result
197    }
198}
199
200/// A session entry with scope context.
201#[derive(Debug, Clone, serde::Serialize)]
202pub struct ScopedEntry {
203    pub scope: String,
204    pub layer: String,
205    pub key: String,
206    pub value: String,
207    pub timestamp: u64,
208    pub source_step: String,
209}
210
211/// Summary info for a scope.
212#[derive(Debug, Clone, serde::Serialize)]
213pub struct ScopeSummary {
214    pub scope: String,
215    pub memory_count: usize,
216    pub store_count: usize,
217}
218
219// ── Tests ────────────────────────────────────────────────────────────────
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224
225    fn test_manager(name: &str) -> ScopedSessionManager {
226        let base = std::env::temp_dir().join(format!("axon_scope_test_{name}"));
227        ScopedSessionManager::new(base.to_str().unwrap())
228    }
229
230    #[test]
231    fn default_scope_created() {
232        let mgr = test_manager("default");
233        assert!(mgr.list_scopes().contains(&"global"));
234        assert_eq!(mgr.scope_count(), 1);
235    }
236
237    #[test]
238    fn remember_recall_in_scope() {
239        let mut mgr = test_manager("rem_recall");
240        mgr.remember("flow:analyze", "result", "42", "step1");
241        let entry = mgr.recall("flow:analyze", "result").unwrap();
242        assert_eq!(entry.value, "42");
243
244        // Not visible in other scope
245        assert!(mgr.recall("global", "result").is_none());
246        assert!(mgr.recall("flow:other", "result").is_none());
247    }
248
249    #[test]
250    fn persist_retrieve_in_scope() {
251        let mut mgr = test_manager("persist_ret");
252        mgr.persist("daemon:worker", "config", "max_retries=3", "init");
253        let entry = mgr.retrieve("daemon:worker", "config").unwrap();
254        assert_eq!(entry.value, "max_retries=3");
255
256        // Not visible in global
257        assert!(mgr.retrieve("global", "config").is_none());
258    }
259
260    #[test]
261    fn mutate_in_scope() {
262        let mut mgr = test_manager("mutate");
263        mgr.persist("flow:a", "key", "old", "s1");
264        assert!(mgr.mutate("flow:a", "key", "new", "s2"));
265        assert_eq!(mgr.retrieve("flow:a", "key").unwrap().value, "new");
266
267        // Mutate in wrong scope fails
268        assert!(!mgr.mutate("flow:b", "key", "val", "s3"));
269    }
270
271    #[test]
272    fn purge_in_scope() {
273        let mut mgr = test_manager("purge");
274        mgr.persist("flow:x", "temp", "data", "s1");
275        assert!(mgr.purge("flow:x", "temp"));
276        assert!(mgr.retrieve("flow:x", "temp").is_none());
277
278        // Purge in wrong scope
279        mgr.persist("flow:y", "keep", "data", "s1");
280        assert!(!mgr.purge("flow:z", "keep"));
281        assert!(mgr.retrieve("flow:y", "keep").is_some());
282    }
283
284    #[test]
285    fn query_in_scope() {
286        let mut mgr = test_manager("query");
287        mgr.persist("flow:a", "analysis_1", "result1", "s1");
288        mgr.persist("flow:a", "analysis_2", "result2", "s2");
289        mgr.persist("flow:b", "analysis_3", "result3", "s3");
290
291        let results = mgr.query("flow:a", "analysis");
292        assert_eq!(results.len(), 2);
293
294        // Different scope
295        let results_b = mgr.query("flow:b", "analysis");
296        assert_eq!(results_b.len(), 1);
297    }
298
299    #[test]
300    fn scope_counts() {
301        let mut mgr = test_manager("counts");
302        mgr.remember("global", "a", "1", "s");
303        mgr.remember("global", "b", "2", "s");
304        mgr.persist("flow:x", "c", "3", "s");
305
306        assert_eq!(mgr.memory_count("global"), 2);
307        assert_eq!(mgr.store_count("flow:x"), 1);
308        assert_eq!(mgr.total_memory_count(), 2);
309        assert_eq!(mgr.total_store_count(), 1);
310    }
311
312    #[test]
313    fn list_scopes_sorted() {
314        let mut mgr = test_manager("list");
315        mgr.remember("flow:z", "k", "v", "s");
316        mgr.remember("daemon:a", "k", "v", "s");
317        let scopes = mgr.list_scopes();
318        assert!(scopes.len() >= 3);
319        // Should be sorted
320        for i in 1..scopes.len() {
321            assert!(scopes[i - 1] <= scopes[i]);
322        }
323    }
324
325    #[test]
326    fn summary_includes_all_scopes() {
327        let mut mgr = test_manager("summary");
328        mgr.remember("global", "a", "1", "s");
329        mgr.persist("flow:report", "b", "2", "s");
330
331        let summary = mgr.summary();
332        assert!(summary.len() >= 2);
333
334        let global = summary.iter().find(|s| s.scope == "global").unwrap();
335        assert_eq!(global.memory_count, 1);
336
337        let flow = summary.iter().find(|s| s.scope == "flow:report").unwrap();
338        assert_eq!(flow.store_count, 1);
339    }
340
341    #[test]
342    fn flow_scope_and_daemon_scope_helpers() {
343        assert_eq!(flow_scope("analyze"), "flow:analyze");
344        assert_eq!(daemon_scope("worker"), "daemon:worker");
345    }
346
347    #[test]
348    fn isolated_scopes_no_leakage() {
349        let mut mgr = test_manager("isolation");
350
351        // Same key in three different scopes
352        mgr.remember("global", "status", "global_val", "s");
353        mgr.remember("flow:a", "status", "flow_a_val", "s");
354        mgr.remember("daemon:d", "status", "daemon_val", "s");
355
356        assert_eq!(mgr.recall("global", "status").unwrap().value, "global_val");
357        assert_eq!(mgr.recall("flow:a", "status").unwrap().value, "flow_a_val");
358        assert_eq!(mgr.recall("daemon:d", "status").unwrap().value, "daemon_val");
359    }
360
361    #[test]
362    fn nonexistent_scope_returns_zero_counts() {
363        let mgr = test_manager("nonexist");
364        assert_eq!(mgr.memory_count("flow:phantom"), 0);
365        assert_eq!(mgr.store_count("daemon:ghost"), 0);
366    }
367
368    #[test]
369    fn scoped_entry_serializes() {
370        let entry = ScopedEntry {
371            scope: "flow:test".into(),
372            layer: "memory".into(),
373            key: "k".into(),
374            value: "v".into(),
375            timestamp: 12345,
376            source_step: "s1".into(),
377        };
378        let json = serde_json::to_string(&entry).unwrap();
379        assert!(json.contains("\"scope\":\"flow:test\""));
380        assert!(json.contains("\"layer\":\"memory\""));
381    }
382
383    #[test]
384    fn scope_summary_serializes() {
385        let summary = ScopeSummary {
386            scope: "global".into(),
387            memory_count: 3,
388            store_count: 1,
389        };
390        let json = serde_json::to_string(&summary).unwrap();
391        assert!(json.contains("\"scope\":\"global\""));
392        assert!(json.contains("\"memory_count\":3"));
393    }
394}