claude-mpm 3.9.7__py3-none-any.whl → 3.9.9__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/base_agent.json +1 -1
- claude_mpm/agents/templates/ticketing.json +1 -1
- claude_mpm/cli/__init__.py +3 -1
- claude_mpm/cli/commands/__init__.py +3 -1
- claude_mpm/cli/commands/cleanup.py +21 -1
- claude_mpm/cli/commands/mcp.py +821 -0
- claude_mpm/cli/parser.py +148 -1
- claude_mpm/config/memory_guardian_config.py +325 -0
- claude_mpm/constants.py +13 -0
- claude_mpm/hooks/claude_hooks/hook_handler.py +76 -19
- claude_mpm/models/state_models.py +433 -0
- claude_mpm/services/__init__.py +28 -0
- claude_mpm/services/communication/__init__.py +2 -2
- claude_mpm/services/communication/socketio.py +18 -16
- claude_mpm/services/infrastructure/__init__.py +4 -1
- claude_mpm/services/infrastructure/logging.py +3 -3
- claude_mpm/services/infrastructure/memory_guardian.py +770 -0
- claude_mpm/services/mcp_gateway/__init__.py +138 -0
- claude_mpm/services/mcp_gateway/config/__init__.py +17 -0
- claude_mpm/services/mcp_gateway/config/config_loader.py +232 -0
- claude_mpm/services/mcp_gateway/config/config_schema.py +234 -0
- claude_mpm/services/mcp_gateway/config/configuration.py +371 -0
- claude_mpm/services/mcp_gateway/core/__init__.py +51 -0
- claude_mpm/services/mcp_gateway/core/base.py +315 -0
- claude_mpm/services/mcp_gateway/core/exceptions.py +239 -0
- claude_mpm/services/mcp_gateway/core/interfaces.py +476 -0
- claude_mpm/services/mcp_gateway/main.py +326 -0
- claude_mpm/services/mcp_gateway/registry/__init__.py +12 -0
- claude_mpm/services/mcp_gateway/registry/service_registry.py +397 -0
- claude_mpm/services/mcp_gateway/registry/tool_registry.py +477 -0
- claude_mpm/services/mcp_gateway/server/__init__.py +15 -0
- claude_mpm/services/mcp_gateway/server/mcp_server.py +430 -0
- claude_mpm/services/mcp_gateway/server/mcp_server_simple.py +444 -0
- claude_mpm/services/mcp_gateway/server/stdio_handler.py +373 -0
- claude_mpm/services/mcp_gateway/tools/__init__.py +22 -0
- claude_mpm/services/mcp_gateway/tools/base_adapter.py +497 -0
- claude_mpm/services/mcp_gateway/tools/document_summarizer.py +729 -0
- claude_mpm/services/mcp_gateway/tools/hello_world.py +551 -0
- claude_mpm/utils/file_utils.py +293 -0
- claude_mpm/utils/platform_memory.py +524 -0
- claude_mpm/utils/subprocess_utils.py +305 -0
- {claude_mpm-3.9.7.dist-info → claude_mpm-3.9.9.dist-info}/METADATA +4 -1
- {claude_mpm-3.9.7.dist-info → claude_mpm-3.9.9.dist-info}/RECORD +49 -26
- claude_mpm/agents/templates/.claude-mpm/memories/README.md +0 -36
- claude_mpm/agents/templates/.claude-mpm/memories/engineer_agent.md +0 -39
- claude_mpm/agents/templates/.claude-mpm/memories/qa_agent.md +0 -38
- claude_mpm/agents/templates/.claude-mpm/memories/research_agent.md +0 -39
- claude_mpm/agents/templates/.claude-mpm/memories/version_control_agent.md +0 -38
- /claude_mpm/agents/templates/{research_memory_efficient.json → backup/research_memory_efficient.json} +0 -0
- {claude_mpm-3.9.7.dist-info → claude_mpm-3.9.9.dist-info}/WHEEL +0 -0
- {claude_mpm-3.9.7.dist-info → claude_mpm-3.9.9.dist-info}/entry_points.txt +0 -0
- {claude_mpm-3.9.7.dist-info → claude_mpm-3.9.9.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-3.9.7.dist-info → claude_mpm-3.9.9.dist-info}/top_level.txt +0 -0
| @@ -0,0 +1,770 @@ | |
| 1 | 
            +
            """Memory Guardian service for monitoring and managing Claude Code memory usage.
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            This service monitors a subprocess (Claude Code) for memory consumption and
         | 
| 4 | 
            +
            performs automatic restarts when memory thresholds are exceeded.
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            Design Principles:
         | 
| 7 | 
            +
            - Subprocess lifecycle management with graceful shutdown
         | 
| 8 | 
            +
            - Multi-threshold memory monitoring (warning, critical, emergency)
         | 
| 9 | 
            +
            - Platform-agnostic memory monitoring with fallbacks
         | 
| 10 | 
            +
            - Configurable restart policies with cooldown periods
         | 
| 11 | 
            +
            - State preservation hooks for future enhancement
         | 
| 12 | 
            +
            """
         | 
| 13 | 
            +
             | 
| 14 | 
            +
            import asyncio
         | 
| 15 | 
            +
            import json
         | 
| 16 | 
            +
            import logging
         | 
| 17 | 
            +
            import os
         | 
| 18 | 
            +
            import platform
         | 
| 19 | 
            +
            import signal
         | 
| 20 | 
            +
            import subprocess
         | 
| 21 | 
            +
            import sys
         | 
| 22 | 
            +
            import time
         | 
| 23 | 
            +
            from dataclasses import dataclass, field
         | 
| 24 | 
            +
            from datetime import datetime, timedelta
         | 
| 25 | 
            +
            from enum import Enum
         | 
| 26 | 
            +
            from pathlib import Path
         | 
| 27 | 
            +
            from typing import Optional, Dict, Any, List, Callable, Tuple
         | 
| 28 | 
            +
             | 
| 29 | 
            +
            from claude_mpm.services.core.base import BaseService
         | 
| 30 | 
            +
            from claude_mpm.config.memory_guardian_config import (
         | 
| 31 | 
            +
                MemoryGuardianConfig,
         | 
| 32 | 
            +
                get_default_config
         | 
| 33 | 
            +
            )
         | 
| 34 | 
            +
            from claude_mpm.utils.platform_memory import (
         | 
| 35 | 
            +
                get_process_memory,
         | 
| 36 | 
            +
                get_system_memory,
         | 
| 37 | 
            +
                check_memory_pressure,
         | 
| 38 | 
            +
                MemoryInfo
         | 
| 39 | 
            +
            )
         | 
| 40 | 
            +
             | 
| 41 | 
            +
             | 
| 42 | 
            +
            class MemoryState(Enum):
         | 
| 43 | 
            +
                """Memory usage state levels."""
         | 
| 44 | 
            +
                NORMAL = "normal"
         | 
