claude-mpm 3.9.9__py3-none-any.whl → 3.9.11__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/templates/memory_manager.json +155 -0
- claude_mpm/cli/__init__.py +15 -2
- claude_mpm/cli/commands/__init__.py +3 -0
- claude_mpm/cli/commands/mcp.py +280 -134
- claude_mpm/cli/commands/run_guarded.py +511 -0
- claude_mpm/cli/parser.py +8 -2
- claude_mpm/config/experimental_features.py +219 -0
- claude_mpm/config/memory_guardian_yaml.py +335 -0
- claude_mpm/constants.py +1 -0
- claude_mpm/core/memory_aware_runner.py +353 -0
- claude_mpm/services/infrastructure/context_preservation.py +537 -0
- claude_mpm/services/infrastructure/graceful_degradation.py +616 -0
- claude_mpm/services/infrastructure/health_monitor.py +775 -0
- claude_mpm/services/infrastructure/memory_dashboard.py +479 -0
- claude_mpm/services/infrastructure/memory_guardian.py +189 -15
- claude_mpm/services/infrastructure/restart_protection.py +642 -0
- claude_mpm/services/infrastructure/state_manager.py +774 -0
- claude_mpm/services/mcp_gateway/__init__.py +11 -11
- claude_mpm/services/mcp_gateway/core/__init__.py +2 -2
- claude_mpm/services/mcp_gateway/core/interfaces.py +10 -9
- claude_mpm/services/mcp_gateway/main.py +35 -5
- claude_mpm/services/mcp_gateway/manager.py +334 -0
- claude_mpm/services/mcp_gateway/registry/service_registry.py +4 -8
- claude_mpm/services/mcp_gateway/server/__init__.py +2 -2
- claude_mpm/services/mcp_gateway/server/{mcp_server.py → mcp_gateway.py} +60 -59
- claude_mpm/services/mcp_gateway/tools/base_adapter.py +1 -2
- claude_mpm/services/ticket_manager.py +8 -8
- claude_mpm/services/ticket_manager_di.py +5 -5
- claude_mpm/storage/__init__.py +9 -0
- claude_mpm/storage/state_storage.py +556 -0
- {claude_mpm-3.9.9.dist-info → claude_mpm-3.9.11.dist-info}/METADATA +25 -2
- {claude_mpm-3.9.9.dist-info → claude_mpm-3.9.11.dist-info}/RECORD +37 -24
- claude_mpm/services/mcp_gateway/server/mcp_server_simple.py +0 -444
- {claude_mpm-3.9.9.dist-info → claude_mpm-3.9.11.dist-info}/WHEEL +0 -0
- {claude_mpm-3.9.9.dist-info → claude_mpm-3.9.11.dist-info}/entry_points.txt +0 -0
- {claude_mpm-3.9.9.dist-info → claude_mpm-3.9.11.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-3.9.9.dist-info → claude_mpm-3.9.11.dist-info}/top_level.txt +0 -0
| @@ -0,0 +1,774 @@ | |
| 1 | 
            +
            """State Manager service for capturing and restoring execution state.
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            This service handles comprehensive state management for Claude Code,
         | 
| 4 | 
            +
            enabling seamless restarts with preserved context.
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            Design Principles:
         | 
| 7 | 
            +
            - Atomic state operations with write-to-temp-then-rename
         | 
| 8 | 
            +
            - Automatic cleanup of old state files
         | 
| 9 | 
            +
            - Privacy-preserving (sanitizes sensitive data)
         | 
| 10 | 
            +
            - Platform-agnostic implementation
         | 
| 11 | 
            +
            - Graceful degradation on failures
         | 
| 12 | 
            +
            """
         | 
| 13 | 
            +
             | 
| 14 | 
            +
            import asyncio
         | 
| 15 | 
            +
            import json
         | 
| 16 | 
            +
            import logging
         | 
| 17 | 
            +
            import os
         | 
| 18 | 
            +
            import pickle
         | 
| 19 | 
            +
            import platform
         | 
| 20 | 
            +
            import shutil
         | 
| 21 | 
            +
            import subprocess
         | 
| 22 | 
            +
            import sys
         | 
| 23 | 
            +
            import tempfile
         | 
| 24 | 
            +
            import time
         | 
| 25 | 
            +
            from datetime import datetime, timedelta
         | 
| 26 | 
            +
            from pathlib import Path
         | 
| 27 | 
            +
            from typing import Dict, Any, List, Optional, Tuple
         | 
| 28 | 
            +
            import uuid
         | 
| 29 | 
            +
            import gzip
         | 
| 30 | 
            +
             | 
| 31 | 
            +
            from claude_mpm.services.core.base import BaseService
         | 
| 32 | 
            +
            from claude_mpm.models.state_models import (
         | 
| 33 | 
            +
                ProcessState,
         | 
| 34 | 
            +
                ConversationState,
         | 
| 35 | 
            +
                ConversationContext,
         | 
| 36 | 
            +
                ProjectState,
         | 
| 37 | 
            +
                RestartState,
         | 
| 38 | 
            +
                CompleteState,
         | 
| 39 | 
            +
                StateType
         | 
| 40 | 
            +
            )
         | 
| 41 | 
            +
            from claude_mpm.utils.platform_memory import get_process_memory
         | 
| 42 | 
            +
             | 
| 43 | 
            +
             | 
| 44 | 
            +
            class StateManager(BaseService):
         | 
| 45 | 
            +
                """Service for managing Claude Code execution state across restarts."""
         | 
| 46 | 
            +
                
         | 
| 47 | 
            +
                def __init__(self, state_dir: Optional[Path] = None):
         | 
| 48 | 
            +
                    """Initialize State Manager service.
         | 
| 49 | 
            +
                    
         | 
| 50 | 
            +
                    Args:
         | 
| 51 | 
            +
                        state_dir: Directory for storing state files (default: ~/.claude-mpm/state)
         | 
| 52 | 
            +
                    """
         | 
| 53 | 
            +
                    super().__init__("StateManager")
         | 
| 54 | 
            +
                    
         | 
| 55 | 
            +
                    # State storage configuration
         | 
| 56 | 
            +
                    self.state_dir = state_dir or Path.home() / ".claude-mpm" / "state"
         | 
| 57 | 
            +
                    self.state_dir.mkdir(parents=True, exist_ok=True)
         | 
| 58 | 
            +
                    
         | 
| 59 | 
            +
                    # File naming
         | 
| 60 | 
            +
                    self.current_state_file = self.state_dir / "current_state.json"
         | 
| 61 | 
            +
                    self.compressed_state_file = self.state_dir / "current_state.json.gz"
         | 
| 62 | 
            +
                    
         | 
| 63 | 
            +
                    # Retention policy
         | 
