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,219 @@ | |
| 1 | 
            +
            """Experimental features configuration for Claude MPM.
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            WHY: This module manages experimental and beta features, providing a centralized
         | 
| 4 | 
            +
            way to control feature flags and display appropriate warnings to users.
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            DESIGN DECISION: Use a simple configuration class with static defaults that can
         | 
| 7 | 
            +
            be overridden through environment variables or config files. This allows for
         | 
| 8 | 
            +
            gradual rollout of experimental features while maintaining stability in production.
         | 
| 9 | 
            +
            """
         | 
| 10 | 
            +
             | 
| 11 | 
            +
            import os
         | 
| 12 | 
            +
            from typing import Dict, Any, Optional
         | 
| 13 | 
            +
            from pathlib import Path
         | 
| 14 | 
            +
            import json
         | 
| 15 | 
            +
             | 
| 16 | 
            +
             | 
| 17 | 
            +
            class ExperimentalFeatures:
         | 
| 18 | 
            +
                """Manages experimental feature flags and warnings.
         | 
| 19 | 
            +
                
         | 
| 20 | 
            +
                WHY: Experimental features need special handling to ensure users understand
         | 
| 21 | 
            +
                they are using beta functionality that may change or have issues.
         | 
| 22 | 
            +
                
         | 
| 23 | 
            +
                DESIGN DECISION: Use environment variables for quick override during development,
         | 
| 24 | 
            +
                but also support configuration files for persistent settings.
         | 
| 25 | 
            +
                """
         | 
| 26 | 
            +
                
         | 
| 27 | 
            +
                # Default feature flags
         | 
| 28 | 
            +
                DEFAULTS = {
         | 
| 29 | 
            +
                    'enable_memory_guardian': False,  # Memory Guardian is experimental
         | 
| 30 | 
            +
                    'enable_mcp_gateway': False,      # MCP Gateway is experimental
         | 
| 31 | 
            +
                    'enable_advanced_aggregation': False,  # Advanced aggregation features
         | 
| 32 | 
            +
                    'show_experimental_warnings': True,  # Show warnings for experimental features
         | 
| 33 | 
            +
                    'require_experimental_acceptance': True,  # Require explicit acceptance
         | 
| 34 | 
            +
                }
         | 
| 35 | 
            +
                
         | 
| 36 | 
            +
                # Warning messages for experimental features
         | 
| 37 | 
            +
                WARNINGS = {
         | 
| 38 | 
            +
                    'memory_guardian': (
         | 
| 39 | 
            +
                        "⚠️  EXPERIMENTAL FEATURE: Memory Guardian is in beta.\n"
         | 
| 40 | 
            +
                        "   This feature may change or have issues. Use with caution in production.\n"
         | 
| 41 | 
            +
                        "   Report issues at: https://github.com/bluescreen10/claude-mpm/issues"
         | 
| 42 | 
            +
                    ),
         | 
| 43 | 
            +
                    'mcp_gateway': (
         | 
| 44 | 
            +
                        "⚠️  EXPERIMENTAL FEATURE: MCP Gateway is in early access.\n"
         | 
| 45 | 
            +
                        "   Tool integration may be unstable. Not recommended for production use."
         | 
| 46 | 
            +
                    ),
         | 
| 47 | 
            +
                    'advanced_aggregation': (
         | 
| 48 | 
            +
                        "⚠️  EXPERIMENTAL FEATURE: Advanced aggregation is under development.\n"
         | 
| 49 | 
            +
                        "   Results may vary. Please verify outputs manually."
         | 
| 50 | 
            +
                    ),
         | 
| 51 | 
            +
                }
         | 
| 52 | 
            +
                
         | 
| 53 | 
            +
                def __init__(self, config_file: Optional[Path] = None):
         | 
| 54 | 
            +
                    """Initialize experimental features configuration.
         | 
| 55 | 
            +
                    
         | 
| 56 | 
            +
                    Args:
         | 
| 57 | 
            +
                        config_file: Optional path to configuration file
         | 
| 58 | 
            +
                    """
         | 
| 59 | 
            +
                    self._features = self.DEFAULTS.copy()
         | 
| 60 | 
            +
                    self._config_file = config_file
         | 
| 61 | 
            +
                    self._load_configuration()
         | 
| 62 | 
            +
                    self._apply_environment_overrides()
         | 
| 63 | 
            +
                
         | 
| 64 | 
            +
                def _load_configuration(self):
         | 