| 45 | 
            +
                WARNING = "warning"
         | 
| 46 | 
            +
                CRITICAL = "critical"
         | 
| 47 | 
            +
                EMERGENCY = "emergency"
         | 
| 48 | 
            +
             | 
| 49 | 
            +
             | 
| 50 | 
            +
            class ProcessState(Enum):
         | 
| 51 | 
            +
                """Process lifecycle states."""
         | 
| 52 | 
            +
                NOT_STARTED = "not_started"
         | 
| 53 | 
            +
                STARTING = "starting"
         | 
| 54 | 
            +
                RUNNING = "running"
         | 
| 55 | 
            +
                STOPPING = "stopping"
         | 
| 56 | 
            +
                STOPPED = "stopped"
         | 
| 57 | 
            +
                RESTARTING = "restarting"
         | 
| 58 | 
            +
                FAILED = "failed"
         | 
| 59 | 
            +
             | 
| 60 | 
            +
             | 
| 61 | 
            +
            @dataclass
         | 
| 62 | 
            +
            class RestartAttempt:
         | 
| 63 | 
            +
                """Record of a restart attempt."""
         | 
| 64 | 
            +
                timestamp: float
         | 
| 65 | 
            +
                reason: str
         | 
| 66 | 
            +
                memory_mb: float
         | 
| 67 | 
            +
                success: bool
         | 
| 68 | 
            +
                
         | 
| 69 | 
            +
                def to_dict(self) -> Dict[str, Any]:
         | 
| 70 | 
            +
                    """Convert to dictionary."""
         | 
| 71 | 
            +
                    return {
         | 
| 72 | 
            +
                        'timestamp': self.timestamp,
         | 
| 73 | 
            +
                        'timestamp_iso': datetime.fromtimestamp(self.timestamp).isoformat(),
         | 
| 74 | 
            +
                        'reason': self.reason,
         | 
| 75 | 
            +
                        'memory_mb': self.memory_mb,
         | 
| 76 | 
            +
                        'success': self.success
         | 
| 77 | 
            +
                    }
         | 
| 78 | 
            +
             | 
| 79 | 
            +
             | 
| 80 | 
            +
            @dataclass
         | 
| 81 | 
            +
            class MemoryStats:
         | 
| 82 | 
            +
                """Memory usage statistics."""
         | 
| 83 | 
            +
                current_mb: float = 0.0
         | 
| 84 | 
            +
                peak_mb: float = 0.0
         | 
| 85 | 
            +
                average_mb: float = 0.0
         | 
| 86 | 
            +
                samples: int = 0
         | 
| 87 | 
            +
                last_check: float = 0.0
         | 
| 88 | 
            +
                state: MemoryState = MemoryState.NORMAL
         | 
| 89 | 
            +
                
         | 
| 90 | 
            +
                def update(self, memory_mb: float) -> None:
         | 
| 91 | 
            +
                    """Update statistics with new memory reading."""
         | 
| 92 | 
            +
                    self.current_mb = memory_mb
         | 
| 93 | 
            +
                    self.peak_mb = max(self.peak_mb, memory_mb)
         | 
| 94 | 
            +
                    
         | 
| 95 | 
            +
                    # Update running average
         | 
| 96 | 
            +
                    if self.samples == 0:
         | 
| 97 | 
            +
                        self.average_mb = memory_mb
         | 
| 98 | 
            +
                    else:
         | 
| 99 | 
            +
                        self.average_mb = ((self.average_mb * self.samples) + memory_mb) / (self.samples + 1)
         | 
| 100 | 
            +
                    
         | 
| 101 | 
            +
                    self.samples += 1
         | 
| 102 | 
            +
                    self.last_check = time.time()
         | 
| 103 | 
            +
                
         | 
| 104 | 
            +
                def to_dict(self) -> Dict[str, Any]:
         | 
| 105 | 
            +
                    """Convert to dictionary."""
         | 
| 106 | 
            +
                    return {
         | 
| 107 | 
            +
                        'current_mb': round(self.current_mb, 2),
         | 
| 108 | 
            +
                        'peak_mb': round(self.peak_mb, 2),
         | 
| 109 | 
            +
                        'average_mb': round(self.average_mb, 2),
         | 
| 110 | 
            +
                        'samples': self.samples,
         | 
| 111 | 
            +
                        'last_check': self.last_check,
         | 
| 112 | 
            +
                        'last_check_iso': datetime.fromtimestamp(self.last_check).isoformat() if self.last_check > 0 else None,
         | 
| 113 | 
            +
                        'state': self.state.value
         | 
| 114 | 
            +
                    }
         | 
| 115 | 
            +
             | 
| 116 | 
            +
             | 
| 117 | 
            +
            class MemoryGuardian(BaseService):
         | 
| 118 | 
            +
                """Service for monitoring and managing subprocess memory usage."""
         | 
| 119 | 
            +
                
         | 
| 120 | 
            +
                def __init__(self, config: Optional[MemoryGuardianConfig] = None):
         | 
| 121 | 
            +
                    """Initialize Memory Guardian service.
         | 
| 122 | 
            +
                    
         | 
| 123 | 
            +
                    Args:
         | 
| 124 | 
            +
                        config: Configuration for memory monitoring and management
         | 
| 125 | 
            +
                    """
         | 
| 126 | 
            +
                    super().__init__("MemoryGuardian")
         | 
| 127 | 
            +
                    
         | 
| 128 | 
            +
                    # Configuration
         | 
| 129 | 
            +
                    self.config = config or get_default_config()
         | 
| 130 | 
            +
                    
         | 
| 131 | 
            +
                    # Validate configuration
         | 
| 132 | 
            +
                    issues = self.config.validate()
         | 
| 133 | 
            +
                    if issues:
         | 
| 134 | 
            +
                        for issue in issues:
         | 
| 135 | 
            +
                            self.log_warning(f"Configuration issue: {issue}")
         | 
| 136 | 
            +
                    
         | 
| 137 | 
            +
                    # Process management
         | 
| 138 | 
            +
                    self.process: Optional[subprocess.Popen] = None
         | 
| 139 | 
            +
                    self.process_state = ProcessState.NOT_STARTED
         | 
| 140 | 
            +
                    self.process_pid: Optional[int] = None
         | 
| 141 | 
            +
                    
         | 
| 142 | 
            +
                    # Memory monitoring
         | 
| 143 | 
            +
                    self.memory_stats = MemoryStats()
         | 
| 144 | 
            +
                    self.memory_state = MemoryState.NORMAL
         | 
| 145 | 
            +
                    
         | 
| 146 | 
            +
                    # Restart tracking
         | 
| 147 | 
            +
                    self.restart_attempts: List[RestartAttempt] = []
         | 
| 148 | 
            +
                    self.last_restart_time: float = 0.0
         | 
| 149 | 
            +
                    self.consecutive_failures: int = 0
         | 
| 150 | 
            +
                    
         | 
| 151 | 
            +
                    # Monitoring tasks
         | 