| 64 | 
            +
                    self.retention_days = 7
         | 
| 65 | 
            +
                    self.max_state_files = 50
         | 
| 66 | 
            +
                    
         | 
| 67 | 
            +
                    # Claude conversation paths
         | 
| 68 | 
            +
                    self.claude_config_dir = Path.home() / ".claude"
         | 
| 69 | 
            +
                    self.claude_json_path = self.claude_config_dir / ".claude.json"
         | 
| 70 | 
            +
                    
         | 
| 71 | 
            +
                    # State tracking
         | 
| 72 | 
            +
                    self.current_state: Optional[CompleteState] = None
         | 
| 73 | 
            +
                    self.last_capture_time: float = 0
         | 
| 74 | 
            +
                    self.capture_cooldown = 5.0  # Minimum seconds between captures
         | 
| 75 | 
            +
                    
         | 
| 76 | 
            +
                    # Statistics
         | 
| 77 | 
            +
                    self.capture_count = 0
         | 
| 78 | 
            +
                    self.restore_count = 0
         | 
| 79 | 
            +
                    self.cleanup_count = 0
         | 
| 80 | 
            +
                    
         | 
| 81 | 
            +
                    self.log_info(f"State Manager initialized with state directory: {self.state_dir}")
         | 
| 82 | 
            +
                
         | 
| 83 | 
            +
                async def initialize(self) -> bool:
         | 
| 84 | 
            +
                    """Initialize the State Manager service.
         | 
| 85 | 
            +
                    
         | 
| 86 | 
            +
                    Returns:
         | 
| 87 | 
            +
                        True if initialization successful
         | 
| 88 | 
            +
                    """
         | 
| 89 | 
            +
                    try:
         | 
| 90 | 
            +
                        self.log_info("Initializing State Manager service")
         | 
| 91 | 
            +
                        
         | 
| 92 | 
            +
                        # Ensure state directory exists
         | 
| 93 | 
            +
                        self.state_dir.mkdir(parents=True, exist_ok=True)
         | 
| 94 | 
            +
                        
         | 
| 95 | 
            +
                        # Cleanup old states on startup
         | 
| 96 | 
            +
                        await self.cleanup_old_states()
         | 
| 97 | 
            +
                        
         | 
| 98 | 
            +
                        # Try to load existing state
         | 
| 99 | 
            +
                        loaded_state = await self.load_state()
         | 
| 100 | 
            +
                        if loaded_state:
         | 
| 101 | 
            +
                            self.log_info(f"Loaded existing state: {loaded_state.state_id}")
         | 
| 102 | 
            +
                            self.current_state = loaded_state
         | 
| 103 | 
            +
                        
         | 
| 104 | 
            +
                        self._initialized = True
         | 
| 105 | 
            +
                        self.log_info("State Manager service initialized successfully")
         | 
| 106 | 
            +
                        return True
         | 
| 107 | 
            +
                        
         | 
| 108 | 
            +
                    except Exception as e:
         | 
| 109 | 
            +
                        self.log_error(f"Failed to initialize State Manager: {e}")
         | 
| 110 | 
            +
                        return False
         | 
| 111 | 
            +
                
         | 
| 112 | 
            +
                async def shutdown(self) -> None:
         | 
| 113 | 
            +
                    """Shutdown the State Manager service gracefully."""
         | 
| 114 | 
            +
                    try:
         | 
| 115 | 
            +
                        self.log_info("Shutting down State Manager service")
         | 
| 116 | 
            +
                        
         | 
| 117 | 
            +
                        # Capture final state before shutdown
         | 
| 118 | 
            +
                        if self.current_state:
         | 
| 119 | 
            +
                            await self.persist_state(self.current_state)
         | 
| 120 | 
            +
                        
         | 
| 121 | 
            +
                        self._shutdown = True
         | 
| 122 | 
            +
                        self.log_info("State Manager service shutdown complete")
         | 
| 123 | 
            +
                        
         | 
| 124 | 
            +
                    except Exception as e:
         | 
| 125 | 
            +
                        self.log_error(f"Error during State Manager shutdown: {e}")
         | 
| 126 | 
            +
                
         | 
| 127 | 
            +
                async def capture_state(self, restart_reason: str = "Manual") -> Optional[CompleteState]:
         | 
| 128 | 
            +
                    """Capture current process and conversation state.
         | 
| 129 | 
            +
                    
         | 
| 130 | 
            +
                    Args:
         | 
| 131 | 
            +
                        restart_reason: Reason for the state capture/restart
         | 
| 132 | 
            +
                        
         | 
| 133 | 
            +
                    Returns:
         | 
| 134 | 
            +
                        CompleteState object or None if capture failed
         | 
| 135 | 
            +
                    """
         | 
| 136 | 
            +
                    try:
         | 
| 137 | 
            +
                        # Check cooldown
         | 
| 138 | 
            +
                        if time.time() - self.last_capture_time < self.capture_cooldown:
         | 
| 139 | 
            +
                            self.log_debug("State capture skipped due to cooldown")
         | 
| 140 | 
            +
                            return self.current_state
         | 
| 141 | 
            +
                        
         | 
| 142 | 
            +
                        self.log_info(f"Capturing state: {restart_reason}")
         | 
| 143 | 
            +
                        
         | 
| 144 | 
            +
                        # Capture process state
         | 
| 145 | 
            +
                        process_state = await self._capture_process_state()
         | 
| 146 | 
            +
                        
         | 
| 147 | 
            +
                        # Capture conversation state
         | 
| 148 | 
            +
                        conversation_state = await self._capture_conversation_state()
         | 
| 149 | 
            +
                        
         | 
| 150 | 
            +
                        # Capture project state
         | 
| 151 | 
            +
                        project_state = await self._capture_project_state()
         | 
| 152 | 
            +
                        
         | 
| 153 | 
            +
                        # Create restart state
         | 
| 154 | 
            +
                        restart_state = self._create_restart_state(restart_reason, process_state)
         | 
| 155 | 
            +
                        
         | 
| 156 | 
            +
                        # Create complete state
         | 
| 157 | 
            +
                        state = CompleteState(
         | 
| 158 | 
            +
                            process_state=process_state,
         | 
| 159 | 
            +
                            conversation_state=conversation_state,
         | 
| 160 | 
            +
                            project_state=project_state,
         | 
| 161 | 
            +
                            restart_state=restart_state
         | 
| 162 | 
            +
                        )
         | 
| 163 | 
            +
                        
         | 
| 164 | 
            +
                        # Validate state
         | 
| 165 | 
            +
                        issues = state.validate()
         | 
| 166 | 
            +
                        if issues:
         | 
| 167 | 
            +
                            for issue in issues:
         | 
| 168 | 
            +
                                self.log_warning(f"State validation issue: {issue}")
         | 