| 65 | 
            +
                    """Load configuration from file if it exists.
         | 
| 66 | 
            +
                    
         | 
| 67 | 
            +
                    WHY: Allow persistent configuration of experimental features without
         | 
| 68 | 
            +
                    requiring environment variables to be set every time.
         | 
| 69 | 
            +
                    """
         | 
| 70 | 
            +
                    if self._config_file and self._config_file.exists():
         | 
| 71 | 
            +
                        try:
         | 
| 72 | 
            +
                            with open(self._config_file, 'r') as f:
         | 
| 73 | 
            +
                                config = json.load(f)
         | 
| 74 | 
            +
                                experimental = config.get('experimental_features', {})
         | 
| 75 | 
            +
                                self._features.update(experimental)
         | 
| 76 | 
            +
                        except Exception:
         | 
| 77 | 
            +
                            # Silently ignore configuration errors for experimental features
         | 
| 78 | 
            +
                            pass
         | 
| 79 | 
            +
                
         | 
| 80 | 
            +
                def _apply_environment_overrides(self):
         | 
| 81 | 
            +
                    """Apply environment variable overrides.
         | 
| 82 | 
            +
                    
         | 
| 83 | 
            +
                    WHY: Environment variables provide a quick way to enable/disable features
         | 
| 84 | 
            +
                    during development and testing without modifying configuration files.
         | 
| 85 | 
            +
                    
         | 
| 86 | 
            +
                    Format: CLAUDE_MPM_EXPERIMENTAL_<FEATURE_NAME>=true/false
         | 
| 87 | 
            +
                    """
         | 
| 88 | 
            +
                    for key in self._features:
         | 
| 89 | 
            +
                        env_key = f"CLAUDE_MPM_EXPERIMENTAL_{key.upper()}"
         | 
| 90 | 
            +
                        if env_key in os.environ:
         | 
| 91 | 
            +
                            value = os.environ[env_key].lower()
         | 
| 92 | 
            +
                            self._features[key] = value in ('true', '1', 'yes', 'on')
         | 
| 93 | 
            +
                
         | 
| 94 | 
            +
                def is_enabled(self, feature: str) -> bool:
         | 
| 95 | 
            +
                    """Check if a feature is enabled.
         | 
| 96 | 
            +
                    
         | 
| 97 | 
            +
                    Args:
         | 
| 98 | 
            +
                        feature: Feature name (without 'enable_' prefix)
         | 
| 99 | 
            +
                        
         | 
| 100 | 
            +
                    Returns:
         | 
| 101 | 
            +
                        True if the feature is enabled
         | 
| 102 | 
            +
                    """
         | 
| 103 | 
            +
                    key = f"enable_{feature}" if not feature.startswith('enable_') else feature
         | 
| 104 | 
            +
                    return self._features.get(key, False)
         | 
| 105 | 
            +
                
         | 
| 106 | 
            +
                def get_warning(self, feature: str) -> Optional[str]:
         | 
| 107 | 
            +
                    """Get warning message for a feature.
         | 
| 108 | 
            +
                    
         | 
| 109 | 
            +
                    Args:
         | 
| 110 | 
            +
                        feature: Feature name
         | 
| 111 | 
            +
                        
         | 
| 112 | 
            +
                    Returns:
         | 
| 113 | 
            +
                        Warning message or None if no warning exists
         | 
| 114 | 
            +
                    """
         | 
| 115 | 
            +
                    return self.WARNINGS.get(feature)
         | 
| 116 | 
            +
                
         | 
| 117 | 
            +
                def should_show_warning(self, feature: str) -> bool:
         | 
| 118 | 
            +
                    """Check if warning should be shown for a feature.
         | 
| 119 | 
            +
                    
         | 
| 120 | 
            +
                    Args:
         | 
| 121 | 
            +
                        feature: Feature name
         | 
| 122 | 
            +
                        
         | 
| 123 | 
            +
                    Returns:
         | 
| 124 | 
            +
                        True if warning should be displayed
         | 
| 125 | 
            +
                    """
         | 
| 126 | 
            +
                    if not self._features.get('show_experimental_warnings', True):
         | 
| 127 | 
            +
                        return False
         | 
| 128 | 
            +
                    
         | 
| 129 | 
            +
                    # Check if user has already accepted this feature
         | 
| 130 | 
            +
                    accepted_file = Path.home() / '.claude-mpm' / '.experimental_accepted'
         | 
| 131 | 
            +
                    if accepted_file.exists():
         | 
| 132 | 
            +
                        try:
         | 
| 133 | 
            +
                            with open(accepted_file, 'r') as f:
         | 
| 134 | 
            +
                                accepted = json.load(f)
         | 