| 152 | 
            +
                    self.monitor_task: Optional[asyncio.Task] = None
         | 
| 153 | 
            +
                    self.monitoring = False
         | 
| 154 | 
            +
                    
         | 
| 155 | 
            +
                    # State preservation hooks (for future implementation)
         | 
| 156 | 
            +
                    self.state_save_hooks: List[Callable[[Dict[str, Any]], None]] = []
         | 
| 157 | 
            +
                    self.state_restore_hooks: List[Callable[[Dict[str, Any]], None]] = []
         | 
| 158 | 
            +
                    
         | 
| 159 | 
            +
                    # Statistics
         | 
| 160 | 
            +
                    self.start_time = time.time()
         | 
| 161 | 
            +
                    self.total_restarts = 0
         | 
| 162 | 
            +
                    self.total_uptime = 0.0
         | 
| 163 | 
            +
                    
         | 
| 164 | 
            +
                    self.log_info(f"Memory Guardian initialized with thresholds: "
         | 
| 165 | 
            +
                                 f"Warning={self.config.thresholds.warning}MB, "
         | 
| 166 | 
            +
                                 f"Critical={self.config.thresholds.critical}MB, "
         | 
| 167 | 
            +
                                 f"Emergency={self.config.thresholds.emergency}MB")
         | 
| 168 | 
            +
                
         | 
| 169 | 
            +
                async def initialize(self) -> bool:
         | 
| 170 | 
            +
                    """Initialize the Memory Guardian service.
         | 
| 171 | 
            +
                    
         | 
| 172 | 
            +
                    Returns:
         | 
| 173 | 
            +
                        True if initialization successful
         | 
| 174 | 
            +
                    """
         | 
| 175 | 
            +
                    try:
         | 
| 176 | 
            +
                        self.log_info("Initializing Memory Guardian service")
         | 
| 177 | 
            +
                        
         | 
| 178 | 
            +
                        # Load persisted state if available
         | 
| 179 | 
            +
                        if self.config.persist_state and self.config.state_file:
         | 
| 180 | 
            +
                            self._load_state()
         | 
| 181 | 
            +
                        
         | 
| 182 | 
            +
                        # Auto-start process if configured
         | 
| 183 | 
            +
                        if self.config.auto_start and self.config.enabled:
         | 
| 184 | 
            +
                            self.log_info("Auto-starting monitored process")
         | 
| 185 | 
            +
                            success = await self.start_process()
         | 
| 186 | 
            +
                            if not success:
         | 
| 187 | 
            +
                                self.log_warning("Failed to auto-start process")
         | 
| 188 | 
            +
                        
         | 
| 189 | 
            +
                        # Start monitoring if enabled
         | 
| 190 | 
            +
                        if self.config.enabled:
         | 
| 191 | 
            +
                            self.start_monitoring()
         | 
| 192 | 
            +
                        
         | 
| 193 | 
            +
                        self._initialized = True
         | 
| 194 | 
            +
                        self.log_info("Memory Guardian service initialized successfully")
         | 
| 195 | 
            +
                        return True
         | 
| 196 | 
            +
                        
         | 
| 197 | 
            +
                    except Exception as e:
         | 
| 198 | 
            +
                        self.log_error(f"Failed to initialize Memory Guardian: {e}")
         | 
| 199 | 
            +
                        return False
         | 
| 200 | 
            +
                
         | 
| 201 | 
            +
                async def shutdown(self) -> None:
         | 
| 202 | 
            +
                    """Shutdown the Memory Guardian service gracefully."""
         | 
| 203 | 
            +
                    try:
         | 
| 204 | 
            +
                        self.log_info("Shutting down Memory Guardian service")
         | 
| 205 | 
            +
                        
         | 
| 206 | 
            +
                        # Stop monitoring
         | 
| 207 | 
            +
                        await self.stop_monitoring()
         | 
| 208 | 
            +
                        
         | 
| 209 | 
            +
                        # Save state if configured
         | 
| 210 | 
            +
                        if self.config.persist_state and self.config.state_file:
         | 
| 211 | 
            +
                            self._save_state()
         | 
| 212 | 
            +
                        
         | 
| 213 | 
            +
                        # Terminate process if running
         | 
| 214 | 
            +
                        if self.process and self.process_state == ProcessState.RUNNING:
         | 
| 215 | 
            +
                            await self.terminate_process()
         | 
| 216 | 
            +
                        
         | 
| 217 | 
            +
                        self._shutdown = True
         | 
| 218 | 
            +
                        self.log_info("Memory Guardian service shutdown complete")
         | 
| 219 | 
            +
                        
         | 
| 220 | 
            +
                    except Exception as e:
         | 
| 221 | 
            +
                        self.log_error(f"Error during Memory Guardian shutdown: {e}")
         | 
| 222 | 
            +
                
         | 
| 223 | 
            +
                async def start_process(self) -> bool:
         | 
| 224 | 
            +
                    """Start the monitored subprocess.
         | 
| 225 | 
            +
                    
         | 
| 226 | 
            +
                    Returns:
         | 
| 227 | 
            +
                        True if process started successfully
         | 
| 228 | 
            +
                    """
         | 
| 229 | 
            +
                    if self.process and self.process_state == ProcessState.RUNNING:
         | 
| 230 | 
            +
                        self.log_warning("Process is already running")
         | 
| 231 | 
            +
                        return True
         | 
| 232 | 
            +
                    
         | 
| 233 | 
            +
                    try:
         | 
| 234 | 
            +
                        self.log_info(f"Starting process: {' '.join(self.config.process_command)}")
         | 
| 235 | 
            +
                        self.process_state = ProcessState.STARTING
         | 
| 236 | 
            +
                        
         | 
| 237 | 
            +
                        # Prepare environment
         | 
| 238 | 
            +
                        env = os.environ.copy()
         | 
| 239 | 
            +
                        env.update(self.config.process_env)
         | 
| 240 | 
            +
                        
         | 
| 241 | 
            +
                        # Build command
         | 
| 242 | 
            +
                        cmd = self.config.process_command + self.config.process_args
         | 
| 243 | 
            +
                        
         | 
| 244 | 
            +
                        # Start subprocess
         | 
| 245 | 
            +
                        self.process = subprocess.Popen(
         | 
| 246 | 
            +
                            cmd,
         | 
| 247 | 
            +
                            env=env,
         | 
| 248 | 
            +
                            cwd=self.config.working_directory,
         | 
| 249 | 
            +
                            stdout=subprocess.PIPE,
         | 
| 250 | 
            +
                            stderr=subprocess.PIPE,
         | 
| 251 | 
            +
                            start_new_session=True  # Create new process group for clean termination
         | 
| 252 | 
            +
                        )
         | 
| 253 | 
            +
                        
         | 
| 254 | 
            +
                        self.process_pid = self.process.pid
         | 
| 255 | 
            +
                        self.process_state = ProcessState.RUNNING
         | 
