Skip to main content

post_cortex_mcp/
session.rs

1//! Session lifecycle: create, load, checkpoint, list, search, and metadata.
2
3use crate::{MCPToolResult, get_memory_system, string_to_anyhow};
4use anyhow::Result;
5use post_cortex_core::core::timeout_utils::with_storage_timeout;
6use post_cortex_core::session::active_session::ActiveSession;
7use post_cortex_memory::ConversationMemorySystem;
8use post_cortex_storage::rocksdb_storage::SessionCheckpoint;
9use std::sync::Arc;
10use tracing::{debug, error, instrument};
11use uuid::Uuid;
12
13/// Create a session checkpoint using an explicit memory system reference.
14pub async fn create_session_checkpoint_with_system(
15    session_id: Uuid,
16    system: &ConversationMemorySystem,
17) -> Result<MCPToolResult> {
18    let session_arc = system
19        .get_session(session_id)
20        .await
21        .map_err(|e| anyhow::anyhow!("Failed to load session: {}", e))?;
22    let session = session_arc.load();
23
24    let checkpoint = create_comprehensive_checkpoint(&session).await?;
25
26    system
27        .storage_actor
28        .save_checkpoint(&checkpoint)
29        .await
30        .map_err(string_to_anyhow)?;
31
32    Ok(MCPToolResult::success(
33        "Checkpoint created successfully".to_string(),
34        Some(serde_json::json!({ "checkpoint_id": checkpoint.id.to_string() })),
35    ))
36}
37
38/// Load a session from a previously saved checkpoint.
39pub async fn load_session_checkpoint_with_system(
40    checkpoint_id: String,
41    session_id: Uuid,
42    system: &ConversationMemorySystem,
43) -> Result<MCPToolResult> {
44    eprintln!("Loading checkpoint - step 1: Parsing checkpoint ID");
45    let checkpoint_id = Uuid::parse_str(&checkpoint_id)?;
46
47    eprintln!("Loading checkpoint - step 2: Loading checkpoint from storage");
48    let checkpoint = system
49        .storage_actor
50        .load_checkpoint(checkpoint_id)
51        .await
52        .map_err(string_to_anyhow)?;
53
54    eprintln!("Loading checkpoint - step 3: Checkpoint loaded successfully");
55
56    eprintln!("Loading checkpoint - step 4: Creating session from checkpoint");
57    let mut session = ActiveSession::new(session_id, None, None);
58
59    eprintln!("Loading checkpoint - step 5: Restoring current state");
60    session.current_state = Arc::new(checkpoint.structured_context);
61
62    eprintln!("Loading checkpoint - step 6: Restoring incremental updates");
63    session.set_incremental_updates(checkpoint.recent_updates);
64
65    eprintln!("Loading checkpoint - step 10: Restoring code references");
66    session.code_references = Arc::new(checkpoint.code_references);
67
68    eprintln!("Loading checkpoint - step 11: Restoring change history");
69    session.set_change_history(checkpoint.change_history);
70
71    eprintln!("Loading checkpoint - step 12: Entity graph restored");
72
73    eprintln!("Loading checkpoint - step 13: Adding session to session manager");
74    system.session_manager.install(session_id, session);
75
76    eprintln!("Loading checkpoint - step 14: Updated session manager");
77
78    Ok(MCPToolResult::success(
79        "Session loaded from checkpoint successfully".to_string(),
80        None,
81    ))
82}
83
84/// Create a session checkpoint using the global memory system.
85pub async fn create_session_checkpoint(session_id: Uuid) -> Result<MCPToolResult> {
86    let result = with_storage_timeout(async {
87        let system = get_memory_system().await?;
88        let session_arc = system
89            .get_session(session_id)
90            .await
91            .map_err(|e| anyhow::anyhow!("Failed to load session: {}", e))?;
92        let session = session_arc.load();
93
94        let checkpoint = create_comprehensive_checkpoint(&session).await?;
95
96        system
97            .storage_actor
98            .save_checkpoint(&checkpoint)
99            .await
100            .map_err(string_to_anyhow)?;
101
102        Ok(MCPToolResult::success(
103            "Checkpoint created successfully".to_string(),
104            Some(serde_json::json!({ "checkpoint_id": checkpoint.id.to_string() })),
105        ))
106    })
107    .await;
108
109    match result {
110        Ok(success_result) => success_result,
111        Err(timeout_error) => {
112            error!(
113                "TIMEOUT: create_session_checkpoint - session: {}, error: {}",
114                session_id, timeout_error
115            );
116            Ok(MCPToolResult::error(format!(
117                "Checkpoint creation timed out: {}",
118                timeout_error
119            )))
120        }
121    }
122}
123
124/// Load a session checkpoint using the global memory system.
125pub async fn load_session_checkpoint(
126    checkpoint_id: String,
127    session_id: Uuid,
128) -> Result<MCPToolResult> {
129    let result = with_storage_timeout(async {
130        let system = get_memory_system().await?;
131        let checkpoint_id = Uuid::parse_str(&checkpoint_id)?;
132
133        let checkpoint = system
134            .storage_actor
135            .load_checkpoint(checkpoint_id)
136            .await
137            .map_err(string_to_anyhow)?;
138
139        let mut session = ActiveSession::new(session_id, None, None);
140        session.current_state = Arc::new(checkpoint.structured_context);
141        session.set_incremental_updates(checkpoint.recent_updates);
142        session.code_references = Arc::new(checkpoint.code_references);
143        session.set_change_history(checkpoint.change_history);
144
145        system.session_manager.install(session_id, session);
146
147        Ok(MCPToolResult::success(
148            "Session loaded from checkpoint successfully".to_string(),
149            None,
150        ))
151    })
152    .await;
153
154    match result {
155        Ok(success_result) => success_result,
156        Err(timeout_error) => {
157            error!(
158                "TIMEOUT: load_session_checkpoint - session: {}, error: {}",
159                session_id, timeout_error
160            );
161            Ok(MCPToolResult::error(format!(
162                "Checkpoint loading timed out: {}",
163                timeout_error
164            )))
165        }
166    }
167}
168
169/// Mark a specific context update as important within a session.
170pub async fn mark_important(session_id: Uuid, update_id: String) -> Result<MCPToolResult> {
171    let update_id = Uuid::parse_str(&update_id)?;
172    let system = get_memory_system().await?;
173    let session_arc = system
174        .get_session(session_id)
175        .await
176        .map_err(|e| anyhow::anyhow!("Failed to load session: {}", e))?;
177
178    let mut found = false;
179    session_arc.rcu(|current| {
180        let mut updated = (**current).clone();
181        let updates = Arc::make_mut(&mut updated.incremental_updates);
182        for update in updates.iter_mut() {
183            if update.id == update_id {
184                Arc::make_mut(update).user_marked_important = true;
185                found = true;
186                break;
187            }
188        }
189        Arc::new(updated)
190    });
191
192    if found {
193        Ok(MCPToolResult::success(
194            "Update marked as important".to_string(),
195            None,
196        ))
197    } else {
198        Ok(MCPToolResult::error("Update not found".to_string()))
199    }
200}
201
202/// List all persisted sessions using a direct storage reference.
203pub async fn list_sessions_with_storage(
204    storage: &post_cortex_storage::rocksdb_storage::RealRocksDBStorage,
205) -> Result<MCPToolResult> {
206    // Metadata records only: listing never loads a session.
207    match storage.list_session_summaries().await {
208        Ok(summaries) => {
209            let sessions_info: Vec<_> = summaries
210                .iter()
211                .map(|summary| summary_json(summary, None))
212                .collect();
213            Ok(MCPToolResult::success(
214                format!("Found {} sessions", sessions_info.len()),
215                Some(serde_json::json!({
216                    "sessions": sessions_info
217                })),
218            ))
219        }
220        Err(e) => Ok(MCPToolResult::error(format!(
221            "Failed to load sessions: {e}"
222        ))),
223    }
224}
225
226/// Listing row for a session; `entity_count` is null when unknown.
227fn summary_json(
228    summary: &post_cortex_storage::traits::SessionSummary,
229    entity_count: Option<usize>,
230) -> serde_json::Value {
231    serde_json::json!({
232        "id": summary.id.to_string(),
233        "name": summary.name,
234        "description": summary.description,
235        "created_at": summary.created_at.to_rfc3339(),
236        "last_updated": summary.last_updated.to_rfc3339(),
237        "update_count": summary.update_count,
238        "entity_count": entity_count
239    })
240}
241
242/// List all sessions via the global memory system.
243pub async fn list_sessions() -> Result<MCPToolResult> {
244    debug!("MCP-TOOLS: list_sessions() called");
245    let result = with_storage_timeout(async {
246        debug!("MCP-TOOLS: Getting memory system for list_sessions");
247        let system = get_memory_system().await?;
248        debug!("MCP-TOOLS: Got memory system, listing sessions");
249        // Metadata records only: listing neither loads sessions nor fills
250        // the session cache. Entity counts are only known for sessions
251        // that are already in memory.
252        let summaries = system
253            .list_session_summaries()
254            .await
255            .map_err(string_to_anyhow)?;
256        let sessions_info: Vec<_> = summaries
257            .iter()
258            .map(|summary| {
259                let entity_count = system
260                    .session_manager
261                    .sessions
262                    .get(&summary.id)
263                    .map(|s| s.load().entity_graph.entities.len());
264                summary_json(summary, entity_count)
265            })
266            .collect();
267
268        Ok(MCPToolResult::success(
269            format!("Found {} sessions", sessions_info.len()),
270            Some(serde_json::json!({
271                "sessions": sessions_info
272            })),
273        ))
274    })
275    .await;
276
277    match result {
278        Ok(success_result) => success_result,
279        Err(timeout_error) => {
280            error!("TIMEOUT: list_sessions - error: {timeout_error}");
281            Ok(MCPToolResult::error(format!(
282                "Session listing timed out: {timeout_error}"
283            )))
284        }
285    }
286}
287
288/// Load a session by ID using an explicit memory system reference.
289pub async fn load_session_with_system(
290    session_id: Uuid,
291    system: &ConversationMemorySystem,
292) -> Result<MCPToolResult> {
293    match system.get_session(session_id).await {
294        Ok(session_arc) => {
295            let session = session_arc.load();
296            Ok(MCPToolResult::success(
297                "Session loaded successfully".to_string(),
298                Some(serde_json::json!({
299                    "session": {
300                        "id": session.id().to_string(),
301                        "created_at": session.created_at().to_rfc3339(),
302                        "last_updated": session.last_updated.to_rfc3339(),
303                        "update_count": session.incremental_updates.len(),
304                        "entity_count": session.entity_graph.entities.len(),
305                        "hot_context_size": session.hot_context.len(),
306                        "warm_context_size": session.warm_context.len(),
307                        "cold_context_size": session.cold_context.len(),
308                        "code_references": session.code_references.keys().collect::<Vec<_>>(),
309                        "change_history_count": session.change_history.len()
310                    }
311                })),
312            ))
313        }
314        Err(e) => Ok(MCPToolResult::error(
315            format!("Failed to load session: {e}",),
316        )),
317    }
318}
319
320/// Load a session by ID via the global memory system.
321pub async fn load_session(session_id: Uuid) -> Result<MCPToolResult> {
322    let result = with_storage_timeout(async {
323        let system = get_memory_system().await?;
324        load_session_with_system(session_id, &system).await
325    })
326    .await;
327
328    match result {
329        Ok(success_result) => success_result,
330        Err(timeout_error) => {
331            error!(
332                "TIMEOUT: load_session - session: {}, error: {}",
333                session_id, timeout_error
334            );
335            Ok(MCPToolResult::error(format!(
336                "Session loading timed out: {}",
337                timeout_error
338            )))
339        }
340    }
341}
342
343/// Search sessions by name or description.
344pub async fn search_sessions(query: String) -> Result<MCPToolResult> {
345    let result = with_storage_timeout(async {
346        let system = get_memory_system().await?;
347        let sessions: Vec<_> = system
348            .find_session_summaries(&query)
349            .await
350            .map_err(string_to_anyhow)?
351            .into_iter()
352            .map(|s| {
353                serde_json::json!({
354                    "id": s.id.to_string(),
355                    "name": s.name,
356                    "description": s.description
357                })
358            })
359            .collect();
360
361        Ok(MCPToolResult::success(
362            format!("Found {} sessions matching '{}'", sessions.len(), query),
363            Some(serde_json::json!({
364                "sessions": sessions
365            })),
366        ))
367    })
368    .await;
369
370    match result {
371        Ok(success_result) => success_result,
372        Err(timeout_error) => {
373            error!(
374                "TIMEOUT: search_sessions - query: {}, error: {}",
375                query, timeout_error
376            );
377            Ok(MCPToolResult::error(format!(
378                "Session search timed out: {}",
379                timeout_error
380            )))
381        }
382    }
383}
384
385/// Update the name and/or description of a session.
386#[instrument(skip(session_id), fields(session_id = %session_id))]
387pub async fn update_session_metadata(
388    session_id: Uuid,
389    name: Option<String>,
390    description: Option<String>,
391) -> Result<MCPToolResult> {
392    let result = with_storage_timeout(async {
393        let system = get_memory_system().await?;
394        system
395            .update_session_metadata(session_id, name, description)
396            .await
397            .map_err(string_to_anyhow)?;
398
399        let session_arc = system
400            .get_session(session_id)
401            .await
402            .map_err(|e| anyhow::anyhow!("Failed to load session: {}", e))?;
403        let session = session_arc.load();
404        let (final_name, final_description) = session.get_metadata();
405
406        Ok(MCPToolResult::success(
407            "Session metadata updated successfully".to_string(),
408            Some(serde_json::json!({
409                "session_id": session_id.to_string(),
410                "name": final_name,
411                "description": final_description
412            })),
413        ))
414    })
415    .await;
416
417    match result {
418        Ok(success_result) => success_result,
419        Err(timeout_error) => {
420            error!(
421                "TIMEOUT: update_session_metadata - session_id: {}, error: {}",
422                session_id, timeout_error
423            );
424            Ok(MCPToolResult::error(format!(
425                "TIMEOUT: Failed to update session metadata: {}",
426                timeout_error
427            )))
428        }
429    }
430}
431
432/// Build a comprehensive checkpoint snapshot from an active session.
433async fn create_comprehensive_checkpoint(session: &ActiveSession) -> Result<SessionCheckpoint> {
434    Ok(SessionCheckpoint {
435        id: Uuid::new_v4(),
436        session_id: session.id(),
437        created_at: chrono::Utc::now(),
438        structured_context: (*session.current_state).clone(),
439        recent_updates: session.incremental_updates_owned(),
440        code_references: (*session.code_references).clone(),
441        change_history: session.change_history_owned(),
442        total_updates: session.incremental_updates.len(),
443        context_quality_score: 1.0,
444        compression_ratio: 1.0,
445    })
446}