| 135 | 
            +
                                if feature in accepted.get('features', []):
         | 
| 136 | 
            +
                                    return False
         | 
| 137 | 
            +
                        except Exception:
         | 
| 138 | 
            +
                            pass
         | 
| 139 | 
            +
                    
         | 
| 140 | 
            +
                    return True
         | 
| 141 | 
            +
                
         | 
| 142 | 
            +
                def mark_accepted(self, feature: str):
         | 
| 143 | 
            +
                    """Mark a feature as accepted by the user.
         | 
| 144 | 
            +
                    
         | 
| 145 | 
            +
                    WHY: Once a user accepts the experimental status, we don't need to
         | 
| 146 | 
            +
                    warn them every time they use the feature.
         | 
| 147 | 
            +
                    
         | 
| 148 | 
            +
                    Args:
         | 
| 149 | 
            +
                        feature: Feature name to mark as accepted
         | 
| 150 | 
            +
                    """
         | 
| 151 | 
            +
                    accepted_file = Path.home() / '.claude-mpm' / '.experimental_accepted'
         | 
| 152 | 
            +
                    accepted_file.parent.mkdir(parents=True, exist_ok=True)
         | 
| 153 | 
            +
                    
         | 
| 154 | 
            +
                    try:
         | 
| 155 | 
            +
                        if accepted_file.exists():
         | 
| 156 | 
            +
                            with open(accepted_file, 'r') as f:
         | 
| 157 | 
            +
                                data = json.load(f)
         | 
| 158 | 
            +
                        else:
         | 
| 159 | 
            +
                            data = {'features': [], 'timestamp': {}}
         | 
| 160 | 
            +
                        
         | 
| 161 | 
            +
                        if feature not in data['features']:
         | 
| 162 | 
            +
                            data['features'].append(feature)
         | 
| 163 | 
            +
                            data['timestamp'][feature] = os.environ.get('CLAUDE_MPM_TIMESTAMP', 
         | 
| 164 | 
            +
                                                                       str(Path.cwd()))
         | 
| 165 | 
            +
                        
         | 
| 166 | 
            +
                        with open(accepted_file, 'w') as f:
         | 
| 167 | 
            +
                            json.dump(data, f, indent=2)
         | 
| 168 | 
            +
                    except Exception:
         | 
| 169 | 
            +
                        # Silently ignore errors in acceptance tracking
         | 
| 170 | 
            +
                        pass
         | 
| 171 | 
            +
                
         | 
| 172 | 
            +
                def requires_acceptance(self) -> bool:
         | 
| 173 | 
            +
                    """Check if experimental features require explicit acceptance.
         | 
| 174 | 
            +
                    
         | 
| 175 | 
            +
                    Returns:
         | 
| 176 | 
            +
                        True if acceptance is required
         | 
| 177 | 
            +
                    """
         | 
| 178 | 
            +
                    return self._features.get('require_experimental_acceptance', True)
         | 
| 179 | 
            +
                
         | 
| 180 | 
            +
                def get_all_features(self) -> Dict[str, bool]:
         | 
| 181 | 
            +
                    """Get all feature flags and their current values.
         | 
| 182 | 
            +
                    
         | 
| 183 | 
            +
                    Returns:
         | 
| 184 | 
            +
                        Dictionary of feature flags and their values
         | 
| 185 | 
            +
                    """
         | 
| 186 | 
            +
                    return self._features.copy()
         | 
| 187 | 
            +
             | 
| 188 | 
            +
             | 
| 189 | 
            +
            # Global instance for easy access
         | 
| 190 | 
            +
            _experimental_features = None
         | 
| 191 | 
            +
             | 
| 192 | 
            +
             | 
| 193 | 
            +
            def get_experimental_features(config_file: Optional[Path] = None) -> ExperimentalFeatures:
         | 
| 194 | 
            +
                """Get the global experimental features instance.
         | 
| 195 | 
            +
                
         | 
| 196 | 
            +
                WHY: Provide a singleton-like access pattern to experimental features
         | 
| 197 | 
            +
                configuration to ensure consistency across the application.
         | 
| 198 | 
            +
                
         | 
| 199 | 
            +
                Args:
         | 
| 200 | 
            +
                    config_file: Optional configuration file path
         | 
| 201 | 
            +
                    
         | 
| 202 | 
            +
                Returns:
         | 
| 203 | 
            +
                    ExperimentalFeatures instance
         | 
| 204 | 
            +
                """
         | 
| 205 | 
            +
                global _experimental_features
         | 
| 206 | 
            +
                if _experimental_features is None:
         | 
| 207 | 
            +
                    # Check for config file in standard locations
         | 