| 256 | 
            +
                        
         | 
| 257 | 
            +
                        # Reset failure counter on successful start
         | 
| 258 | 
            +
                        self.consecutive_failures = 0
         | 
| 259 | 
            +
                        
         | 
| 260 | 
            +
                        self.log_info(f"Process started successfully with PID {self.process_pid}")
         | 
| 261 | 
            +
                        
         | 
| 262 | 
            +
                        # Give process time to initialize
         | 
| 263 | 
            +
                        await asyncio.sleep(2)
         | 
| 264 | 
            +
                        
         | 
| 265 | 
            +
                        # Check if process is still running
         | 
| 266 | 
            +
                        if self.process.poll() is not None:
         | 
| 267 | 
            +
                            self.log_error(f"Process exited immediately with code {self.process.returncode}")
         | 
| 268 | 
            +
                            self.process_state = ProcessState.FAILED
         | 
| 269 | 
            +
                            return False
         | 
| 270 | 
            +
                        
         | 
| 271 | 
            +
                        return True
         | 
| 272 | 
            +
                        
         | 
| 273 | 
            +
                    except FileNotFoundError:
         | 
| 274 | 
            +
                        self.log_error(f"Command not found: {self.config.process_command[0]}")
         | 
| 275 | 
            +
                        self.process_state = ProcessState.FAILED
         | 
| 276 | 
            +
                        return False
         | 
| 277 | 
            +
                    except Exception as e:
         | 
| 278 | 
            +
                        self.log_error(f"Failed to start process: {e}")
         | 
| 279 | 
            +
                        self.process_state = ProcessState.FAILED
         | 
| 280 | 
            +
                        return False
         | 
| 281 | 
            +
                
         | 
| 282 | 
            +
                async def restart_process(self, reason: str = "Manual restart") -> bool:
         | 
| 283 | 
            +
                    """Restart the monitored process with cooldown and retry logic.
         | 
| 284 | 
            +
                    
         | 
| 285 | 
            +
                    Args:
         | 
| 286 | 
            +
                        reason: Reason for restart
         | 
| 287 | 
            +
                        
         | 
| 288 | 
            +
                    Returns:
         | 
| 289 | 
            +
                        True if restart successful
         | 
| 290 | 
            +
                    """
         | 
| 291 | 
            +
                    self.log_info(f"Initiating process restart: {reason}")
         | 
| 292 | 
            +
                    
         | 
| 293 | 
            +
                    # Check restart attempts
         | 
| 294 | 
            +
                    if not self._can_restart():
         | 
| 295 | 
            +
                        self.log_error("Maximum restart attempts exceeded")
         | 
| 296 | 
            +
                        self.process_state = ProcessState.FAILED
         | 
| 297 | 
            +
                        return False
         | 
| 298 | 
            +
                    
         | 
| 299 | 
            +
                    # Apply cooldown if needed
         | 
| 300 | 
            +
                    cooldown = self._get_restart_cooldown()
         | 
| 301 | 
            +
                    if cooldown > 0:
         | 
| 302 | 
            +
                        self.log_info(f"Applying restart cooldown of {cooldown} seconds")
         | 
| 303 | 
            +
                        await asyncio.sleep(cooldown)
         | 
| 304 | 
            +
                    
         | 
| 305 | 
            +
                    # Record restart attempt
         | 
| 306 | 
            +
                    memory_mb = self.memory_stats.current_mb
         | 
| 307 | 
            +
                    self.process_state = ProcessState.RESTARTING
         | 
| 308 | 
            +
                    
         | 
| 309 | 
            +
                    # Save state before restart
         | 
| 310 | 
            +
                    await self._trigger_state_save()
         | 
| 311 | 
            +
                    
         | 
| 312 | 
            +
                    # Terminate existing process
         | 
| 313 | 
            +
                    if self.process:
         | 
| 314 | 
            +
                        await self.terminate_process()
         | 
| 315 | 
            +
                    
         | 
| 316 | 
            +
                    # Start new process
         | 
| 317 | 
            +
                    success = await self.start_process()
         | 
| 318 | 
            +
                    
         | 
| 319 | 
            +
                    # Record attempt
         | 
| 320 | 
            +
                    attempt = RestartAttempt(
         | 
| 321 | 
            +
                        timestamp=time.time(),
         | 
| 322 | 
            +
                        reason=reason,
         | 
| 323 | 
            +
                        memory_mb=memory_mb,
         | 
| 324 | 
            +
                        success=success
         | 
| 325 | 
            +
                    )
         | 
| 326 | 
            +
                    self.restart_attempts.append(attempt)
         | 
| 327 | 
            +
                    
         | 
| 328 | 
            +
                    if success:
         | 
| 329 | 
            +
                        self.total_restarts += 1
         | 
| 330 | 
            +
                        self.last_restart_time = time.time()
         | 
| 331 | 
            +
                        self.log_info("Process restarted successfully")
         | 
| 332 | 
            +
                        
         | 
| 333 | 
            +
                        # Restore state after restart
         | 
| 334 | 
            +
                        await self._trigger_state_restore()
         | 
| 335 | 
            +
                    else:
         | 
| 336 | 
            +
                        self.consecutive_failures += 1
         | 
| 337 | 
            +
                        self.log_error("Process restart failed")
         | 
| 338 | 
            +
                    
         | 
| 339 | 
            +
                    return success
         | 
| 340 | 
            +
                
         | 
| 341 | 
            +
                async def terminate_process(self, timeout: Optional[int] = None) -> bool:
         | 
| 342 | 
            +
                    """Terminate the monitored process gracefully with escalation.
         | 
| 343 | 
            +
                    
         | 
| 344 | 
            +
                    Args:
         | 
| 345 | 
            +
                        timeout: Override timeout for graceful shutdown
         | 
| 346 | 
            +
                        
         | 
| 347 | 
            +
                    Returns:
         | 
| 348 | 
            +
                        True if process terminated successfully
         | 
| 349 | 
            +
                    """
         | 
| 350 | 
            +
                    if not self.process:
         | 
| 351 | 
            +
                        return True
         | 
| 352 | 
            +
                    
         | 
| 353 | 
            +
                    timeout = timeout or self.config.restart_policy.graceful_timeout
         | 
| 354 | 
            +
                    
         | 
| 355 | 
            +
                    try:
         | 
| 356 | 
            +
                        self.log_info(f"Terminating process {self.process_pid}")
         | 
| 357 | 
            +
                        self.process_state = ProcessState.STOPPING
         | 
| 358 | 
            +
                        
         | 
| 359 | 
            +
                        # Try graceful termination first (SIGTERM)
         | 
| 360 | 
            +
                        if platform.system() != 'Windows':
         | 
| 361 | 
            +
                            self.process.terminate()
         | 
| 362 | 
            +
                        else:
         | 
| 363 | 
            +
                            # On Windows, terminate() is already forceful
         | 