| 169 | 
            +
                        
         | 
| 170 | 
            +
                        # Update tracking
         | 
| 171 | 
            +
                        self.current_state = state
         | 
| 172 | 
            +
                        self.last_capture_time = time.time()
         | 
| 173 | 
            +
                        self.capture_count += 1
         | 
| 174 | 
            +
                        
         | 
| 175 | 
            +
                        self.log_info(f"State captured successfully: {state.state_id}")
         | 
| 176 | 
            +
                        return state
         | 
| 177 | 
            +
                        
         | 
| 178 | 
            +
                    except Exception as e:
         | 
| 179 | 
            +
                        self.log_error(f"Failed to capture state: {e}")
         | 
| 180 | 
            +
                        return None
         | 
| 181 | 
            +
                
         | 
| 182 | 
            +
                async def restore_state(self, state: Optional[CompleteState] = None) -> bool:
         | 
| 183 | 
            +
                    """Restore state after process restart.
         | 
| 184 | 
            +
                    
         | 
| 185 | 
            +
                    Args:
         | 
| 186 | 
            +
                        state: State to restore (default: load from disk)
         | 
| 187 | 
            +
                        
         | 
| 188 | 
            +
                    Returns:
         | 
| 189 | 
            +
                        True if restoration successful
         | 
| 190 | 
            +
                    """
         | 
| 191 | 
            +
                    try:
         | 
| 192 | 
            +
                        # Load state if not provided
         | 
| 193 | 
            +
                        if state is None:
         | 
| 194 | 
            +
                            state = await self.load_state()
         | 
| 195 | 
            +
                            if state is None:
         | 
| 196 | 
            +
                                self.log_warning("No state available to restore")
         | 
| 197 | 
            +
                                return False
         | 
| 198 | 
            +
                        
         | 
| 199 | 
            +
                        self.log_info(f"Restoring state: {state.state_id}")
         | 
| 200 | 
            +
                        
         | 
| 201 | 
            +
                        # Restore working directory
         | 
| 202 | 
            +
                        if state.process_state.working_directory:
         | 
| 203 | 
            +
                            try:
         | 
| 204 | 
            +
                                os.chdir(state.process_state.working_directory)
         | 
| 205 | 
            +
                                self.log_debug(f"Restored working directory: {state.process_state.working_directory}")
         | 
| 206 | 
            +
                            except Exception as e:
         | 
| 207 | 
            +
                                self.log_warning(f"Could not restore working directory: {e}")
         | 
| 208 | 
            +
                        
         | 
| 209 | 
            +
                        # Restore environment variables (non-sensitive only)
         | 
| 210 | 
            +
                        for key, value in state.process_state.environment.items():
         | 
| 211 | 
            +
                            if value != '***REDACTED***':
         | 
| 212 | 
            +
                                os.environ[key] = value
         | 
| 213 | 
            +
                        
         | 
| 214 | 
            +
                        # Restore project-specific environment
         | 
| 215 | 
            +
                        if state.project_state.environment_vars:
         | 
| 216 | 
            +
                            for key, value in state.project_state.environment_vars.items():
         | 
| 217 | 
            +
                                if value != '***REDACTED***':
         | 
| 218 | 
            +
                                    os.environ[key] = value
         | 
| 219 | 
            +
                        
         | 
| 220 | 
            +
                        # Log restoration summary
         | 
| 221 | 
            +
                        self.log_info(f"State restoration complete:")
         | 
| 222 | 
            +
                        self.log_info(f"  - Working directory: {state.process_state.working_directory}")
         | 
| 223 | 
            +
                        self.log_info(f"  - Git branch: {state.project_state.git_branch}")
         | 
| 224 | 
            +
                        self.log_info(f"  - Open files: {len(state.conversation_state.open_files)}")
         | 
| 225 | 
            +
                        self.log_info(f"  - Active conversation: {state.conversation_state.active_conversation_id}")
         | 
| 226 | 
            +
                        
         | 
| 227 | 
            +
                        self.restore_count += 1
         | 
| 228 | 
            +
                        return True
         | 
| 229 | 
            +
                        
         | 
| 230 | 
            +
                    except Exception as e:
         | 
| 231 | 
            +
                        self.log_error(f"Failed to restore state: {e}")
         | 
| 232 | 
            +
                        return False
         | 
| 233 | 
            +
                
         | 
| 234 | 
            +
                async def persist_state(self, state: CompleteState, compress: bool = True) -> bool:
         | 
| 235 | 
            +
                    """Save state to disk atomically.
         | 
| 236 | 
            +
                    
         | 
| 237 | 
            +
                    Args:
         | 
| 238 | 
            +
                        state: State to persist
         | 
| 239 | 
            +
                        compress: Whether to compress the state file
         | 
| 240 | 
            +
                        
         | 
| 241 | 
            +
                    Returns:
         | 
| 242 | 
            +
                        True if persistence successful
         | 
| 243 | 
            +
                    """
         | 
| 244 | 
            +
                    try:
         | 
| 245 | 
            +
                        self.log_debug(f"Persisting state: {state.state_id}")
         | 
| 246 | 
            +
                        
         | 
| 247 | 
            +
                        # Convert state to dictionary
         | 
| 248 | 
            +
                        state_dict = state.to_dict()
         | 
| 249 | 
            +
                        
         | 
| 250 | 
            +
                        # Create temporary file
         | 
| 251 | 
            +
                        temp_fd, temp_path = tempfile.mkstemp(
         | 
| 252 | 
            +
                            dir=self.state_dir,
         | 
| 253 | 
            +
                            suffix='.tmp',
         | 
| 254 | 
            +
                            prefix='state_'
         | 
| 255 | 
            +
                        )
         | 
| 256 | 
            +
                        
         | 
| 257 | 
            +
                        try:
         | 
| 258 | 
            +
                            # Write state to temporary file
         | 
| 259 | 
            +
                            if compress:
         | 
| 260 | 
            +
                                # Write compressed JSON
         | 
| 261 | 
            +
                                with gzip.open(temp_path, 'wt', encoding='utf-8') as f:
         | 
| 262 | 
            +
                                    json.dump(state_dict, f, indent=2)
         | 
| 263 | 
            +
                                target_path = self.compressed_state_file
         | 
| 264 | 
            +
                            else:
         | 
| 265 | 
            +
                                # Write plain JSON
         | 
| 266 | 
            +
                                with os.fdopen(temp_fd, 'w') as f:
         | 
| 267 | 
            +
                                    json.dump(state_dict, f, indent=2)
         | 
| 268 | 
            +
                                target_path = self.current_state_file
         | 
| 269 | 
            +
                            
         | 
| 270 | 
            +
                            # Atomic rename
         | 
