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,511 @@
1
+ """Run-guarded command implementation for memory-aware Claude execution.
2
+
3
+ WHY: This experimental command provides memory monitoring and automatic restart
4
+ capabilities for Claude Code sessions, preventing memory-related crashes.
5
+
6
+ DESIGN DECISION: This is kept completely separate from the main run command to
7
+ ensure stability. It extends ClaudeRunner through MemoryAwareClaudeRunner without
8
+ modifying the base implementation.
9
+
10
+ STATUS: EXPERIMENTAL - This feature is in beta and may change or have issues.
11
+ """
12
+
13
+ import argparse
14
+ import asyncio
15
+ import os
16
+ import sys
17
+ from pathlib import Path
18
+ from typing import Optional, Dict, Any
19
+
20
+ from claude_mpm.core.memory_aware_runner import MemoryAwareClaudeRunner
21
+ from claude_mpm.core.logging_config import get_logger
22
+ from claude_mpm.cli.utils import setup_logging
23
+ from claude_mpm.config.experimental_features import get_experimental_features
24
+ from claude_mpm.config.memory_guardian_config import (
25
+ MemoryGuardianConfig,
26
+ MemoryThresholds,
27
+ RestartPolicy,
28
+ MonitoringConfig,
29
+ get_default_config
30
+ )
31
+ from claude_mpm.constants import LogLevel
32
+
33
+
34
+ logger = get_logger(__name__)
35
+
36
+
37
+ def add_run_guarded_parser(subparsers) -> argparse.ArgumentParser:
38
+ """Add run-guarded command parser.
39
+
40
+ Args:
41
+ subparsers: Subparsers object from main parser
42
+
43
+ Returns:
44
+ The run-guarded parser for further configuration
45
+ """
46
+ parser = subparsers.add_parser(
47
+ 'run-guarded',
48
+ help='(EXPERIMENTAL) Run Claude with memory monitoring and automatic restart',
49
+ description=(
50
+ '⚠️ EXPERIMENTAL FEATURE\n\n'
51
+ 'Run Claude Code with memory monitoring and automatic restart capabilities. '
52
+ 'This command monitors memory usage and performs controlled restarts when '
53
+ 'thresholds are exceeded, preserving conversation state across restarts.\n\n'
54
+ 'NOTE: This is a beta feature. Use with caution in production environments.'
55
+ ),
56
+ formatter_class=argparse.RawDescriptionHelpFormatter,
57
+ epilog="""
58
+ Examples:
59
+ # Run with default settings (18GB threshold, 30s checks)
60
+ claude-mpm run-guarded
61
+
62
+ # Custom memory threshold (in MB)
63
+ claude-mpm run-guarded --memory-threshold 16000
64
+
65
+ # Faster monitoring for development
66
+ claude-mpm run-guarded --check-interval 10
67
+
68
+ # Limit restart attempts
69
+ claude-mpm run-guarded --max-restarts 5
70
+
71
+ # Disable state preservation (faster restarts)
72
+ claude-mpm run-guarded --no-state-preservation
73
+
74
+ # Use configuration file
75
+ claude-mpm run-guarded --config ~/.claude-mpm/memory-guardian.yaml
76
+
77
+ Memory thresholds:
78
+ - Warning: 80% of threshold (logs warning)
79
+ - Critical: 100% of threshold (triggers restart)
80
+ - Emergency: 120% of threshold (immediate restart)
81
+
82
+ State preservation:
83
+ When enabled, the runner captures and restores:
84
+ - Current conversation context
85
+ - Open files and working directory
86
+ - Environment variables
87
+ - Recent command history
88
+ """
89
+ )
90
+
91
+ # Memory monitoring options
92
+ memory_group = parser.add_argument_group('memory monitoring')
93
+ memory_group.add_argument(
94
+ '--memory-threshold',
95
+ type=float,
96
+ default=18000, # 18GB in MB
97
+ help='Memory threshold in MB before restart (default: 18000MB/18GB)'
98
+ )
99
+ memory_group.add_argument(
100
+ '--check-interval',
101
+ type=int,
102
+ default=30,
103
+ help='Memory check interval in seconds (default: 30)'
104
+ )
105
+ memory_group.add_argument(
106
+ '--warning-threshold',
107
+ type=float,
108
+ help='Warning threshold in MB (default: 80%% of memory threshold)'
109
+ )
110
+ memory_group.add_argument(
111
+ '--emergency-threshold',
112
+ type=float,
113
+ help='Emergency threshold in MB (default: 120%% of memory threshold)'
114
+ )
115
+
116
+ # Restart policy options
117
+ restart_group = parser.add_argument_group('restart policy')
118
+ restart_group.add_argument(
119
+ '--max-restarts',
120
+ type=int,
121
+ default=3,
122
+ help='Maximum number of automatic restarts (default: 3)'
123
+ )
124
+ restart_group.add_argument(
125
+ '--restart-cooldown',
126
+ type=int,
127
+ default=10,
128
+ help='Cooldown period between restarts in seconds (default: 10)'
129
+ )
130
+ restart_group.add_argument(
131
+ '--graceful-timeout',
132
+ type=int,
133
+ default=30,
134
+ help='Timeout for graceful shutdown in seconds (default: 30)'
135
+ )
136
+
137
+ # State preservation options
138
+ state_group = parser.add_argument_group('state preservation')
139
+ state_group.add_argument(
140
+ '--enable-state-preservation',
141
+ action='store_true',
142
+ default=True,
143
+ dest='state_preservation',
144
+ help='Enable state preservation across restarts (default: enabled)'
145
+ )
146
+ state_group.add_argument(
147
+ '--no-state-preservation',
148
+ action='store_false',
149
+ dest='state_preservation',
150
+ help='Disable state preservation for faster restarts'
151
+ )
152
+ state_group.add_argument(
153
+ '--state-dir',
154
+ type=Path,
155
+ help='Directory for state files (default: ~/.claude-mpm/state)'
156
+ )
157
+
158
+ # Configuration file
159
+ parser.add_argument(
160
+ '--config',
161
+ '--config-file',
162
+ type=Path,
163
+ dest='config_file',
164
+ help='Path to memory guardian configuration file (YAML)'
165
+ )
166
+
167
+ # Monitoring display options
168
+ display_group = parser.add_argument_group('display options')
169
+ display_group.add_argument(
170
+ '--quiet',
171
+ action='store_true',
172
+ help='Minimal output, only show critical events'
173
+ )
174
+ display_group.add_argument(
175
+ '--verbose',
176
+ action='store_true',
177
+ help='Verbose output with detailed monitoring information'
178
+ )
179
+ display_group.add_argument(
180
+ '--show-stats',
181
+ action='store_true',
182
+ help='Display memory statistics periodically'
183
+ )
184
+ display_group.add_argument(
185
+ '--stats-interval',
186
+ type=int,
187
+ default=60,
188
+ help='Statistics display interval in seconds (default: 60)'
189
+ )
190
+
191
+ # Claude runner options (inherited from run command)
192
+ run_group = parser.add_argument_group('claude options')
193
+ run_group.add_argument(
194
+ '--no-hooks',
195
+ action='store_true',
196
+ help='Disable hook service'
197
+ )
198
+ run_group.add_argument(
199
+ '--no-tickets',
200
+ action='store_true',
201
+ help='Disable automatic ticket creation'
202
+ )
203
+ run_group.add_argument(
204
+ '--no-native-agents',
205
+ action='store_true',
206
+ help='Disable deployment of Claude Code native agents'
207
+ )
208
+ run_group.add_argument(
209
+ '--websocket-port',
210
+ type=int,
211
+ default=8765,
212
+ help='WebSocket server port (default: 8765)'
213
+ )
214
+
215
+ # Input/output options
216
+ io_group = parser.add_argument_group('input/output')
217
+ io_group.add_argument(
218
+ '-i', '--input',
219
+ type=str,
220
+ help='Input text or file path for initial context'
221
+ )
222
+ io_group.add_argument(
223
+ '--non-interactive',
224
+ action='store_true',
225
+ help='Run in non-interactive mode'
226
+ )
227
+
228
+ # Logging options
229
+ logging_group = parser.add_argument_group('logging')
230
+ logging_group.add_argument(
231
+ '--logging',
232
+ choices=[level.value for level in LogLevel],
233
+ default=LogLevel.INFO.value,
234
+ help='Logging level (default: INFO)'
235
+ )
236
+ logging_group.add_argument(
237
+ '--log-dir',
238
+ type=Path,
239
+ help='Custom log directory'
240
+ )
241
+
242
+ # Experimental feature control
243
+ experimental_group = parser.add_argument_group('experimental control')
244
+ experimental_group.add_argument(
245
+ '--accept-experimental',
246
+ action='store_true',
247
+ help='Accept experimental status and suppress warning'
248
+ )
249
+ experimental_group.add_argument(
250
+ '--force-experimental',
251
+ action='store_true',
252
+ help='Force run even if experimental features are disabled'
253
+ )
254
+
255
+ # Claude CLI arguments
256
+ parser.add_argument(
257
+ 'claude_args',
258
+ nargs=argparse.REMAINDER,
259
+ help='Additional arguments to pass to Claude CLI'
260
+ )
261
+
262
+ return parser
263
+
264
+
265
+ def load_config_file(config_path: Path) -> Optional[MemoryGuardianConfig]:
266
+ """Load memory guardian configuration from YAML file.
267
+
268
+ Args:
269
+ config_path: Path to configuration file
270
+
271
+ Returns:
272
+ MemoryGuardianConfig or None if loading failed
273
+ """
274
+ try:
275
+ import yaml
276
+
277
+ if not config_path.exists():
278
+ logger.warning(f"Configuration file not found: {config_path}")
279
+ return None
280
+
281
+ with open(config_path, 'r') as f:
282
+ config_data = yaml.safe_load(f)
283
+
284
+ # Create configuration from YAML data
285
+ config = MemoryGuardianConfig()
286
+
287
+ # Update thresholds
288
+ if 'thresholds' in config_data:
289
+ t = config_data['thresholds']
290
+ config.thresholds = MemoryThresholds(
291
+ warning=t.get('warning', config.thresholds.warning),
292
+ critical=t.get('critical', config.thresholds.critical),
293
+ emergency=t.get('emergency', config.thresholds.emergency)
294
+ )
295
+
296
+ # Update monitoring settings
297
+ if 'monitoring' in config_data:
298
+ m = config_data['monitoring']
299
+ config.monitoring = MonitoringConfig(
300
+ normal_interval=m.get('normal_interval', config.monitoring.normal_interval),
301
+ warning_interval=m.get('warning_interval', config.monitoring.warning_interval),
302
+ critical_interval=m.get('critical_interval', config.monitoring.critical_interval),
303
+ log_memory_stats=m.get('log_memory_stats', config.monitoring.log_memory_stats),
304
+ log_interval=m.get('log_interval', config.monitoring.log_interval)
305
+ )
306
+
307
+ # Update restart policy
308
+ if 'restart_policy' in config_data:
309
+ r = config_data['restart_policy']
310
+ config.restart_policy = RestartPolicy(
311
+ max_attempts=r.get('max_attempts', config.restart_policy.max_attempts),
312
+ attempt_window=r.get('attempt_window', config.restart_policy.attempt_window),
313
+ initial_cooldown=r.get('initial_cooldown', config.restart_policy.initial_cooldown),
314
+ cooldown_multiplier=r.get('cooldown_multiplier', config.restart_policy.cooldown_multiplier),
315
+ max_cooldown=r.get('max_cooldown', config.restart_policy.max_cooldown),
316
+ graceful_timeout=r.get('graceful_timeout', config.restart_policy.graceful_timeout),
317
+ force_kill_timeout=r.get('force_kill_timeout', config.restart_policy.force_kill_timeout)
318
+ )
319
+
320
+ # Update general settings
321
+ config.enabled = config_data.get('enabled', True)
322
+ config.auto_start = config_data.get('auto_start', True)
323
+ config.persist_state = config_data.get('persist_state', True)
324
+
325
+ logger.info(f"Loaded configuration from {config_path}")
326
+ return config
327
+
328
+ except ImportError:
329
+ logger.error("PyYAML not installed. Install with: pip install pyyaml")
330
+ return None
331
+ except Exception as e:
332
+ logger.error(f"Failed to load configuration file: {e}")
333
+ return None
334
+
335
+
336
+ def execute_run_guarded(args: argparse.Namespace) -> int:
337
+ """Execute the run-guarded command.
338
+
339
+ WHY: This is the entry point for the experimental memory-guarded execution mode.
340
+ It checks experimental feature flags and shows appropriate warnings before
341
+ delegating to MemoryAwareClaudeRunner.
342
+
343
+ DESIGN DECISION: All experimental checks happen here, before any actual work,
344
+ to ensure users are aware they're using beta functionality.
345
+
346
+ Args:
347
+ args: Parsed command line arguments
348
+
349
+ Returns:
350
+ Exit code (0 for success, non-zero for failure)
351
+ """
352
+ try:
353
+ # Setup logging
354
+ log_level = getattr(args, 'logging', 'INFO')
355
+ log_dir = getattr(args, 'log_dir', None)
356
+ setup_logging(log_level, log_dir)
357
+
358
+ # Check experimental features
359
+ experimental = get_experimental_features()
360
+
361
+ # Check if Memory Guardian is enabled
362
+ if not experimental.is_enabled('memory_guardian') and not getattr(args, 'force_experimental', False):
363
+ logger.error("Memory Guardian is an experimental feature that is currently disabled.")
364
+ print("\n❌ Memory Guardian is disabled in experimental features configuration.")
365
+ print("\nTo enable it:")
366
+ print(" 1. Set environment variable: export CLAUDE_MPM_EXPERIMENTAL_ENABLE_MEMORY_GUARDIAN=true")
367
+ print(" 2. Or use --force-experimental flag to override")
368
+ print(" 3. Or enable in ~/.claude-mpm/experimental.json")
369
+ return 1
370
+
371
+ # Show experimental warning unless suppressed
372
+ if not getattr(args, 'accept_experimental', False):
373
+ if experimental.should_show_warning('memory_guardian'):
374
+ warning = experimental.get_warning('memory_guardian')
375
+ if warning:
376
+ print("\n" + warning)
377
+ print("\nThis feature is experimental and may:")
378
+ print(" • Have bugs or stability issues")
379
+ print(" • Change significantly in future versions")
380
+ print(" • Not work as expected in all environments")
381
+ print("\nContinue? [y/N]: ", end="")
382
+
383
+ try:
384
+ response = input().strip().lower()
385
+ if response != 'y':
386
+ print("\n✅ Cancelled. Use the stable 'run' command instead.")
387
+ return 0
388
+ # Mark as accepted for this session
389
+ experimental.mark_accepted('memory_guardian')
390
+ except (EOFError, KeyboardInterrupt):
391
+ print("\n✅ Cancelled.")
392
+ return 0
393
+
394
+ logger.info("Starting experimental run-guarded command")
395
+
396
+ # Load configuration
397
+ config = None
398
+ if hasattr(args, 'config_file') and args.config_file:
399
+ config = load_config_file(args.config_file)
400
+
401
+ if config is None:
402
+ # Create configuration from command line arguments
403
+ config = MemoryGuardianConfig()
404
+
405
+ # Set thresholds
406
+ config.thresholds.critical = args.memory_threshold
407
+
408
+ if hasattr(args, 'warning_threshold') and args.warning_threshold:
409
+ config.thresholds.warning = args.warning_threshold
410
+ else:
411
+ config.thresholds.warning = args.memory_threshold * 0.8
412
+
413
+ if hasattr(args, 'emergency_threshold') and args.emergency_threshold:
414
+ config.thresholds.emergency = args.emergency_threshold
415
+ else:
416
+ config.thresholds.emergency = args.memory_threshold * 1.2
417
+
418
+ # Set monitoring settings
419
+ config.monitoring.normal_interval = args.check_interval
420
+ config.monitoring.log_memory_stats = args.show_stats if hasattr(args, 'show_stats') else False
421
+
422
+ if hasattr(args, 'stats_interval'):
423
+ config.monitoring.log_interval = args.stats_interval
424
+
425
+ # Set restart policy
426
+ config.restart_policy.max_attempts = args.max_restarts
427
+
428
+ if hasattr(args, 'restart_cooldown'):
429
+ config.restart_policy.initial_cooldown = args.restart_cooldown
430
+
431
+ if hasattr(args, 'graceful_timeout'):
432
+ config.restart_policy.graceful_timeout = args.graceful_timeout
433
+
434
+ # Override config with CLI arguments
435
+ if hasattr(args, 'memory_threshold'):
436
+ config.thresholds.critical = args.memory_threshold
437
+
438
+ if hasattr(args, 'check_interval'):
439
+ config.monitoring.normal_interval = args.check_interval
440
+
441
+ if hasattr(args, 'max_restarts'):
442
+ config.restart_policy.max_attempts = args.max_restarts
443
+
444
+ # Check mode early to avoid unnecessary setup
445
+ if getattr(args, 'non_interactive', False):
446
+ logger.error("Non-interactive mode not yet supported for run-guarded")
447
+ return 1
448
+
449
+ # Determine verbosity
450
+ if hasattr(args, 'quiet') and args.quiet:
451
+ log_level = 'WARNING'
452
+ elif hasattr(args, 'verbose') and args.verbose:
453
+ log_level = 'DEBUG'
454
+
455
+ # Create runner
456
+ runner = MemoryAwareClaudeRunner(
457
+ enable_tickets=not getattr(args, 'no_tickets', False),
458
+ log_level=log_level,
459
+ claude_args=getattr(args, 'claude_args', []),
460
+ launch_method='subprocess', # Always subprocess for monitoring
461
+ enable_websocket=False, # Could be enabled in future
462
+ websocket_port=getattr(args, 'websocket_port', 8765),
463
+ memory_config=config,
464
+ enable_monitoring=True,
465
+ state_dir=getattr(args, 'state_dir', None)
466
+ )
467
+
468
+ # Deploy agents if not disabled
469
+ if not getattr(args, 'no_native_agents', False):
470
+ if not runner.setup_agents():
471
+ logger.warning("Failed to deploy some agents, continuing anyway")
472
+
473
+ # Get initial context if provided
474
+ initial_context = None
475
+ if hasattr(args, 'input') and args.input:
476
+ if Path(args.input).exists():
477
+ with open(args.input, 'r') as f:
478
+ initial_context = f.read()
479
+ else:
480
+ initial_context = args.input
481
+
482
+ # Run with monitoring
483
+ runner.run_interactive_with_monitoring(
484
+ initial_context=initial_context,
485
+ memory_threshold=config.thresholds.critical,
486
+ check_interval=config.monitoring.normal_interval,
487
+ max_restarts=config.restart_policy.max_attempts,
488
+ enable_state_preservation=getattr(args, 'state_preservation', True)
489
+ )
490
+
491
+ return 0
492
+
493
+ except KeyboardInterrupt:
494
+ logger.info("Run-guarded interrupted by user")
495
+ return 130 # Standard exit code for SIGINT
496
+ except Exception as e:
497
+ logger.error(f"Run-guarded failed: {e}", exc_info=True)
498
+ return 1
499
+
500
+
501
+ # Convenience function for direct module execution
502
+ def main():
503
+ """Main entry point for run-guarded command."""
504
+ parser = argparse.ArgumentParser(description='Run Claude with memory monitoring')
505
+ add_run_guarded_parser(parser._subparsers)
506
+ args = parser.parse_args()
507
+ sys.exit(execute_run_guarded(args))
508
+
509
+
510
+ if __name__ == '__main__':
511
+ main()
claude_mpm/cli/parser.py CHANGED
@@ -423,8 +423,8 @@ def create_parser(prog_name: str = "claude-mpm", version: str = "0.0.0") -> argp
423
423
  )
424
424
  update_ticket_parser.add_argument(
425
425
  "-s", "--status",
426
- choices=["open", "in_progress", "done", "closed", "blocked"],
427
- help="Update status"
426
+ choices=["waiting", "in_progress", "ready", "tested"],
427
+ help="Update workflow state (aitrackdown compatible)"
428
428
  )
429
429
  update_ticket_parser.add_argument(
430
430
  "-p", "--priority",
@@ -979,6 +979,12 @@ def create_parser(prog_name: str = "claude-mpm", version: str = "0.0.0") -> argp
979
979
  from .commands.cleanup import add_cleanup_parser
980
980
  add_cleanup_parser(subparsers)
981
981
 
982
+ # Import and add run-guarded command parser (EXPERIMENTAL)
983
+ # WHY: run-guarded is kept separate from run to maintain stability
984
+ # DESIGN DECISION: Experimental features are clearly marked and isolated
985
+ from .commands.run_guarded import add_run_guarded_parser
986
+ add_run_guarded_parser(subparsers)
987
+
982
988
  # MCP command with subcommands
983
989
  mcp_parser = subparsers.add_parser(
984
990
  CLICommands.MCP.value,