Skip to main content

ruv_neural_memory/
session.rs

1//! Session-based memory management for grouping embeddings by recording session.
2
3use std::collections::HashMap;
4
5use serde::{Deserialize, Serialize};
6
7use ruv_neural_core::embedding::NeuralEmbedding;
8use ruv_neural_core::error::{Result, RuvNeuralError};
9use ruv_neural_core::topology::CognitiveState;
10
11use crate::store::NeuralMemoryStore;
12
13/// Metadata for a recording session.
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct SessionMetadata {
16    /// Unique session identifier.
17    pub session_id: String,
18    /// Subject being recorded.
19    pub subject_id: String,
20    /// Session start time (Unix timestamp).
21    pub start_time: f64,
22    /// Session end time (None if still active).
23    pub end_time: Option<f64>,
24    /// Number of embeddings stored during this session.
25    pub num_embeddings: usize,
26    /// Cognitive states observed during the session.
27    pub cognitive_states_observed: Vec<CognitiveState>,
28}
29
30/// Manages neural memory across recording sessions.
31pub struct SessionMemory {
32    /// Underlying embedding store.
33    store: NeuralMemoryStore,
34    /// Currently active session ID.
35    current_session: Option<String>,
36    /// Metadata for all sessions.
37    session_metadata: HashMap<String, SessionMetadata>,
38    /// Maps session_id to embedding indices.
39    session_indices: HashMap<String, Vec<usize>>,
40    /// Counter for generating session IDs.
41    session_counter: u64,
42}
43
44impl SessionMemory {
45    /// Create a new session memory with the given store capacity.
46    pub fn new(capacity: usize) -> Self {
47        Self {
48            store: NeuralMemoryStore::new(capacity),
49            current_session: None,
50            session_metadata: HashMap::new(),
51            session_indices: HashMap::new(),
52            session_counter: 0,
53        }
54    }
55
56    /// Start a new recording session, returning its unique ID.
57    ///
58    /// If a session is already active, it is automatically ended first.
59    pub fn start_session(&mut self, subject_id: &str) -> String {
60        if self.current_session.is_some() {
61            self.end_session();
62        }
63
64        self.session_counter += 1;
65        let session_id = format!("session-{:04}", self.session_counter);
66
67        let metadata = SessionMetadata {
68            session_id: session_id.clone(),
69            subject_id: subject_id.to_string(),
70            start_time: 0.0, // Will be updated on first embedding
71            end_time: None,
72            num_embeddings: 0,
73            cognitive_states_observed: Vec::new(),
74        };
75
76        self.session_metadata
77            .insert(session_id.clone(), metadata);
78        self.session_indices
79            .insert(session_id.clone(), Vec::new());
80        self.current_session = Some(session_id.clone());
81
82        session_id
83    }
84
85    /// End the current recording session.
86    pub fn end_session(&mut self) {
87        if let Some(ref session_id) = self.current_session.clone() {
88            if let Some(meta) = self.session_metadata.get_mut(session_id) {
89                // Set end time from the last embedding's timestamp
90                if let Some(indices) = self.session_indices.get(session_id) {
91                    if let Some(&last_idx) = indices.last() {
92                        if let Some(emb) = self.store.get(last_idx) {
93                            meta.end_time = Some(emb.timestamp);
94                        }
95                    }
96                }
97            }
98        }
99        self.current_session = None;
100    }
101
102    /// Store an embedding in the current session.
103    ///
104    /// Returns an error if no session is active.
105    pub fn store(&mut self, embedding: NeuralEmbedding) -> Result<usize> {
106        let session_id = self
107            .current_session
108            .clone()
109            .ok_or_else(|| RuvNeuralError::Memory("No active session".into()))?;
110
111        let timestamp = embedding.timestamp;
112        let state = embedding.metadata.cognitive_state;
113        let idx = self.store.store(embedding)?;
114
115        // Update session metadata
116        if let Some(meta) = self.session_metadata.get_mut(&session_id) {
117            if meta.num_embeddings == 0 {
118                meta.start_time = timestamp;
119            }
120            meta.num_embeddings += 1;
121
122            if let Some(s) = state {
123                if !meta.cognitive_states_observed.contains(&s) {
124                    meta.cognitive_states_observed.push(s);
125                }
126            }
127        }
128
129        if let Some(indices) = self.session_indices.get_mut(&session_id) {
130            indices.push(idx);
131        }
132
133        Ok(idx)
134    }
135
136    /// Get all embeddings from a specific session.
137    pub fn get_session_history(&self, session_id: &str) -> Vec<&NeuralEmbedding> {
138        match self.session_indices.get(session_id) {
139            Some(indices) => indices
140                .iter()
141                .filter_map(|&i| self.store.get(i))
142                .collect(),
143            None => Vec::new(),
144        }
145    }
146
147    /// Get all embeddings for a given subject across all sessions.
148    pub fn get_subject_history(&self, subject_id: &str) -> Vec<&NeuralEmbedding> {
149        self.store.query_by_subject(subject_id)
150    }
151
152    /// Get metadata for a session.
153    pub fn get_session_metadata(&self, session_id: &str) -> Option<&SessionMetadata> {
154        self.session_metadata.get(session_id)
155    }
156
157    /// Get the current active session ID.
158    pub fn current_session_id(&self) -> Option<&str> {
159        self.current_session.as_deref()
160    }
161
162    /// Access the underlying store.
163    pub fn store_ref(&self) -> &NeuralMemoryStore {
164        &self.store
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171    use ruv_neural_core::brain::Atlas;
172    use ruv_neural_core::embedding::EmbeddingMetadata;
173
174    fn make_embedding(vector: Vec<f64>, subject: &str, timestamp: f64) -> NeuralEmbedding {
175        NeuralEmbedding::new(
176            vector,
177            timestamp,
178            EmbeddingMetadata {
179                subject_id: Some(subject.to_string()),
180                session_id: None,
181                cognitive_state: Some(CognitiveState::Rest),
182                source_atlas: Atlas::Schaefer100,
183                embedding_method: "test".to_string(),
184            },
185        )
186        .unwrap()
187    }
188
189    #[test]
190    fn session_lifecycle() {
191        let mut mem = SessionMemory::new(100);
192
193        // No session active
194        assert!(mem.current_session_id().is_none());
195
196        // Start session
197        let sid = mem.start_session("subj1");
198        assert_eq!(mem.current_session_id(), Some(sid.as_str()));
199
200        // Store embeddings
201        mem.store(make_embedding(vec![1.0, 0.0], "subj1", 1.0))
202            .unwrap();
203        mem.store(make_embedding(vec![0.0, 1.0], "subj1", 2.0))
204            .unwrap();
205
206        // Check session history
207        let history = mem.get_session_history(&sid);
208        assert_eq!(history.len(), 2);
209
210        // Check metadata
211        let meta = mem.get_session_metadata(&sid).unwrap();
212        assert_eq!(meta.num_embeddings, 2);
213        assert_eq!(meta.subject_id, "subj1");
214
215        // End session
216        mem.end_session();
217        assert!(mem.current_session_id().is_none());
218
219        let meta = mem.get_session_metadata(&sid).unwrap();
220        assert_eq!(meta.end_time, Some(2.0));
221    }
222
223    #[test]
224    fn store_without_session_fails() {
225        let mut mem = SessionMemory::new(100);
226        let result = mem.store(make_embedding(vec![1.0], "subj1", 0.0));
227        assert!(result.is_err());
228    }
229
230    #[test]
231    fn multiple_sessions() {
232        let mut mem = SessionMemory::new(100);
233
234        let s1 = mem.start_session("subj1");
235        mem.store(make_embedding(vec![1.0], "subj1", 1.0))
236            .unwrap();
237        mem.end_session();
238
239        let s2 = mem.start_session("subj1");
240        mem.store(make_embedding(vec![2.0], "subj1", 2.0))
241            .unwrap();
242        mem.store(make_embedding(vec![3.0], "subj1", 3.0))
243            .unwrap();
244        mem.end_session();
245
246        assert_eq!(mem.get_session_history(&s1).len(), 1);
247        assert_eq!(mem.get_session_history(&s2).len(), 2);
248
249        // Subject history spans all sessions
250        let subject_history = mem.get_subject_history("subj1");
251        assert_eq!(subject_history.len(), 3);
252    }
253
254    #[test]
255    fn starting_new_session_ends_previous() {
256        let mut mem = SessionMemory::new(100);
257
258        let s1 = mem.start_session("subj1");
259        mem.store(make_embedding(vec![1.0], "subj1", 1.0))
260            .unwrap();
261
262        // Starting a new session auto-ends the previous one
263        let _s2 = mem.start_session("subj2");
264
265        let meta = mem.get_session_metadata(&s1).unwrap();
266        assert!(meta.end_time.is_some());
267    }
268}