| 271 | 
            +
                            temp_path_obj = Path(temp_path)
         | 
| 272 | 
            +
                            temp_path_obj.replace(target_path)
         | 
| 273 | 
            +
                            
         | 
| 274 | 
            +
                            # Also create timestamped backup
         | 
| 275 | 
            +
                            backup_name = f"state_{state.state_id}.json"
         | 
| 276 | 
            +
                            if compress:
         | 
| 277 | 
            +
                                backup_name += ".gz"
         | 
| 278 | 
            +
                            backup_path = self.state_dir / backup_name
         | 
| 279 | 
            +
                            shutil.copy2(target_path, backup_path)
         | 
| 280 | 
            +
                            
         | 
| 281 | 
            +
                            self.log_debug(f"State persisted to {target_path}")
         | 
| 282 | 
            +
                            return True
         | 
| 283 | 
            +
                            
         | 
| 284 | 
            +
                        finally:
         | 
| 285 | 
            +
                            # Clean up temp file if it still exists
         | 
| 286 | 
            +
                            if Path(temp_path).exists():
         | 
| 287 | 
            +
                                os.unlink(temp_path)
         | 
| 288 | 
            +
                                
         | 
| 289 | 
            +
                    except Exception as e:
         | 
| 290 | 
            +
                        self.log_error(f"Failed to persist state: {e}")
         | 
| 291 | 
            +
                        return False
         | 
| 292 | 
            +
                
         | 
| 293 | 
            +
                async def load_state(self, state_file: Optional[Path] = None) -> Optional[CompleteState]:
         | 
| 294 | 
            +
                    """Load state from disk with validation.
         | 
| 295 | 
            +
                    
         | 
| 296 | 
            +
                    Args:
         | 
| 297 | 
            +
                        state_file: Path to state file (default: current_state.json[.gz])
         | 
| 298 | 
            +
                        
         | 
| 299 | 
            +
                    Returns:
         | 
| 300 | 
            +
                        CompleteState object or None if load failed
         | 
| 301 | 
            +
                    """
         | 
| 302 | 
            +
                    try:
         | 
| 303 | 
            +
                        # Determine which file to load
         | 
| 304 | 
            +
                        if state_file is None:
         | 
| 305 | 
            +
                            if self.compressed_state_file.exists():
         | 
| 306 | 
            +
                                state_file = self.compressed_state_file
         | 
| 307 | 
            +
                            elif self.current_state_file.exists():
         | 
| 308 | 
            +
                                state_file = self.current_state_file
         | 
| 309 | 
            +
                            else:
         | 
| 310 | 
            +
                                self.log_debug("No state file found")
         | 
| 311 | 
            +
                                return None
         | 
| 312 | 
            +
                        
         | 
| 313 | 
            +
                        self.log_debug(f"Loading state from {state_file}")
         | 
| 314 | 
            +
                        
         | 
| 315 | 
            +
                        # Read state file
         | 
| 316 | 
            +
                        if str(state_file).endswith('.gz'):
         | 
| 317 | 
            +
                            with gzip.open(state_file, 'rt', encoding='utf-8') as f:
         | 
| 318 | 
            +
                                state_dict = json.load(f)
         | 
| 319 | 
            +
                        else:
         | 
| 320 | 
            +
                            with open(state_file, 'r') as f:
         | 
| 321 | 
            +
                                state_dict = json.load(f)
         | 
| 322 | 
            +
                        
         | 
| 323 | 
            +
                        # Create state object
         | 
| 324 | 
            +
                        state = CompleteState.from_dict(state_dict)
         | 
| 325 | 
            +
                        
         | 
| 326 | 
            +
                        # Validate state
         | 
| 327 | 
            +
                        issues = state.validate()
         | 
| 328 | 
            +
                        if issues:
         | 
| 329 | 
            +
                            for issue in issues:
         | 
| 330 | 
            +
                                self.log_warning(f"State validation issue: {issue}")
         | 
| 331 | 
            +
                        
         | 
| 332 | 
            +
                        self.log_info(f"Loaded state: {state.state_id}")
         | 
| 333 | 
            +
                        return state
         | 
| 334 | 
            +
                        
         | 
| 335 | 
            +
                    except Exception as e:
         | 
| 336 | 
            +
                        self.log_error(f"Failed to load state: {e}")
         | 
| 337 | 
            +
                        return None
         | 
| 338 | 
            +
                
         | 
| 339 | 
            +
                async def cleanup_old_states(self) -> int:
         | 
| 340 | 
            +
                    """Remove states older than retention period.
         | 
| 341 | 
            +
                    
         | 
| 342 | 
            +
                    Returns:
         | 
| 343 | 
            +
                        Number of files removed
         | 
| 344 | 
            +
                    """
         | 
| 345 | 
            +
                    try:
         | 
| 346 | 
            +
                        self.log_debug("Cleaning up old state files")
         | 
| 347 | 
            +
                        
         | 
| 348 | 
            +
                        removed_count = 0
         | 
| 349 | 
            +
                        cutoff_time = datetime.now() - timedelta(days=self.retention_days)
         | 
| 350 | 
            +
                        
         | 
| 351 | 
            +
                        # Find all state files
         | 
| 352 | 
            +
                        state_files = list(self.state_dir.glob("state_*.json*"))
         | 
| 353 | 
            +
                        
         | 
| 354 | 
            +
                        # Sort by modification time
         | 
| 355 | 
            +
                        state_files.sort(key=lambda f: f.stat().st_mtime)
         | 
| 356 | 
            +
                        
         | 
| 357 | 
            +
                        # Remove old files
         | 
| 358 | 
            +
                        for state_file in state_files:
         | 
| 359 | 
            +
                            try:
         | 
| 360 | 
            +
                                # Check age
         | 
| 361 | 
            +
                                file_time = datetime.fromtimestamp(state_file.stat().st_mtime)
         | 
| 362 | 
            +
                                if file_time < cutoff_time:
         | 
| 363 | 
            +
                                    state_file.unlink()
         | 
| 364 | 
            +
                                    removed_count += 1
         | 
| 365 | 
            +
                                    self.log_debug(f"Removed old state file: {state_file.name}")
         | 
| 366 | 
            +
                            except Exception as e:
         | 
| 367 | 
            +
                                self.log_warning(f"Could not remove state file {state_file}: {e}")
         | 
| 368 | 
            +
                        
         | 
| 369 | 
            +
                        # Also enforce max file limit
         | 
| 370 | 
            +
                        remaining_files = list(self.state_dir.glob("state_*.json*"))
         | 
| 371 | 
            +
                        if len(remaining_files) > self.max_state_files:
         | 
| 372 | 
            +
                            # Remove oldest files
         | 
| 373 | 
            +
                            remaining_files.sort(key=lambda f: f.stat().st_mtime)
         | 