| 208 | 
            +
                    if config_file is None:
         | 
| 209 | 
            +
                        for path in [
         | 
| 210 | 
            +
                            Path.cwd() / '.claude-mpm' / 'experimental.json',
         | 
| 211 | 
            +
                            Path.home() / '.claude-mpm' / 'experimental.json',
         | 
| 212 | 
            +
                        ]:
         | 
| 213 | 
            +
                            if path.exists():
         | 
| 214 | 
            +
                                config_file = path
         | 
| 215 | 
            +
                                break
         | 
| 216 | 
            +
                    
         | 
| 217 | 
            +
                    _experimental_features = ExperimentalFeatures(config_file)
         | 
| 218 | 
            +
                
         | 
| 219 | 
            +
                return _experimental_features
         | 
| @@ -0,0 +1,335 @@ | |
| 1 | 
            +
            """YAML configuration support for Memory Guardian.
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            This module provides YAML configuration loading and validation for
         | 
| 4 | 
            +
            the Memory Guardian service, allowing users to configure memory monitoring
         | 
| 5 | 
            +
            through configuration files.
         | 
| 6 | 
            +
            """
         | 
| 7 | 
            +
             | 
| 8 | 
            +
            import os
         | 
| 9 | 
            +
            from pathlib import Path
         | 
| 10 | 
            +
            from typing import Optional, Dict, Any
         | 
| 11 | 
            +
             | 
| 12 | 
            +
            import yaml
         | 
| 13 | 
            +
             | 
| 14 | 
            +
            from claude_mpm.config.memory_guardian_config import (
         | 
| 15 | 
            +
                MemoryGuardianConfig,
         | 
| 16 | 
            +
                MemoryThresholds,
         | 
| 17 | 
            +
                RestartPolicy,
         | 
| 18 | 
            +
                MonitoringConfig
         | 
| 19 | 
            +
            )
         | 
| 20 | 
            +
            from claude_mpm.core.logging_config import get_logger
         | 
| 21 | 
            +
             | 
| 22 | 
            +
             | 
| 23 | 
            +
            logger = get_logger(__name__)
         | 
| 24 | 
            +
             | 
| 25 | 
            +
             | 
| 26 | 
            +
            # Default configuration file locations
         | 
| 27 | 
            +
            DEFAULT_CONFIG_LOCATIONS = [
         | 
| 28 | 
            +
                Path.home() / ".claude-mpm" / "config" / "memory_guardian.yaml",
         | 
| 29 | 
            +
                Path.home() / ".claude-mpm" / "memory_guardian.yaml",
         | 
| 30 | 
            +
                Path.cwd() / ".claude-mpm" / "memory_guardian.yaml",
         | 
| 31 | 
            +
                Path.cwd() / "memory_guardian.yaml",
         | 
| 32 | 
            +
            ]
         | 
| 33 | 
            +
             | 
| 34 | 
            +
             | 
| 35 | 
            +
            def find_config_file() -> Optional[Path]:
         | 
| 36 | 
            +
                """Find memory guardian configuration file in standard locations.
         | 
| 37 | 
            +
                
         | 
| 38 | 
            +
                Returns:
         | 
| 39 | 
            +
                    Path to configuration file or None if not found
         | 
| 40 | 
            +
                """
         | 
| 41 | 
            +
                for path in DEFAULT_CONFIG_LOCATIONS:
         | 
| 42 | 
            +
                    if path.exists():
         | 
| 43 | 
            +
                        logger.debug(f"Found memory guardian config at: {path}")
         | 
| 44 | 
            +
                        return path
         | 
| 45 | 
            +
                
         | 
| 46 | 
            +
                return None
         | 
| 47 | 
            +
             | 
| 48 | 
            +
             | 
| 49 | 
            +
            def load_yaml_config(config_path: Path) -> Optional[Dict[str, Any]]:
         | 
| 50 | 
            +
                """Load YAML configuration from file.
         | 
| 51 | 
            +
                
         | 
| 52 | 
            +
                Args:
         | 
| 53 | 
            +
                    config_path: Path to YAML configuration file
         | 
| 54 | 
            +
                    
         | 
| 55 | 
            +
                Returns:
         | 
| 56 | 
            +
                    Dictionary of configuration data or None if loading failed
         | 
| 57 | 
            +
                """
         | 
| 58 | 
            +
                try:
         | 
| 59 | 
            +
                    if not config_path.exists():
         | 
| 60 | 
            +
                        logger.warning(f"Configuration file not found: {config_path}")
         | 
| 61 | 
            +
                        return None
         | 