| 364 | 
            +
                            self.process.terminate()
         | 
| 365 | 
            +
                        
         | 
| 366 | 
            +
                        # Wait for graceful shutdown
         | 
| 367 | 
            +
                        try:
         | 
| 368 | 
            +
                            self.log_debug(f"Waiting {timeout}s for graceful shutdown")
         | 
| 369 | 
            +
                            await asyncio.wait_for(
         | 
| 370 | 
            +
                                asyncio.create_task(self._wait_for_process()),
         | 
| 371 | 
            +
                                timeout=timeout
         | 
| 372 | 
            +
                            )
         | 
| 373 | 
            +
                            self.log_info("Process terminated gracefully")
         | 
| 374 | 
            +
                            
         | 
| 375 | 
            +
                        except asyncio.TimeoutError:
         | 
| 376 | 
            +
                            # Escalate to SIGKILL
         | 
| 377 | 
            +
                            self.log_warning("Graceful shutdown timeout, forcing termination")
         | 
| 378 | 
            +
                            
         | 
| 379 | 
            +
                            if platform.system() != 'Windows':
         | 
| 380 | 
            +
                                self.process.kill()
         | 
| 381 | 
            +
                            else:
         | 
| 382 | 
            +
                                # On Windows, use taskkill /F
         | 
| 383 | 
            +
                                subprocess.run(
         | 
| 384 | 
            +
                                    ['taskkill', '/F', '/PID', str(self.process_pid)],
         | 
| 385 | 
            +
                                    capture_output=True
         | 
| 386 | 
            +
                                )
         | 
| 387 | 
            +
                            
         | 
| 388 | 
            +
                            # Wait for forced termination
         | 
| 389 | 
            +
                            try:
         | 
| 390 | 
            +
                                await asyncio.wait_for(
         | 
| 391 | 
            +
                                    asyncio.create_task(self._wait_for_process()),
         | 
| 392 | 
            +
                                    timeout=self.config.restart_policy.force_kill_timeout
         | 
| 393 | 
            +
                                )
         | 
| 394 | 
            +
                                self.log_info("Process terminated forcefully")
         | 
| 395 | 
            +
                            except asyncio.TimeoutError:
         | 
| 396 | 
            +
                                self.log_error("Failed to terminate process")
         | 
| 397 | 
            +
                                return False
         | 
| 398 | 
            +
                        
         | 
| 399 | 
            +
                        self.process = None
         | 
| 400 | 
            +
                        self.process_pid = None
         | 
| 401 | 
            +
                        self.process_state = ProcessState.STOPPED
         | 
| 402 | 
            +
                        return True
         | 
| 403 | 
            +
                        
         | 
| 404 | 
            +
                    except Exception as e:
         | 
| 405 | 
            +
                        self.log_error(f"Error terminating process: {e}")
         | 
| 406 | 
            +
                        return False
         | 
| 407 | 
            +
                
         | 
| 408 | 
            +
                async def _wait_for_process(self) -> None:
         | 
| 409 | 
            +
                    """Wait for process to exit."""
         | 
| 410 | 
            +
                    while self.process and self.process.poll() is None:
         | 
| 411 | 
            +
                        await asyncio.sleep(0.1)
         | 
| 412 | 
            +
                
         | 
| 413 | 
            +
                def get_memory_usage(self) -> Optional[float]:
         | 
| 414 | 
            +
                    """Get current memory usage of monitored process in MB.
         | 
| 415 | 
            +
                    
         | 
| 416 | 
            +
                    Returns:
         | 
| 417 | 
            +
                        Memory usage in MB or None if unable to determine
         | 
| 418 | 
            +
                    """
         | 
| 419 | 
            +
                    if not self.process or self.process_state != ProcessState.RUNNING:
         | 
| 420 | 
            +
                        return None
         | 
| 421 | 
            +
                    
         | 
| 422 | 
            +
                    try:
         | 
| 423 | 
            +
                        # Get memory info using platform utilities
         | 
| 424 | 
            +
                        mem_info = get_process_memory(self.process_pid)
         | 
| 425 | 
            +
                        if mem_info:
         | 
| 426 | 
            +
                            return mem_info.rss_mb
         | 
| 427 | 
            +
                        
         | 
| 428 | 
            +
                        self.log_warning(f"Unable to get memory info for PID {self.process_pid}")
         | 
| 429 | 
            +
                        return None
         | 
| 430 | 
            +
                        
         | 
| 431 | 
            +
                    except Exception as e:
         | 
| 432 | 
            +
                        self.log_error(f"Error getting memory usage: {e}")
         | 
| 433 | 
            +
                        return None
         | 
| 434 | 
            +
                
         | 
| 435 | 
            +
                async def monitor_memory(self) -> None:
         | 
| 436 | 
            +
                    """Check memory usage and take action if thresholds exceeded."""
         | 
| 437 | 
            +
                    if not self.process or self.process_state != ProcessState.RUNNING:
         | 
| 438 | 
            +
                        return
         | 
| 439 | 
            +
                    
         | 
| 440 | 
            +
                    # Check if process is still alive
         | 
| 441 | 
            +
                    if self.process.poll() is not None:
         | 
| 442 | 
            +
                        self.log_warning(f"Process exited with code {self.process.returncode}")
         | 
| 443 | 
            +
                        self.process_state = ProcessState.STOPPED
         | 
| 444 | 
            +
                        self.process = None
         | 
| 445 | 
            +
                        self.process_pid = None
         | 
| 446 | 
            +
                        
         | 
| 447 | 
            +
                        # Auto-restart if configured
         | 
| 448 | 
            +
                        if self.config.auto_start:
         | 
| 449 | 
            +
                            await self.restart_process("Process exited unexpectedly")
         | 
| 450 | 
            +
                        return
         | 
| 451 | 
            +
                    
         | 
| 452 | 
            +
                    # Get memory usage
         | 
| 453 | 
            +
                    memory_mb = self.get_memory_usage()
         | 
| 454 | 
            +
                    if memory_mb is None:
         | 
| 455 | 
            +
                        return
         | 
| 456 | 
            +
                    
         | 
| 457 | 
            +
                    # Update statistics
         | 
| 458 | 
            +
                    self.memory_stats.update(memory_mb)
         | 
| 459 | 
            +
                    
         | 
| 460 | 
            +
                    # Determine memory state
         | 
| 461 | 
            +
                    old_state = self.memory_state
         | 
| 462 | 
            +
                    
         | 
| 463 | 
            +
                    if memory_mb >= self.config.thresholds.emergency:
         | 
| 464 | 
            +
                        self.memory_state = MemoryState.EMERGENCY
         | 
| 465 | 
            +
                    elif memory_mb >= self.config.thresholds.critical:
         | 
| 466 | 
            +
                        self.memory_state = MemoryState.CRITICAL
         | 
| 467 | 
            +
                    elif memory_mb >= self.config.thresholds.warning:
         | 