| 374 | 
            +
                            for state_file in remaining_files[:-self.max_state_files]:
         | 
| 375 | 
            +
                                try:
         | 
| 376 | 
            +
                                    state_file.unlink()
         | 
| 377 | 
            +
                                    removed_count += 1
         | 
| 378 | 
            +
                                    self.log_debug(f"Removed excess state file: {state_file.name}")
         | 
| 379 | 
            +
                                except Exception as e:
         | 
| 380 | 
            +
                                    self.log_warning(f"Could not remove state file {state_file}: {e}")
         | 
| 381 | 
            +
                        
         | 
| 382 | 
            +
                        if removed_count > 0:
         | 
| 383 | 
            +
                            self.log_info(f"Cleaned up {removed_count} old state files")
         | 
| 384 | 
            +
                        
         | 
| 385 | 
            +
                        self.cleanup_count += removed_count
         | 
| 386 | 
            +
                        return removed_count
         | 
| 387 | 
            +
                        
         | 
| 388 | 
            +
                    except Exception as e:
         | 
| 389 | 
            +
                        self.log_error(f"Error during state cleanup: {e}")
         | 
| 390 | 
            +
                        return 0
         | 
| 391 | 
            +
                
         | 
| 392 | 
            +
                async def get_conversation_context(self) -> Optional[ConversationState]:
         | 
| 393 | 
            +
                    """Extract relevant Claude conversation data.
         | 
| 394 | 
            +
                    
         | 
| 395 | 
            +
                    Returns:
         | 
| 396 | 
            +
                        ConversationState object or None if extraction failed
         | 
| 397 | 
            +
                    """
         | 
| 398 | 
            +
                    try:
         | 
| 399 | 
            +
                        # Check if Claude config exists
         | 
| 400 | 
            +
                        if not self.claude_json_path.exists():
         | 
| 401 | 
            +
                            self.log_debug("Claude configuration not found")
         | 
| 402 | 
            +
                            return ConversationState(
         | 
| 403 | 
            +
                                active_conversation_id=None,
         | 
| 404 | 
            +
                                active_conversation=None,
         | 
| 405 | 
            +
                                recent_conversations=[],
         | 
| 406 | 
            +
                                total_conversations=0,
         | 
| 407 | 
            +
                                total_storage_mb=0.0,
         | 
| 408 | 
            +
                                preferences={},
         | 
| 409 | 
            +
                                open_files=[],
         | 
| 410 | 
            +
                                recent_files=[],
         | 
| 411 | 
            +
                                pinned_files=[]
         | 
| 412 | 
            +
                            )
         | 
| 413 | 
            +
                        
         | 
| 414 | 
            +
                        # Get file size
         | 
| 415 | 
            +
                        file_size_mb = self.claude_json_path.stat().st_size / (1024 * 1024)
         | 
| 416 | 
            +
                        
         | 
| 417 | 
            +
                        # For large files, use streaming approach
         | 
| 418 | 
            +
                        if file_size_mb > 100:
         | 
| 419 | 
            +
                            self.log_warning(f"Large Claude config file ({file_size_mb:.2f}MB), using minimal extraction")
         | 
| 420 | 
            +
                            return self._extract_minimal_conversation_state()
         | 
| 421 | 
            +
                        
         | 
| 422 | 
            +
                        # Load Claude configuration
         | 
| 423 | 
            +
                        with open(self.claude_json_path, 'r') as f:
         | 
| 424 | 
            +
                            claude_data = json.load(f)
         | 
| 425 | 
            +
                        
         | 
| 426 | 
            +
                        # Extract conversation information
         | 
| 427 | 
            +
                        conversations = claude_data.get('conversations', [])
         | 
| 428 | 
            +
                        active_conv_id = claude_data.get('activeConversationId')
         | 
| 429 | 
            +
                        
         | 
| 430 | 
            +
                        # Find active conversation
         | 
| 431 | 
            +
                        active_conv = None
         | 
| 432 | 
            +
                        if active_conv_id:
         | 
| 433 | 
            +
                            for conv in conversations:
         | 
| 434 | 
            +
                                if conv.get('id') == active_conv_id:
         | 
| 435 | 
            +
                                    active_conv = ConversationContext(
         | 
| 436 | 
            +
                                        conversation_id=conv.get('id', ''),
         | 
| 437 | 
            +
                                        title=conv.get('title', 'Untitled'),
         | 
| 438 | 
            +
                                        created_at=conv.get('createdAt', 0),
         | 
| 439 | 
            +
                                        updated_at=conv.get('updatedAt', 0),
         | 
| 440 | 
            +
                                        message_count=len(conv.get('messages', [])),
         | 
| 441 | 
            +
                                        total_tokens=conv.get('totalTokens', 0),
         | 
| 442 | 
            +
                                        max_tokens=conv.get('maxTokens', 100000),
         | 
| 443 | 
            +
                                        referenced_files=self._extract_referenced_files(conv),
         | 
| 444 | 
            +
                                        open_tabs=conv.get('openTabs', []),
         | 
| 445 | 
            +
                                        tags=conv.get('tags', []),
         | 
| 446 | 
            +
                                        is_active=True
         | 
| 447 | 
            +
                                    )
         | 
| 448 | 
            +
                                    break
         | 
| 449 | 
            +
                        
         | 
| 450 | 
            +
                        # Get recent conversations (last 5)
         | 
| 451 | 
            +
                        recent_convs = []
         | 
| 452 | 
            +
                        sorted_convs = sorted(
         | 
| 453 | 
            +
                            conversations,
         | 
| 454 | 
            +
                            key=lambda c: c.get('updatedAt', 0),
         | 
| 455 | 
            +
                            reverse=True
         | 
| 456 | 
            +
                        )[:5]
         | 
| 457 | 
            +
                        
         | 
| 458 | 
            +
                        for conv in sorted_convs:
         | 
| 459 | 
            +
                            if conv.get('id') != active_conv_id:
         | 
| 460 | 
            +
                                recent_convs.append(ConversationContext(
         | 
| 461 | 
            +
                                    conversation_id=conv.get('id', ''),
         | 
| 462 | 
            +
                                    title=conv.get('title', 'Untitled'),
         | 
| 463 | 
            +
                                    created_at=conv.get('createdAt', 0),
         | 
| 464 | 
            +
                                    updated_at=conv.get('updatedAt', 0),
         | 
| 465 | 
            +
                                    message_count=len(conv.get('messages', [])),
         | 
| 466 | 
            +
                                    total_tokens=conv.get('totalTokens', 0),
         | 
| 467 | 
            +
                                    max_tokens=conv.get('maxTokens', 100000),
         | 
| 468 | 
            +
                                    referenced_files=self._extract_referenced_files(conv),
         | 
| 469 | 
            +
                                    open_tabs=conv.get('openTabs', []),
         | 
| 470 | 
            +
                                    tags=conv.get('tags', []),
         | 
| 471 | 
            +
                                    is_active=False
         | 
| 472 | 
            +
                                ))
         | 