| 62 | 
            +
                    
         | 
| 63 | 
            +
                    with open(config_path, 'r') as f:
         | 
| 64 | 
            +
                        config_data = yaml.safe_load(f)
         | 
| 65 | 
            +
                    
         | 
| 66 | 
            +
                    if not config_data:
         | 
| 67 | 
            +
                        logger.warning(f"Empty configuration file: {config_path}")
         | 
| 68 | 
            +
                        return {}
         | 
| 69 | 
            +
                    
         | 
| 70 | 
            +
                    logger.info(f"Loaded configuration from: {config_path}")
         | 
| 71 | 
            +
                    return config_data
         | 
| 72 | 
            +
                    
         | 
| 73 | 
            +
                except yaml.YAMLError as e:
         | 
| 74 | 
            +
                    logger.error(f"Invalid YAML in configuration file: {e}")
         | 
| 75 | 
            +
                    return None
         | 
| 76 | 
            +
                except Exception as e:
         | 
| 77 | 
            +
                    logger.error(f"Failed to load configuration file: {e}")
         | 
| 78 | 
            +
                    return None
         | 
| 79 | 
            +
             | 
| 80 | 
            +
             | 
| 81 | 
            +
            def create_config_from_yaml(yaml_data: Dict[str, Any]) -> MemoryGuardianConfig:
         | 
| 82 | 
            +
                """Create MemoryGuardianConfig from YAML data.
         | 
| 83 | 
            +
                
         | 
| 84 | 
            +
                Args:
         | 
| 85 | 
            +
                    yaml_data: Dictionary of configuration data from YAML
         | 
| 86 | 
            +
                    
         | 
| 87 | 
            +
                Returns:
         | 
| 88 | 
            +
                    MemoryGuardianConfig instance
         | 
| 89 | 
            +
                """
         | 
| 90 | 
            +
                config = MemoryGuardianConfig()
         | 
| 91 | 
            +
                
         | 
| 92 | 
            +
                # General settings
         | 
| 93 | 
            +
                config.enabled = yaml_data.get('enabled', config.enabled)
         | 
| 94 | 
            +
                config.auto_start = yaml_data.get('auto_start', config.auto_start)
         | 
| 95 | 
            +
                config.persist_state = yaml_data.get('persist_state', config.persist_state)
         | 
| 96 | 
            +
                config.state_file = yaml_data.get('state_file', config.state_file)
         | 
| 97 | 
            +
                
         | 
| 98 | 
            +
                # Memory thresholds
         | 
| 99 | 
            +
                if 'thresholds' in yaml_data:
         | 
| 100 | 
            +
                    thresholds = yaml_data['thresholds']
         | 
| 101 | 
            +
                    config.thresholds = MemoryThresholds(
         | 
| 102 | 
            +
                        warning=thresholds.get('warning', config.thresholds.warning),
         | 
| 103 | 
            +
                        critical=thresholds.get('critical', config.thresholds.critical),
         | 
| 104 | 
            +
                        emergency=thresholds.get('emergency', config.thresholds.emergency)
         | 
| 105 | 
            +
                    )
         | 
| 106 | 
            +
                
         | 
| 107 | 
            +
                # Monitoring configuration
         | 
| 108 | 
            +
                if 'monitoring' in yaml_data:
         | 
| 109 | 
            +
                    monitoring = yaml_data['monitoring']
         | 
| 110 | 
            +
                    config.monitoring = MonitoringConfig(
         | 
| 111 | 
            +
                        check_interval=monitoring.get('check_interval', config.monitoring.check_interval),
         | 
| 112 | 
            +
                        check_interval_warning=monitoring.get('check_interval_warning', config.monitoring.check_interval_warning),
         | 
| 113 | 
            +
                        check_interval_critical=monitoring.get('check_interval_critical', config.monitoring.check_interval_critical),
         | 
| 114 | 
            +
                        log_memory_stats=monitoring.get('log_memory_stats', config.monitoring.log_memory_stats),
         | 
| 115 | 
            +
                        log_interval=monitoring.get('log_interval', config.monitoring.log_interval)
         | 
| 116 | 
            +
                    )
         | 
| 117 | 
            +
                
         | 
| 118 | 
            +
                # Restart policy
         | 
| 119 | 
            +
                if 'restart_policy' in yaml_data:
         | 
| 120 | 
            +
                    policy = yaml_data['restart_policy']
         | 
