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.
Files changed (38) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/templates/memory_manager.json +155 -0
  3. claude_mpm/cli/__init__.py +15 -2
  4. claude_mpm/cli/commands/__init__.py +3 -0
  5. claude_mpm/cli/commands/mcp.py +280 -134
  6. claude_mpm/cli/commands/run_guarded.py +511 -0
  7. claude_mpm/cli/parser.py +8 -2
  8. claude_mpm/config/experimental_features.py +219 -0
  9. claude_mpm/config/memory_guardian_yaml.py +335 -0
  10. claude_mpm/constants.py +1 -0
  11. claude_mpm/core/memory_aware_runner.py +353 -0
  12. claude_mpm/services/infrastructure/context_preservation.py +537 -0
  13. claude_mpm/services/infrastructure/graceful_degradation.py +616 -0
  14. claude_mpm/services/infrastructure/health_monitor.py +775 -0
  15. claude_mpm/services/infrastructure/memory_dashboard.py +479 -0
  16. claude_mpm/services/infrastructure/memory_guardian.py +189 -15
  17. claude_mpm/services/infrastructure/restart_protection.py +642 -0
  18. claude_mpm/services/infrastructure/state_manager.py +774 -0
  19. claude_mpm/services/mcp_gateway/__init__.py +11 -11
  20. claude_mpm/services/mcp_gateway/core/__init__.py +2 -2
  21. claude_mpm/services/mcp_gateway/core/interfaces.py +10 -9
  22. claude_mpm/services/mcp_gateway/main.py +35 -5
  23. claude_mpm/services/mcp_gateway/manager.py +334 -0
  24. claude_mpm/services/mcp_gateway/registry/service_registry.py +4 -8
  25. claude_mpm/services/mcp_gateway/server/__init__.py +2 -2
  26. claude_mpm/services/mcp_gateway/server/{mcp_server.py → mcp_gateway.py} +60 -59
  27. claude_mpm/services/mcp_gateway/tools/base_adapter.py +1 -2
  28. claude_mpm/services/ticket_manager.py +8 -8
  29. claude_mpm/services/ticket_manager_di.py +5 -5
  30. claude_mpm/storage/__init__.py +9 -0
  31. claude_mpm/storage/state_storage.py +556 -0
  32. {claude_mpm-3.9.9.dist-info → claude_mpm-3.9.11.dist-info}/METADATA +25 -2
  33. {claude_mpm-3.9.9.dist-info → claude_mpm-3.9.11.dist-info}/RECORD +37 -24
  34. claude_mpm/services/mcp_gateway/server/mcp_server_simple.py +0 -444
  35. {claude_mpm-3.9.9.dist-info → claude_mpm-3.9.11.dist-info}/WHEEL +0 -0
  36. {claude_mpm-3.9.9.dist-info → claude_mpm-3.9.11.dist-info}/entry_points.txt +0 -0
  37. {claude_mpm-3.9.9.dist-info → claude_mpm-3.9.11.dist-info}/licenses/LICENSE +0 -0
  38. {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
claude_mpm/constants.py CHANGED
@@ -23,6 +23,7 @@ class CLIPrefix(str, Enum):
23
23
  class CLICommands(str, Enum):
24
24
  """CLI command constants."""
25
25
  RUN = "run"
26
+ RUN_GUARDED = "run-guarded"
26
27
  TICKETS = "tickets"
27
28
  INFO = "info"
28
29
  AGENTS = "agents"