| 473 | 
            +
                        
         | 
| 474 | 
            +
                        # Extract preferences
         | 
| 475 | 
            +
                        preferences = claude_data.get('preferences', {})
         | 
| 476 | 
            +
                        
         | 
| 477 | 
            +
                        # Extract file state
         | 
| 478 | 
            +
                        open_files = claude_data.get('openFiles', [])
         | 
| 479 | 
            +
                        recent_files = claude_data.get('recentFiles', [])
         | 
| 480 | 
            +
                        pinned_files = claude_data.get('pinnedFiles', [])
         | 
| 481 | 
            +
                        
         | 
| 482 | 
            +
                        return ConversationState(
         | 
| 483 | 
            +
                            active_conversation_id=active_conv_id,
         | 
| 484 | 
            +
                            active_conversation=active_conv,
         | 
| 485 | 
            +
                            recent_conversations=recent_convs,
         | 
| 486 | 
            +
                            total_conversations=len(conversations),
         | 
| 487 | 
            +
                            total_storage_mb=file_size_mb,
         | 
| 488 | 
            +
                            preferences=preferences,
         | 
| 489 | 
            +
                            open_files=open_files,
         | 
| 490 | 
            +
                            recent_files=recent_files,
         | 
| 491 | 
            +
                            pinned_files=pinned_files
         | 
| 492 | 
            +
                        )
         | 
| 493 | 
            +
                        
         | 
| 494 | 
            +
                    except Exception as e:
         | 
| 495 | 
            +
                        self.log_error(f"Failed to extract conversation context: {e}")
         | 
| 496 | 
            +
                        return None
         | 
| 497 | 
            +
                
         | 
| 498 | 
            +
                async def _capture_process_state(self) -> ProcessState:
         | 
| 499 | 
            +
                    """Capture current process state."""
         | 
| 500 | 
            +
                    try:
         | 
| 501 | 
            +
                        pid = os.getpid()
         | 
| 502 | 
            +
                        
         | 
| 503 | 
            +
                        # Get memory info
         | 
| 504 | 
            +
                        mem_info = get_process_memory(pid)
         | 
| 505 | 
            +
                        memory_mb = mem_info.rss_mb if mem_info else 0.0
         | 
| 506 | 
            +
                        cpu_percent = 0.0  # Would need psutil for accurate CPU%
         | 
| 507 | 
            +
                        
         | 
| 508 | 
            +
                        # Get command line
         | 
| 509 | 
            +
                        if platform.system() != 'Windows':
         | 
| 510 | 
            +
                            try:
         | 
| 511 | 
            +
                                with open(f'/proc/{pid}/cmdline', 'r') as f:
         | 
| 512 | 
            +
                                    cmdline = f.read().replace('\0', ' ').strip().split()
         | 
| 513 | 
            +
                            except:
         | 
| 514 | 
            +
                                cmdline = sys.argv
         | 
| 515 | 
            +
                        else:
         | 
| 516 | 
            +
                            cmdline = sys.argv
         | 
| 517 | 
            +
                        
         | 
| 518 | 
            +
                        # Get open files (simplified - would need lsof or psutil for full list)
         | 
| 519 | 
            +
                        open_files = []
         | 
| 520 | 
            +
                        
         | 
| 521 | 
            +
                        return ProcessState(
         | 
| 522 | 
            +
                            pid=pid,
         | 
| 523 | 
            +
                            parent_pid=os.getppid(),
         | 
| 524 | 
            +
                            process_name=os.path.basename(sys.executable),
         | 
| 525 | 
            +
                            command=cmdline[:1] if cmdline else [],
         | 
| 526 | 
            +
                            args=cmdline[1:] if len(cmdline) > 1 else [],
         | 
| 527 | 
            +
                            working_directory=os.getcwd(),
         | 
| 528 | 
            +
                            environment=dict(os.environ),
         | 
| 529 | 
            +
                            memory_mb=memory_mb,
         | 
| 530 | 
            +
                            cpu_percent=cpu_percent,
         | 
| 531 | 
            +
                            open_files=open_files,
         | 
| 532 | 
            +
                            start_time=time.time() - time.process_time(),
         | 
| 533 | 
            +
                            capture_time=time.time()
         | 
| 534 | 
            +
                        )
         | 
| 535 | 
            +
                        
         | 
| 536 | 
            +
                    except Exception as e:
         | 
| 537 | 
            +
                        self.log_error(f"Error capturing process state: {e}")
         | 
| 538 | 
            +
                        # Return minimal state
         | 
| 539 | 
            +
                        return ProcessState(
         | 
| 540 | 
            +
                            pid=os.getpid(),
         | 
| 541 | 
            +
                            parent_pid=os.getppid(),
         | 
| 542 | 
            +
                            process_name="claude-mpm",
         | 
| 543 | 
            +
                            command=sys.argv[:1],
         | 
| 544 | 
            +
                            args=sys.argv[1:],
         | 
| 545 | 
            +
                            working_directory=os.getcwd(),
         | 
| 546 | 
            +
                            environment={},
         | 
| 547 | 
            +
                            memory_mb=0.0,
         | 
| 548 | 
            +
                            cpu_percent=0.0,
         | 
| 549 | 
            +
                            open_files=[],
         | 
| 550 | 
            +
                            start_time=time.time(),
         | 
| 551 | 
            +
                            capture_time=time.time()
         | 
| 552 | 
            +
                        )
         | 
| 553 | 
            +
                
         | 
| 554 | 
            +
                async def _capture_conversation_state(self) -> ConversationState:
         | 
| 555 | 
            +
                    """Capture conversation state from Claude."""
         | 
| 556 | 
            +
                    conversation_state = await self.get_conversation_context()
         | 
| 557 | 
            +
                    return conversation_state or ConversationState(
         | 
| 558 | 
            +
                        active_conversation_id=None,
         | 
| 559 | 
            +
                        active_conversation=None,
         | 
| 560 | 
            +
                        recent_conversations=[],
         | 
| 561 | 
            +
                        total_conversations=0,
         | 
| 562 | 
            +
                        total_storage_mb=0.0,
         | 
| 563 | 
            +
                        preferences={},
         | 
| 564 | 
            +
                        open_files=[],
         | 
| 565 | 
            +
                        recent_files=[],
         | 
| 566 | 
            +
                        pinned_files=[]
         | 
| 567 | 
            +
                    )
         | 
| 568 | 
            +
                
         | 
| 569 | 
            +
                async def _capture_project_state(self) -> ProjectState:
         | 