| 468 | 
            +
                        self.memory_state = MemoryState.WARNING
         | 
| 469 | 
            +
                    else:
         | 
| 470 | 
            +
                        self.memory_state = MemoryState.NORMAL
         | 
| 471 | 
            +
                    
         | 
| 472 | 
            +
                    self.memory_stats.state = self.memory_state
         | 
| 473 | 
            +
                    
         | 
| 474 | 
            +
                    # Log state changes
         | 
| 475 | 
            +
                    if self.memory_state != old_state:
         | 
| 476 | 
            +
                        self.log_info(f"Memory state changed: {old_state.value} -> {self.memory_state.value} "
         | 
| 477 | 
            +
                                     f"(current: {memory_mb:.2f}MB)")
         | 
| 478 | 
            +
                    
         | 
| 479 | 
            +
                    # Take action based on state
         | 
| 480 | 
            +
                    if self.memory_state == MemoryState.EMERGENCY:
         | 
| 481 | 
            +
                        self.log_critical(f"Emergency memory threshold exceeded: {memory_mb:.2f}MB")
         | 
| 482 | 
            +
                        await self.restart_process(f"Emergency memory threshold exceeded ({memory_mb:.2f}MB)")
         | 
| 483 | 
            +
                        
         | 
| 484 | 
            +
                    elif self.memory_state == MemoryState.CRITICAL:
         | 
| 485 | 
            +
                        self.log_warning(f"Critical memory threshold exceeded: {memory_mb:.2f}MB")
         | 
| 486 | 
            +
                        # Check if we've been in critical state for too long
         | 
| 487 | 
            +
                        if self._should_restart_for_critical():
         | 
| 488 | 
            +
                            await self.restart_process(f"Sustained critical memory usage ({memory_mb:.2f}MB)")
         | 
| 489 | 
            +
                    
         | 
| 490 | 
            +
                    elif self.memory_state == MemoryState.WARNING:
         | 
| 491 | 
            +
                        self.log_debug(f"Warning memory threshold exceeded: {memory_mb:.2f}MB")
         | 
| 492 | 
            +
                    
         | 
| 493 | 
            +
                    # Log periodic summary
         | 
| 494 | 
            +
                    if self.config.monitoring.log_memory_stats:
         | 
| 495 | 
            +
                        if time.time() - self.memory_stats.last_check > self.config.monitoring.log_interval:
         | 
| 496 | 
            +
                            self._log_memory_summary()
         | 
| 497 | 
            +
                
         | 
| 498 | 
            +
                def start_monitoring(self) -> None:
         | 
| 499 | 
            +
                    """Start continuous memory monitoring."""
         | 
| 500 | 
            +
                    if self.monitoring:
         | 
| 501 | 
            +
                        self.log_warning("Monitoring is already active")
         | 
| 502 | 
            +
                        return
         | 
| 503 | 
            +
                    
         | 
| 504 | 
            +
                    self.monitoring = True
         | 
| 505 | 
            +
                    self.monitor_task = asyncio.create_task(self._monitoring_loop())
         | 
| 506 | 
            +
                    self.log_info("Started memory monitoring")
         | 
| 507 | 
            +
                
         | 
| 508 | 
            +
                async def stop_monitoring(self) -> None:
         | 
| 509 | 
            +
                    """Stop continuous memory monitoring."""
         | 
| 510 | 
            +
                    if not self.monitoring:
         | 
| 511 | 
            +
                        return
         | 
| 512 | 
            +
                    
         | 
| 513 | 
            +
                    self.monitoring = False
         | 
| 514 | 
            +
                    
         | 
| 515 | 
            +
                    if self.monitor_task:
         | 
| 516 | 
            +
                        self.monitor_task.cancel()
         | 
| 517 | 
            +
                        try:
         | 
| 518 | 
            +
                            await self.monitor_task
         | 
| 519 | 
            +
                        except asyncio.CancelledError:
         | 
| 520 | 
            +
                            pass
         | 
| 521 | 
            +
                        self.monitor_task = None
         | 
| 522 | 
            +
                    
         | 
| 523 | 
            +
                    self.log_info("Stopped memory monitoring")
         | 
| 524 | 
            +
                
         | 
| 525 | 
            +
                async def _monitoring_loop(self) -> None:
         | 
| 526 | 
            +
                    """Continuous monitoring loop."""
         | 
| 527 | 
            +
                    try:
         | 
| 528 | 
            +
                        while self.monitoring:
         | 
| 529 | 
            +
                            try:
         | 
| 530 | 
            +
                                await self.monitor_memory()
         | 
| 531 | 
            +
                                
         | 
| 532 | 
            +
                                # Get check interval based on current state
         | 
| 533 | 
            +
                                interval = self.config.monitoring.get_check_interval(
         | 
| 534 | 
            +
                                    self.memory_state.value
         | 
| 535 | 
            +
                                )
         | 
| 536 | 
            +
                                
         | 
| 537 | 
            +
                                await asyncio.sleep(interval)
         | 
| 538 | 
            +
                                
         | 
| 539 | 
            +
                            except Exception as e:
         | 
| 540 | 
            +
                                self.log_error(f"Error in monitoring loop: {e}")
         | 
| 541 | 
            +
                                await asyncio.sleep(5)  # Brief pause before retry
         | 
| 542 | 
            +
                                
         | 
| 543 | 
            +
                    except asyncio.CancelledError:
         | 
| 544 | 
            +
                        self.log_debug("Monitoring loop cancelled")
         | 
| 545 | 
            +
                
         | 
| 546 | 
            +
                def _can_restart(self) -> bool:
         | 
| 547 | 
            +
                    """Check if restart is allowed based on policy.
         | 
| 548 | 
            +
                    
         | 
| 549 | 
            +
                    Returns:
         | 
| 550 | 
            +
                        True if restart is allowed
         | 
| 551 | 
            +
                    """
         | 
| 552 | 
            +
                    # Check max attempts
         | 
| 553 | 
            +
                    if self.config.restart_policy.max_attempts <= 0:
         | 
| 554 | 
            +
                        return True  # Unlimited restarts
         | 
| 555 | 
            +
                    
         | 
| 556 | 
            +
                    # Count recent attempts
         | 
| 557 | 
            +
                    window_start = time.time() - self.config.restart_policy.attempt_window
         | 
| 558 | 
            +
                    recent_attempts = [
         | 
| 559 | 
            +
                        a for a in self.restart_attempts
         | 
| 560 | 
            +
                        if a.timestamp >= window_start
         | 
| 561 | 
            +
                    ]
         | 
| 562 | 
            +
                    
         | 
| 563 | 
            +
                    return len(recent_attempts) < self.config.restart_policy.max_attempts
         | 
| 564 | 
            +
                
         | 
| 565 | 
            +
                def _get_restart_cooldown(self) -> int:
         | 