| 121 | 
            +
                    config.restart_policy = RestartPolicy(
         | 
| 122 | 
            +
                        max_attempts=policy.get('max_attempts', config.restart_policy.max_attempts),
         | 
| 123 | 
            +
                        attempt_window=policy.get('attempt_window', config.restart_policy.attempt_window),
         | 
| 124 | 
            +
                        cooldown_base=policy.get('cooldown_base', config.restart_policy.cooldown_base),
         | 
| 125 | 
            +
                        cooldown_multiplier=policy.get('cooldown_multiplier', config.restart_policy.cooldown_multiplier),
         | 
| 126 | 
            +
                        cooldown_max=policy.get('cooldown_max', config.restart_policy.cooldown_max),
         | 
| 127 | 
            +
                        graceful_timeout=policy.get('graceful_timeout', config.restart_policy.graceful_timeout),
         | 
| 128 | 
            +
                        force_kill_timeout=policy.get('force_kill_timeout', config.restart_policy.force_kill_timeout)
         | 
| 129 | 
            +
                    )
         | 
| 130 | 
            +
                
         | 
| 131 | 
            +
                # Process configuration
         | 
| 132 | 
            +
                if 'process' in yaml_data:
         | 
| 133 | 
            +
                    process = yaml_data['process']
         | 
| 134 | 
            +
                    config.process_command = process.get('command', config.process_command)
         | 
| 135 | 
            +
                    config.process_args = process.get('args', config.process_args)
         | 
| 136 | 
            +
                    config.process_env = process.get('env', config.process_env)
         | 
| 137 | 
            +
                    config.working_directory = process.get('working_directory', config.working_directory)
         | 
| 138 | 
            +
                
         | 
| 139 | 
            +
                return config
         | 
| 140 | 
            +
             | 
| 141 | 
            +
             | 
| 142 | 
            +
            def load_config(config_path: Optional[Path] = None) -> Optional[MemoryGuardianConfig]:
         | 
| 143 | 
            +
                """Load memory guardian configuration from YAML file.
         | 
| 144 | 
            +
                
         | 
| 145 | 
            +
                Args:
         | 
| 146 | 
            +
                    config_path: Optional path to configuration file.
         | 
| 147 | 
            +
                                If not provided, searches standard locations.
         | 
| 148 | 
            +
                
         | 
| 149 | 
            +
                Returns:
         | 
| 150 | 
            +
                    MemoryGuardianConfig instance or None if loading failed
         | 
| 151 | 
            +
                """
         | 
| 152 | 
            +
                # Find configuration file if not specified
         | 
| 153 | 
            +
                if config_path is None:
         | 
| 154 | 
            +
                    config_path = find_config_file()
         | 
| 155 | 
            +
                    if config_path is None:
         | 
| 156 | 
            +
                        logger.debug("No memory guardian configuration file found")
         | 
| 157 | 
            +
                        return None
         | 
| 158 | 
            +
                
         | 
| 159 | 
            +
                # Load YAML data
         | 
| 160 | 
            +
                yaml_data = load_yaml_config(config_path)
         | 
| 161 | 
            +
                if yaml_data is None:
         | 
| 162 | 
            +
                    return None
         | 
| 163 | 
            +
                
         | 
| 164 | 
            +
                # Create configuration
         | 
| 165 | 
            +
                try:
         | 
| 166 | 
            +
                    config = create_config_from_yaml(yaml_data)
         | 
| 167 | 
            +
                    
         | 
| 168 | 
            +
                    # Validate configuration
         | 
| 169 | 
            +
                    issues = config.validate()
         | 
| 170 | 
            +
                    if issues:
         | 
| 171 | 
            +
                        for issue in issues:
         | 
| 172 | 
            +
                            logger.warning(f"Configuration validation issue: {issue}")
         | 
| 173 | 
            +
                    
         | 
| 174 | 
            +
                    return config
         | 
| 175 | 
            +
                    
         | 
| 176 | 
            +
                except Exception as e:
         | 
| 177 | 
            +
                    logger.error(f"Failed to create configuration from YAML: {e}")
         | 
| 178 | 
            +
                    return None
         | 
| 179 | 
            +
             | 
| 180 | 
            +
             | 
| 181 | 
            +
            def save_config(config: MemoryGuardianConfig, config_path: Optional[Path] = None) -> bool:
         | 
| 182 | 
            +
                """Save memory guardian configuration to YAML file.
         | 
| 183 | 
            +
                
         | 
| 184 | 
            +
                Args:
         | 
| 185 | 
            +
                    config: MemoryGuardianConfig instance to save
         | 
| 186 | 
            +
                    config_path: Optional path to save configuration.
         | 
| 187 | 
            +
                                If not provided, uses default location.
         | 
| 188 | 
            +
                
         | 
| 189 | 
            +
                Returns:
         | 
| 190 | 
            +
                    True if save successful, False otherwise
         | 
| 191 | 
            +
                """
         | 