| 570 | 
            +
                    """Capture project and Git state."""
         | 
| 571 | 
            +
                    try:
         | 
| 572 | 
            +
                        project_path = os.getcwd()
         | 
| 573 | 
            +
                        project_name = os.path.basename(project_path)
         | 
| 574 | 
            +
                        
         | 
| 575 | 
            +
                        # Get Git information
         | 
| 576 | 
            +
                        git_branch = None
         | 
| 577 | 
            +
                        git_commit = None
         | 
| 578 | 
            +
                        git_status = {}
         | 
| 579 | 
            +
                        git_remotes = {}
         | 
| 580 | 
            +
                        
         | 
| 581 | 
            +
                        try:
         | 
| 582 | 
            +
                            # Get current branch
         | 
| 583 | 
            +
                            result = subprocess.run(
         | 
| 584 | 
            +
                                ['git', 'rev-parse', '--abbrev-ref', 'HEAD'],
         | 
| 585 | 
            +
                                capture_output=True,
         | 
| 586 | 
            +
                                text=True,
         | 
| 587 | 
            +
                                cwd=project_path
         | 
| 588 | 
            +
                            )
         | 
| 589 | 
            +
                            if result.returncode == 0:
         | 
| 590 | 
            +
                                git_branch = result.stdout.strip()
         | 
| 591 | 
            +
                            
         | 
| 592 | 
            +
                            # Get current commit
         | 
| 593 | 
            +
                            result = subprocess.run(
         | 
| 594 | 
            +
                                ['git', 'rev-parse', 'HEAD'],
         | 
| 595 | 
            +
                                capture_output=True,
         | 
| 596 | 
            +
                                text=True,
         | 
| 597 | 
            +
                                cwd=project_path
         | 
| 598 | 
            +
                            )
         | 
| 599 | 
            +
                            if result.returncode == 0:
         | 
| 600 | 
            +
                                git_commit = result.stdout.strip()[:8]
         | 
| 601 | 
            +
                            
         | 
| 602 | 
            +
                            # Get status
         | 
| 603 | 
            +
                            result = subprocess.run(
         | 
| 604 | 
            +
                                ['git', 'status', '--porcelain'],
         | 
| 605 | 
            +
                                capture_output=True,
         | 
| 606 | 
            +
                                text=True,
         | 
| 607 | 
            +
                                cwd=project_path
         | 
| 608 | 
            +
                            )
         | 
| 609 | 
            +
                            if result.returncode == 0:
         | 
| 610 | 
            +
                                staged = []
         | 
| 611 | 
            +
                                modified = []
         | 
| 612 | 
            +
                                untracked = []
         | 
| 613 | 
            +
                                
         | 
| 614 | 
            +
                                for line in result.stdout.splitlines():
         | 
| 615 | 
            +
                                    if line.startswith('??'):
         | 
| 616 | 
            +
                                        untracked.append(line[3:])
         | 
| 617 | 
            +
                                    elif line.startswith(' M'):
         | 
| 618 | 
            +
                                        modified.append(line[3:])
         | 
| 619 | 
            +
                                    elif line.startswith('M '):
         | 
| 620 | 
            +
                                        staged.append(line[3:])
         | 
| 621 | 
            +
                                
         | 
| 622 | 
            +
                                git_status = {
         | 
| 623 | 
            +
                                    'staged': staged,
         | 
| 624 | 
            +
                                    'modified': modified,
         | 
| 625 | 
            +
                                    'untracked': untracked
         | 
| 626 | 
            +
                                }
         | 
| 627 | 
            +
                            
         | 
| 628 | 
            +
                        except Exception as e:
         | 
| 629 | 
            +
                            self.log_debug(f"Could not get Git information: {e}")
         | 
| 630 | 
            +
                        
         | 
| 631 | 
            +
                        # Detect project type
         | 
| 632 | 
            +
                        project_type = self._detect_project_type(project_path)
         | 
| 633 | 
            +
                        
         | 
| 634 | 
            +
                        return ProjectState(
         | 
| 635 | 
            +
                            project_path=project_path,
         | 
| 636 | 
            +
                            project_name=project_name,
         | 
| 637 | 
            +
                            git_branch=git_branch,
         | 
| 638 | 
            +
                            git_commit=git_commit,
         | 
| 639 | 
            +
                            git_status=git_status,
         | 
| 640 | 
            +
                            git_remotes=git_remotes,
         | 
| 641 | 
            +
                            modified_files=git_status.get('modified', []),
         | 
| 642 | 
            +
                            open_editors=[],
         | 
| 643 | 
            +
                            breakpoints={},
         | 
| 644 | 
            +
                            project_type=project_type,
         | 
| 645 | 
            +
                            dependencies={},
         | 
| 646 | 
            +
                            environment_vars={},
         | 
| 647 | 
            +
                            last_build_status=None,
         | 
| 648 | 
            +
                            last_test_results=None
         | 
| 649 | 
            +
                        )
         | 
| 650 | 
            +
                        
         | 
| 651 | 
            +
                    except Exception as e:
         | 
| 652 | 
            +
                        self.log_error(f"Error capturing project state: {e}")
         | 
| 653 | 
            +
                        return ProjectState(
         | 
| 654 | 
            +
                            project_path=os.getcwd(),
         | 
| 655 | 
            +
                            project_name=os.path.basename(os.getcwd()),
         | 
| 656 | 
            +
                            git_branch=None,
         | 
| 657 | 
            +
                            git_commit=None,
         | 
| 658 | 
            +
                            git_status={},
         | 
| 659 | 
            +
                            git_remotes={},
         | 
| 660 | 
            +
                            modified_files=[],
         | 
| 661 | 
            +
                            open_editors=[],
         | 
| 662 | 
            +
                            breakpoints={},
         | 
| 663 | 
            +
                            project_type='unknown',
         | 
| 664 | 
            +
                            dependencies={},
         | 
| 665 | 
            +
                            environment_vars={},
         | 
| 666 | 
            +
                            last_build_status=None,
         | 
| 667 | 
            +
                            last_test_results=None
         | 
| 668 | 
            +
                        )
         | 
| 669 | 
            +
                
         | 
| 670 | 
            +
                def _create_restart_state(self, reason: str, process_state: ProcessState) -> RestartState:
         | 
| 671 | 
            +
                    """Create restart state information."""
         | 