| 566 | 
            +
                    """Get cooldown period for next restart.
         | 
| 567 | 
            +
                    
         | 
| 568 | 
            +
                    Returns:
         | 
| 569 | 
            +
                        Cooldown period in seconds
         | 
| 570 | 
            +
                    """
         | 
| 571 | 
            +
                    if not self.restart_attempts:
         | 
| 572 | 
            +
                        return 0
         | 
| 573 | 
            +
                    
         | 
| 574 | 
            +
                    # Calculate based on consecutive failures
         | 
| 575 | 
            +
                    return self.config.restart_policy.get_cooldown(self.consecutive_failures + 1)
         | 
| 576 | 
            +
                
         | 
| 577 | 
            +
                def _should_restart_for_critical(self) -> bool:
         | 
| 578 | 
            +
                    """Determine if we should restart due to sustained critical memory.
         | 
| 579 | 
            +
                    
         | 
| 580 | 
            +
                    Returns:
         | 
| 581 | 
            +
                        True if restart should be triggered
         | 
| 582 | 
            +
                    """
         | 
| 583 | 
            +
                    # Check how long we've been in critical state
         | 
| 584 | 
            +
                    critical_duration = 60  # seconds
         | 
| 585 | 
            +
                    
         | 
| 586 | 
            +
                    # Look at recent memory samples
         | 
| 587 | 
            +
                    recent_samples = [
         | 
| 588 | 
            +
                        s for s in self.restart_attempts
         | 
| 589 | 
            +
                        if s.timestamp >= time.time() - critical_duration
         | 
| 590 | 
            +
                    ]
         | 
| 591 | 
            +
                    
         | 
| 592 | 
            +
                    # If we've been critical for the duration, restart
         | 
| 593 | 
            +
                    # This is a simplified check - could be enhanced
         | 
| 594 | 
            +
                    return self.memory_state == MemoryState.CRITICAL and len(recent_samples) == 0
         | 
| 595 | 
            +
                
         | 
| 596 | 
            +
                async def _trigger_state_save(self) -> None:
         | 
| 597 | 
            +
                    """Trigger state preservation hooks."""
         | 
| 598 | 
            +
                    if not self.state_save_hooks:
         | 
| 599 | 
            +
                        return
         | 
| 600 | 
            +
                    
         | 
| 601 | 
            +
                    state = self.get_state()
         | 
| 602 | 
            +
                    
         | 
| 603 | 
            +
                    for hook in self.state_save_hooks:
         | 
| 604 | 
            +
                        try:
         | 
| 605 | 
            +
                            hook(state)
         | 
| 606 | 
            +
                        except Exception as e:
         | 
| 607 | 
            +
                            self.log_error(f"State save hook failed: {e}")
         | 
| 608 | 
            +
                
         | 
| 609 | 
            +
                async def _trigger_state_restore(self) -> None:
         | 
| 610 | 
            +
                    """Trigger state restoration hooks."""
         | 
| 611 | 
            +
                    if not self.state_restore_hooks:
         | 
| 612 | 
            +
                        return
         | 
| 613 | 
            +
                    
         | 
| 614 | 
            +
                    state = self.get_state()
         | 
| 615 | 
            +
                    
         | 
| 616 | 
            +
                    for hook in self.state_restore_hooks:
         | 
| 617 | 
            +
                        try:
         | 
| 618 | 
            +
                            hook(state)
         | 
| 619 | 
            +
                        except Exception as e:
         | 
| 620 | 
            +
                            self.log_error(f"State restore hook failed: {e}")
         | 
| 621 | 
            +
                
         | 
| 622 | 
            +
                def _log_memory_summary(self) -> None:
         | 
| 623 | 
            +
                    """Log memory usage summary."""
         | 
| 624 | 
            +
                    uptime = time.time() - self.start_time
         | 
| 625 | 
            +
                    
         | 
| 626 | 
            +
                    self.log_info(
         | 
| 627 | 
            +
                        f"Memory Summary - "
         | 
| 628 | 
            +
                        f"Current: {self.memory_stats.current_mb:.2f}MB, "
         | 
| 629 | 
            +
                        f"Peak: {self.memory_stats.peak_mb:.2f}MB, "
         | 
| 630 | 
            +
                        f"Average: {self.memory_stats.average_mb:.2f}MB, "
         | 
| 631 | 
            +
                        f"State: {self.memory_state.value}, "
         | 
| 632 | 
            +
                        f"Restarts: {self.total_restarts}, "
         | 
| 633 | 
            +
                        f"Uptime: {uptime/3600:.2f}h"
         | 
| 634 | 
            +
                    )
         | 
| 635 | 
            +
                
         | 
| 636 | 
            +
                def _save_state(self) -> None:
         | 
| 637 | 
            +
                    """Save service state to file."""
         | 
| 638 | 
            +
                    if not self.config.state_file:
         | 
| 639 | 
            +
                        return
         | 
| 640 | 
            +
                    
         | 
| 641 | 
            +
                    try:
         | 
| 642 | 
            +
                        state = self.get_state()
         | 
| 643 | 
            +
                        state_path = Path(self.config.state_file)
         | 
| 644 | 
            +
                        state_path.parent.mkdir(parents=True, exist_ok=True)
         | 
| 645 | 
            +
                        
         | 
| 646 | 
            +
                        with open(state_path, 'w') as f:
         | 
| 647 | 
            +
                            json.dump(state, f, indent=2)
         | 
| 648 | 
            +
                        
         | 
| 649 | 
            +
                        self.log_debug(f"Saved state to {state_path}")
         | 
| 650 | 
            +
                        
         | 
| 651 | 
            +
                    except Exception as e:
         | 
| 652 | 
            +
                        self.log_error(f"Failed to save state: {e}")
         | 
| 653 | 
            +
                
         | 
| 654 | 
            +
                def _load_state(self) -> None:
         | 
| 655 | 
            +
                    """Load service state from file."""
         | 
| 656 | 
            +
                    if not self.config.state_file:
         | 
| 657 | 
            +
                        return
         | 
| 658 | 
            +
                    
         | 
| 659 | 
            +
                    try:
         | 
| 660 | 
            +
                        state_path = Path(self.config.state_file)
         | 
| 661 | 
            +
                        if not state_path.exists():
         | 
| 662 | 
            +
                            return
         | 
| 663 | 
            +
                        
         | 
| 664 | 
            +
                        with open(state_path, 'r') as f:
         | 
| 665 | 
            +
                            state = json.load(f)
         | 
| 666 | 
            +
                        
         | 
| 667 | 
            +
                        # Restore relevant state
         | 
| 668 | 
            +
                        self.total_restarts = state.get('total_restarts', 0)
         | 
| 669 | 
            +
                        self.memory_stats.peak_mb = state.get('memory_stats', {}).get('peak_mb', 0.0)
         | 
| 670 | 
            +
                        
         | 
| 671 | 
            +
                        # Restore restart attempts
         | 
| 672 | 
            +
                        attempts = state.get('restart_attempts', [])
         | 