| 192 | 
            +
                try:
         | 
| 193 | 
            +
                    # Determine save path
         | 
| 194 | 
            +
                    if config_path is None:
         | 
| 195 | 
            +
                        config_path = Path.home() / ".claude-mpm" / "config" / "memory_guardian.yaml"
         | 
| 196 | 
            +
                    
         | 
| 197 | 
            +
                    # Ensure directory exists
         | 
| 198 | 
            +
                    config_path.parent.mkdir(parents=True, exist_ok=True)
         | 
| 199 | 
            +
                    
         | 
| 200 | 
            +
                    # Convert configuration to dictionary
         | 
| 201 | 
            +
                    config_dict = config.to_dict()
         | 
| 202 | 
            +
                    
         | 
| 203 | 
            +
                    # Write to file
         | 
| 204 | 
            +
                    with open(config_path, 'w') as f:
         | 
| 205 | 
            +
                        yaml.dump(config_dict, f, default_flow_style=False, sort_keys=False)
         | 
| 206 | 
            +
                    
         | 
| 207 | 
            +
                    logger.info(f"Saved configuration to: {config_path}")
         | 
| 208 | 
            +
                    return True
         | 
| 209 | 
            +
                    
         | 
| 210 | 
            +
                except Exception as e:
         | 
| 211 | 
            +
                    logger.error(f"Failed to save configuration: {e}")
         | 
| 212 | 
            +
                    return False
         | 
| 213 | 
            +
             | 
| 214 | 
            +
             | 
| 215 | 
            +
            def create_default_config_file(config_path: Optional[Path] = None) -> bool:
         | 
| 216 | 
            +
                """Create a default memory guardian configuration file.
         | 
| 217 | 
            +
                
         | 
| 218 | 
            +
                Args:
         | 
| 219 | 
            +
                    config_path: Optional path for configuration file.
         | 
| 220 | 
            +
                                If not provided, uses default location.
         | 
| 221 | 
            +
                
         | 
| 222 | 
            +
                Returns:
         | 
| 223 | 
            +
                    True if file created successfully, False otherwise
         | 
| 224 | 
            +
                """
         | 
| 225 | 
            +
                try:
         | 
| 226 | 
            +
                    # Determine save path
         | 
| 227 | 
            +
                    if config_path is None:
         | 
| 228 | 
            +
                        config_path = Path.home() / ".claude-mpm" / "config" / "memory_guardian.yaml"
         | 
| 229 | 
            +
                    
         | 
| 230 | 
            +
                    # Check if file already exists
         | 
| 231 | 
            +
                    if config_path.exists():
         | 
| 232 | 
            +
                        logger.warning(f"Configuration file already exists: {config_path}")
         | 
| 233 | 
            +
                        return False
         | 
| 234 | 
            +
                    
         | 
| 235 | 
            +
                    # Create default configuration
         | 
| 236 | 
            +
                    config = MemoryGuardianConfig()
         | 
| 237 | 
            +
                    
         | 
| 238 | 
            +
                    # Add helpful comments to YAML
         | 