| 672 | 
            +
                    return RestartState(
         | 
| 673 | 
            +
                        restart_id=str(uuid.uuid4()),
         | 
| 674 | 
            +
                        restart_count=self.capture_count,
         | 
| 675 | 
            +
                        timestamp=time.time(),
         | 
| 676 | 
            +
                        previous_uptime=time.time() - process_state.start_time,
         | 
| 677 | 
            +
                        reason=reason,
         | 
| 678 | 
            +
                        trigger='manual',  # Will be updated by caller
         | 
| 679 | 
            +
                        memory_mb=process_state.memory_mb,
         | 
| 680 | 
            +
                        memory_limit_mb=2048.0,  # Default limit
         | 
| 681 | 
            +
                        cpu_percent=process_state.cpu_percent,
         | 
| 682 | 
            +
                        error_type=None,
         | 
| 683 | 
            +
                        error_message=None,
         | 
| 684 | 
            +
                        error_traceback=None,
         | 
| 685 | 
            +
                        recovery_attempted=True,
         | 
| 686 | 
            +
                        recovery_successful=False,  # Will be updated after restore
         | 
| 687 | 
            +
                        data_preserved=['process', 'conversation', 'project']
         | 
| 688 | 
            +
                    )
         | 
| 689 | 
            +
                
         | 
| 690 | 
            +
                def _extract_referenced_files(self, conversation: Dict[str, Any]) -> List[str]:
         | 
| 691 | 
            +
                    """Extract file references from conversation."""
         | 
| 692 | 
            +
                    files = set()
         | 
| 693 | 
            +
                    
         | 
| 694 | 
            +
                    # Extract from messages
         | 
| 695 | 
            +
                    for message in conversation.get('messages', []):
         | 
| 696 | 
            +
                        content = message.get('content', '')
         | 
| 697 | 
            +
                        # Simple extraction - could be enhanced with regex
         | 
| 698 | 
            +
                        if isinstance(content, str):
         | 
| 699 | 
            +
                            # Look for file paths
         | 
| 700 | 
            +
                            import re
         | 
| 701 | 
            +
                            file_pattern = r'[\'"`]([^\'"`]+\.[a-zA-Z0-9]+)[\'"`]'
         | 
| 702 | 
            +
                            matches = re.findall(file_pattern, content)
         | 
| 703 | 
            +
                            files.update(matches)
         | 
| 704 | 
            +
                    
         | 
| 705 | 
            +
                    return list(files)[:100]  # Limit to prevent huge lists
         | 
| 706 | 
            +
                
         | 
| 707 | 
            +
                def _extract_minimal_conversation_state(self) -> ConversationState:
         | 
| 708 | 
            +
                    """Extract minimal conversation state for large files."""
         | 
| 709 | 
            +
                    try:
         | 
| 710 | 
            +
                        # Just get basic metadata without loading full file
         | 
| 711 | 
            +
                        file_size_mb = self.claude_json_path.stat().st_size / (1024 * 1024)
         | 
| 712 | 
            +
                        
         | 
| 713 | 
            +
                        return ConversationState(
         | 
| 714 | 
            +
                            active_conversation_id="large_file",
         | 
| 715 | 
            +
                            active_conversation=None,
         | 
| 716 | 
            +
                            recent_conversations=[],
         | 
| 717 | 
            +
                            total_conversations=-1,  # Unknown
         | 
| 718 | 
            +
                            total_storage_mb=file_size_mb,
         | 
| 719 | 
            +
                            preferences={},
         | 
| 720 | 
            +
                            open_files=[],
         | 
| 721 | 
            +
                            recent_files=[],
         | 
| 722 | 
            +
                            pinned_files=[]
         | 
| 723 | 
            +
                        )
         | 
| 724 | 
            +
                    except:
         | 
| 725 | 
            +
                        return ConversationState(
         | 
| 726 | 
            +
                            active_conversation_id=None,
         | 
| 727 | 
            +
                            active_conversation=None,
         | 
| 728 | 
            +
                            recent_conversations=[],
         | 
| 729 | 
            +
                            total_conversations=0,
         | 
| 730 | 
            +
                            total_storage_mb=0.0,
         | 
| 731 | 
            +
                            preferences={},
         | 
| 732 | 
            +
                            open_files=[],
         | 
| 733 | 
            +
                            recent_files=[],
         | 
| 734 | 
            +
                            pinned_files=[]
         | 
| 735 | 
            +
                        )
         | 
| 736 | 
            +
                
         | 
| 737 | 
            +
                def _detect_project_type(self, project_path: str) -> str:
         | 
| 738 | 
            +
                    """Detect project type from files present."""
         | 
| 739 | 
            +
                    path = Path(project_path)
         | 
| 740 | 
            +
                    
         | 
| 741 | 
            +
                    if (path / 'pyproject.toml').exists() or (path / 'setup.py').exists():
         | 
| 742 | 
            +
                        return 'python'
         | 
| 743 | 
            +
                    elif (path / 'package.json').exists():
         | 
| 744 | 
            +
                        return 'node'
         | 
| 745 | 
            +
                    elif (path / 'go.mod').exists():
         | 
| 746 | 
            +
                        return 'go'
         | 
| 747 | 
            +
                    elif (path / 'Cargo.toml').exists():
         | 
| 748 | 
            +
                        return 'rust'
         | 
| 749 | 
            +
                    elif (path / 'pom.xml').exists():
         | 
| 750 | 
            +
                        return 'java'
         | 
| 751 | 
            +
                    else:
         | 
| 752 | 
            +
                        return 'unknown'
         | 
| 753 | 
            +
                
         | 
| 754 | 
            +
                def get_statistics(self) -> Dict[str, Any]:
         | 
| 755 | 
            +
                    """Get state manager statistics.
         | 
| 756 | 
            +
                    
         | 
| 757 | 
            +
                    Returns:
         | 
| 758 | 
            +
                        Dictionary containing statistics
         | 
| 759 | 
            +
                    """
         | 
| 760 | 
            +
                    state_files = list(self.state_dir.glob("state_*.json*"))
         | 
| 761 | 
            +
                    total_size_mb = sum(f.stat().st_size for f in state_files) / (1024 * 1024)
         | 
| 762 | 
            +
                    
         | 
| 763 | 
            +
                    return {
         | 
| 764 | 
            +
                        'capture_count': self.capture_count,
         | 
| 765 | 
            +
                        'restore_count': self.restore_count,
         | 
| 766 | 
            +
                        'cleanup_count': self.cleanup_count,
         | 
| 767 | 
            +
                        'state_files': len(state_files),
         | 
| 768 | 
            +
                        'total_size_mb': round(total_size_mb, 2),
         | 
| 769 | 
            +
                        'state_directory': str(self.state_dir),
         | 
| 770 | 
            +
                        'current_state_id': self.current_state.state_id if self.current_state else None,
         | 
| 771 | 
            +
                        'last_capture_time': self.last_capture_time,
         | 
| 772 | 
            +
                        'retention_days': self.retention_days,
         | 
| 773 | 
            +
                        'max_state_files': self.max_state_files
         | 
| 774 | 
            +
                    }
         |