claude_code_acp/session/
manager.rs

1//! Session manager for tracking active sessions
2//!
3//! Uses DashMap for concurrent access with entry API to avoid deadlocks.
4
5use std::path::PathBuf;
6use std::sync::Arc;
7
8use dashmap::DashMap;
9
10use crate::types::{AgentConfig, AgentError, NewSessionMeta, Result};
11
12use super::session::Session;
13
14/// Manager for active sessions
15///
16/// Provides thread-safe session storage and lookup using DashMap.
17/// Uses entry API for atomic operations to prevent deadlocks.
18#[derive(Debug, Default)]
19pub struct SessionManager {
20    /// Active sessions keyed by session_id
21    sessions: DashMap<String, Arc<Session>>,
22}
23
24impl SessionManager {
25    /// Create a new session manager
26    pub fn new() -> Self {
27        Self {
28            sessions: DashMap::new(),
29        }
30    }
31
32    /// Create a new session and store it
33    ///
34    /// # Arguments
35    ///
36    /// * `session_id` - Unique identifier for the session
37    /// * `cwd` - Working directory for the session
38    /// * `config` - Agent configuration
39    /// * `meta` - Optional session metadata
40    ///
41    /// # Returns
42    ///
43    /// Arc reference to the created session
44    pub fn create_session(
45        &self,
46        session_id: String,
47        cwd: PathBuf,
48        config: &AgentConfig,
49        meta: Option<&NewSessionMeta>,
50    ) -> Result<Arc<Session>> {
51        // Use entry API to atomically check and insert
52        let entry = self.sessions.entry(session_id.clone());
53
54        match entry {
55            dashmap::Entry::Occupied(_) => {
56                // Session already exists
57                Err(AgentError::SessionAlreadyExists(session_id))
58            }
59            dashmap::Entry::Vacant(vacant) => {
60                // Session::new() now directly returns Arc<Session>
61                let arc_session = Session::new(session_id, cwd, config, meta)?;
62                vacant.insert(Arc::clone(&arc_session));
63                Ok(arc_session)
64            }
65        }
66    }
67
68    /// Get an existing session
69    pub fn get_session(&self, session_id: &str) -> Option<Arc<Session>> {
70        self.sessions.get(session_id).map(|r| Arc::clone(&r))
71    }
72
73    /// Get an existing session or return SessionNotFound error
74    pub fn get_session_or_error(&self, session_id: &str) -> Result<Arc<Session>> {
75        self.get_session(session_id)
76            .ok_or_else(|| AgentError::SessionNotFound(session_id.to_string()))
77    }
78
79    /// Remove a session
80    pub fn remove_session(&self, session_id: &str) -> Option<Arc<Session>> {
81        self.sessions.remove(session_id).map(|(_, v)| v)
82    }
83
84    /// Check if a session exists
85    pub fn has_session(&self, session_id: &str) -> bool {
86        self.sessions.contains_key(session_id)
87    }
88
89    /// Get the number of active sessions
90    pub fn session_count(&self) -> usize {
91        self.sessions.len()
92    }
93
94    /// Get all session IDs
95    pub fn session_ids(&self) -> Vec<String> {
96        self.sessions.iter().map(|r| r.key().clone()).collect()
97    }
98
99    /// Clear all sessions
100    pub fn clear(&self) {
101        self.sessions.clear();
102    }
103
104    /// Execute a function on a session if it exists
105    ///
106    /// Uses entry API to safely access the session without holding the lock
107    pub fn with_session<F, R>(&self, session_id: &str, f: F) -> Option<R>
108    where
109        F: FnOnce(&Arc<Session>) -> R,
110    {
111        self.sessions.get(session_id).map(|r| f(&r))
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    fn test_config() -> AgentConfig {
120        AgentConfig {
121            base_url: None,
122            api_key: None,
123            model: None,
124            small_fast_model: None,
125            max_thinking_tokens: None,
126        }
127    }
128
129    #[test]
130    fn test_manager_new() {
131        let manager = SessionManager::new();
132        assert_eq!(manager.session_count(), 0);
133    }
134
135    #[test]
136    fn test_manager_create_session() {
137        let manager = SessionManager::new();
138        let config = test_config();
139
140        let session = manager
141            .create_session(
142                "session-1".to_string(),
143                PathBuf::from("/tmp"),
144                &config,
145                None,
146            )
147            .unwrap();
148
149        assert_eq!(session.session_id, "session-1");
150        assert_eq!(manager.session_count(), 1);
151        assert!(manager.has_session("session-1"));
152    }
153
154    #[test]
155    fn test_manager_get_session() {
156        let manager = SessionManager::new();
157        let config = test_config();
158
159        manager
160            .create_session(
161                "session-1".to_string(),
162                PathBuf::from("/tmp"),
163                &config,
164                None,
165            )
166            .unwrap();
167
168        let session = manager.get_session("session-1");
169        assert!(session.is_some());
170        assert_eq!(session.unwrap().session_id, "session-1");
171
172        let missing = manager.get_session("nonexistent");
173        assert!(missing.is_none());
174    }
175
176    #[test]
177    fn test_manager_get_session_or_error() {
178        let manager = SessionManager::new();
179        let config = test_config();
180
181        manager
182            .create_session(
183                "session-1".to_string(),
184                PathBuf::from("/tmp"),
185                &config,
186                None,
187            )
188            .unwrap();
189
190        let result = manager.get_session_or_error("session-1");
191        assert!(result.is_ok());
192
193        let error = manager.get_session_or_error("nonexistent");
194        assert!(matches!(error, Err(AgentError::SessionNotFound(_))));
195    }
196
197    #[test]
198    fn test_manager_remove_session() {
199        let manager = SessionManager::new();
200        let config = test_config();
201
202        manager
203            .create_session(
204                "session-1".to_string(),
205                PathBuf::from("/tmp"),
206                &config,
207                None,
208            )
209            .unwrap();
210
211        assert!(manager.has_session("session-1"));
212
213        let removed = manager.remove_session("session-1");
214        assert!(removed.is_some());
215        assert!(!manager.has_session("session-1"));
216        assert_eq!(manager.session_count(), 0);
217    }
218
219    #[test]
220    fn test_manager_duplicate_session() {
221        let manager = SessionManager::new();
222        let config = test_config();
223
224        manager
225            .create_session(
226                "session-1".to_string(),
227                PathBuf::from("/tmp"),
228                &config,
229                None,
230            )
231            .unwrap();
232
233        let duplicate = manager.create_session(
234            "session-1".to_string(),
235            PathBuf::from("/tmp"),
236            &config,
237            None,
238        );
239
240        assert!(matches!(
241            duplicate,
242            Err(AgentError::SessionAlreadyExists(_))
243        ));
244    }
245
246    #[test]
247    fn test_manager_session_ids() {
248        let manager = SessionManager::new();
249        let config = test_config();
250
251        manager
252            .create_session(
253                "session-1".to_string(),
254                PathBuf::from("/tmp"),
255                &config,
256                None,
257            )
258            .unwrap();
259        manager
260            .create_session(
261                "session-2".to_string(),
262                PathBuf::from("/tmp"),
263                &config,
264                None,
265            )
266            .unwrap();
267
268        let ids = manager.session_ids();
269        assert_eq!(ids.len(), 2);
270        assert!(ids.contains(&"session-1".to_string()));
271        assert!(ids.contains(&"session-2".to_string()));
272    }
273
274    #[test]
275    fn test_manager_clear() {
276        let manager = SessionManager::new();
277        let config = test_config();
278
279        manager
280            .create_session(
281                "session-1".to_string(),
282                PathBuf::from("/tmp"),
283                &config,
284                None,
285            )
286            .unwrap();
287        manager
288            .create_session(
289                "session-2".to_string(),
290                PathBuf::from("/tmp"),
291                &config,
292                None,
293            )
294            .unwrap();
295
296        assert_eq!(manager.session_count(), 2);
297
298        manager.clear();
299        assert_eq!(manager.session_count(), 0);
300    }
301
302    #[test]
303    fn test_manager_with_session() {
304        let manager = SessionManager::new();
305        let config = test_config();
306
307        manager
308            .create_session(
309                "session-1".to_string(),
310                PathBuf::from("/tmp"),
311                &config,
312                None,
313            )
314            .unwrap();
315
316        let result = manager.with_session("session-1", |session| session.session_id.clone());
317
318        assert_eq!(result, Some("session-1".to_string()));
319
320        let missing = manager.with_session("nonexistent", |_| "found");
321        assert!(missing.is_none());
322    }
323}