| 239 | 
            +
                    yaml_content = """# Memory Guardian Configuration
         | 
| 240 | 
            +
            # This file configures memory monitoring and automatic restart for Claude Code
         | 
| 241 | 
            +
             | 
| 242 | 
            +
            # Enable/disable memory monitoring
         | 
| 243 | 
            +
            enabled: true
         | 
| 244 | 
            +
             | 
| 245 | 
            +
            # Automatically start monitoring when service initializes
         | 
| 246 | 
            +
            auto_start: true
         | 
| 247 | 
            +
             | 
| 248 | 
            +
            # Preserve state across restarts
         | 
| 249 | 
            +
            persist_state: true
         | 
| 250 | 
            +
             | 
| 251 | 
            +
            # Path to state file for persistence
         | 
| 252 | 
            +
            state_file: ~/.claude-mpm/state/memory_guardian.json
         | 
| 253 | 
            +
             | 
| 254 | 
            +
            # Memory thresholds in MB
         | 
| 255 | 
            +
            thresholds:
         | 
| 256 | 
            +
              # Warning threshold - logs warnings
         | 
| 257 | 
            +
              warning: 14400  # 14GB
         | 
| 258 | 
            +
              
         | 
| 259 | 
            +
              # Critical threshold - triggers restart
         | 
| 260 | 
            +
              critical: 18000  # 18GB
         | 
| 261 | 
            +
              
         | 
| 262 | 
            +
              # Emergency threshold - immediate restart
         | 
| 263 | 
            +
              emergency: 21600  # 21GB
         | 
| 264 | 
            +
             | 
| 265 | 
            +
            # Monitoring configuration
         | 
| 266 | 
            +
            monitoring:
         | 
| 267 | 
            +
              # Default check interval in seconds
         | 
| 268 | 
            +
              check_interval: 30
         | 
| 269 | 
            +
              
         | 
| 270 | 
            +
              # Check interval when in warning state
         | 
| 271 | 
            +
              check_interval_warning: 15
         | 
| 272 | 
            +
              
         | 
| 273 | 
            +
              # Check interval when in critical state
         | 
| 274 | 
            +
              check_interval_critical: 5
         | 
| 275 | 
            +
              
         | 
| 276 | 
            +
              # Enable periodic memory statistics logging
         | 
| 277 | 
            +
              log_memory_stats: true
         | 
| 278 | 
            +
              
         | 
| 279 | 
            +
              # Statistics logging interval in seconds
         | 
| 280 | 
            +
              log_interval: 60
         | 
| 281 | 
            +
             | 
| 282 | 
            +
            # Restart policy configuration
         | 
| 283 | 
            +
            restart_policy:
         | 
| 284 | 
            +
              # Maximum restart attempts (0 = unlimited)
         | 
| 285 | 
            +
              max_attempts: 3
         | 
| 286 | 
            +
              
         | 
| 287 | 
            +
              # Time window for counting attempts (seconds)
         | 
| 288 | 
            +
              attempt_window: 3600  # 1 hour
         | 
| 289 | 
            +
              
         | 
| 290 | 
            +
              # Base cooldown between restarts (seconds)
         | 
| 291 | 
            +
              cooldown_base: 10
         | 
| 292 | 
            +
              
         | 
| 293 | 
            +
              # Cooldown multiplier for consecutive failures
         | 
| 294 | 
            +
              cooldown_multiplier: 2.0
         | 
| 295 | 
            +
              
         | 
| 296 | 
            +
              # Maximum cooldown period (seconds)
         | 
| 297 | 
            +
              cooldown_max: 300  # 5 minutes
         | 
| 298 | 
            +
              
         | 
| 299 | 
            +
              # Timeout for graceful shutdown (seconds)
         | 
| 300 | 
            +
              graceful_timeout: 30
         | 
| 301 | 
            +
              
         | 
| 302 | 
            +
              # Timeout for force kill if graceful fails (seconds)
         | 
| 303 | 
            +
              force_kill_timeout: 10
         | 
| 304 | 
            +
             | 
| 305 | 
            +
            # Process configuration (usually set by runner)
         | 
| 306 | 
            +
            process:
         | 
| 307 | 
            +
              # Command to execute (set by runner)
         | 
| 308 | 
            +
              command: ["claude"]
         | 
| 309 | 
            +
              
         | 
| 310 | 
            +
              # Additional arguments
         | 
| 311 | 
            +
              args: []
         | 
| 312 | 
            +
              
         | 
| 313 | 
            +
              # Environment variables
         | 
| 314 | 
            +
              env: {}
         | 
| 315 | 
            +
              
         | 
| 316 | 
            +
              # Working directory (defaults to current)
         | 
| 317 | 
            +
              working_directory: null
         | 
| 318 | 
            +
            """
         | 
| 319 | 
            +
                    
         | 
| 320 | 
            +
                    # Ensure directory exists
         | 
| 321 | 
            +
                    config_path.parent.mkdir(parents=True, exist_ok=True)
         | 
| 322 | 
            +
                    
         | 
| 323 | 
            +
                    # Write configuration file
         | 
| 324 | 
            +
                    with open(config_path, 'w') as f:
         | 
| 325 | 
            +
                        f.write(yaml_content)
         | 
| 326 | 
            +
                    
         | 
| 327 | 
            +
                    logger.info(f"Created default configuration file: {config_path}")
         | 
| 328 | 
            +
                    print(f"✓ Created memory guardian configuration at: {config_path}")
         | 
| 329 | 
            +
                    print("  Edit this file to customize memory thresholds and policies")
         | 
| 330 | 
            +
                    
         | 
| 331 | 
            +
                    return True
         | 
| 332 | 
            +
                    
         | 
| 333 | 
            +
                except Exception as e:
         | 
| 334 | 
            +
                    logger.error(f"Failed to create default configuration file: {e}")
         | 
| 335 | 
            +
                    return False
         |