| 673 | 
            +
                        for attempt_data in attempts:
         | 
| 674 | 
            +
                            attempt = RestartAttempt(
         | 
| 675 | 
            +
                                timestamp=attempt_data['timestamp'],
         | 
| 676 | 
            +
                                reason=attempt_data['reason'],
         | 
| 677 | 
            +
                                memory_mb=attempt_data['memory_mb'],
         | 
| 678 | 
            +
                                success=attempt_data['success']
         | 
| 679 | 
            +
                            )
         | 
| 680 | 
            +
                            self.restart_attempts.append(attempt)
         | 
| 681 | 
            +
                        
         | 
| 682 | 
            +
                        self.log_debug(f"Loaded state from {state_path}")
         | 
| 683 | 
            +
                        
         | 
| 684 | 
            +
                    except Exception as e:
         | 
| 685 | 
            +
                        self.log_error(f"Failed to load state: {e}")
         | 
| 686 | 
            +
                
         | 
| 687 | 
            +
                def add_state_save_hook(self, hook: Callable[[Dict[str, Any]], None]) -> None:
         | 
| 688 | 
            +
                    """Add a hook to be called before process restart.
         | 
| 689 | 
            +
                    
         | 
| 690 | 
            +
                    Args:
         | 
| 691 | 
            +
                        hook: Function to call with current state
         | 
| 692 | 
            +
                    """
         | 
| 693 | 
            +
                    self.state_save_hooks.append(hook)
         | 
| 694 | 
            +
                    self.log_debug(f"Added state save hook: {hook.__name__}")
         | 
| 695 | 
            +
                
         | 
| 696 | 
            +
                def add_state_restore_hook(self, hook: Callable[[Dict[str, Any]], None]) -> None:
         | 
| 697 | 
            +
                    """Add a hook to be called after process restart.
         | 
| 698 | 
            +
                    
         | 
| 699 | 
            +
                    Args:
         | 
| 700 | 
            +
                        hook: Function to call with saved state
         | 
| 701 | 
            +
                    """
         | 
| 702 | 
            +
                    self.state_restore_hooks.append(hook)
         | 
| 703 | 
            +
                    self.log_debug(f"Added state restore hook: {hook.__name__}")
         | 
| 704 | 
            +
                
         | 
| 705 | 
            +
                def get_state(self) -> Dict[str, Any]:
         | 
| 706 | 
            +
                    """Get current service state.
         | 
| 707 | 
            +
                    
         | 
| 708 | 
            +
                    Returns:
         | 
| 709 | 
            +
                        Dictionary containing service state
         | 
| 710 | 
            +
                    """
         | 
| 711 | 
            +
                    return {
         | 
| 712 | 
            +
                        'process_state': self.process_state.value,
         | 
| 713 | 
            +
                        'process_pid': self.process_pid,
         | 
| 714 | 
            +
                        'memory_state': self.memory_state.value,
         | 
| 715 | 
            +
                        'memory_stats': self.memory_stats.to_dict(),
         | 
| 716 | 
            +
                        'total_restarts': self.total_restarts,
         | 
| 717 | 
            +
                        'consecutive_failures': self.consecutive_failures,
         | 
| 718 | 
            +
                        'restart_attempts': [a.to_dict() for a in self.restart_attempts[-10:]],  # Last 10
         | 
| 719 | 
            +
                        'config': self.config.to_dict(),
         | 
| 720 | 
            +
                        'start_time': self.start_time,
         | 
| 721 | 
            +
                        'monitoring': self.monitoring
         | 
| 722 | 
            +
                    }
         | 
| 723 | 
            +
                
         | 
| 724 | 
            +
                def get_status(self) -> Dict[str, Any]:
         | 
| 725 | 
            +
                    """Get current service status.
         | 
| 726 | 
            +
                    
         | 
| 727 | 
            +
                    Returns:
         | 
| 728 | 
            +
                        Dictionary containing service status
         | 
| 729 | 
            +
                    """
         | 
| 730 | 
            +
                    uptime = time.time() - self.start_time if self.process else 0
         | 
| 731 | 
            +
                    
         | 
| 732 | 
            +
                    # Get system memory info
         | 
| 733 | 
            +
                    total_mem, available_mem = get_system_memory()
         | 
| 734 | 
            +
                    
         | 
| 735 | 
            +
                    return {
         | 
| 736 | 
            +
                        'enabled': self.config.enabled,
         | 
| 737 | 
            +
                        'process': {
         | 
| 738 | 
            +
                            'state': self.process_state.value,
         | 
| 739 | 
            +
                            'pid': self.process_pid,
         | 
| 740 | 
            +
                            'uptime_seconds': uptime,
         | 
| 741 | 
            +
                            'uptime_hours': uptime / 3600 if uptime > 0 else 0
         | 
| 742 | 
            +
                        },
         | 
| 743 | 
            +
                        'memory': {
         | 
| 744 | 
            +
                            'current_mb': self.memory_stats.current_mb,
         | 
| 745 | 
            +
                            'peak_mb': self.memory_stats.peak_mb,
         | 
| 746 | 
            +
                            'average_mb': self.memory_stats.average_mb,
         | 
| 747 | 
            +
                            'state': self.memory_state.value,
         | 
| 748 | 
            +
                            'thresholds': {
         | 
| 749 | 
            +
                                'warning_mb': self.config.thresholds.warning,
         | 
| 750 | 
            +
                                'critical_mb': self.config.thresholds.critical,
         | 
| 751 | 
            +
                                'emergency_mb': self.config.thresholds.emergency
         | 
| 752 | 
            +
                            },
         | 
| 753 | 
            +
                            'system': {
         | 
| 754 | 
            +
                                'total_mb': total_mem / (1024 * 1024) if total_mem > 0 else 0,
         | 
| 755 | 
            +
                                'available_mb': available_mem / (1024 * 1024) if available_mem > 0 else 0,
         | 
| 756 | 
            +
                                'pressure': check_memory_pressure()
         | 
| 757 | 
            +
                            }
         | 
| 758 | 
            +
                        },
         | 
| 759 | 
            +
                        'restarts': {
         | 
| 760 | 
            +
                            'total': self.total_restarts,
         | 
| 761 | 
            +
                            'consecutive_failures': self.consecutive_failures,
         | 
| 762 | 
            +
                            'can_restart': self._can_restart(),
         | 
| 763 | 
            +
                            'recent_attempts': [a.to_dict() for a in self.restart_attempts[-5:]]
         | 
| 764 | 
            +
                        },
         | 
| 765 | 
            +
                        'monitoring': {
         | 
| 766 | 
            +
                            'active': self.monitoring,
         | 
| 767 | 
            +
                            'check_interval': self.config.monitoring.get_check_interval(self.memory_state.value),
         | 
| 768 | 
            +
                            'samples': self.memory_stats.samples
         | 
| 769 | 
            +
                        }
         | 
| 770 | 
            +
                    }
         |