claude-mpm 4.0.3__py3-none-any.whl → 4.0.8__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 (40) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/templates/ticketing.json +1 -1
  3. claude_mpm/cli/commands/monitor.py +131 -9
  4. claude_mpm/cli/commands/tickets.py +61 -26
  5. claude_mpm/cli/parsers/monitor_parser.py +22 -2
  6. claude_mpm/constants.py +4 -1
  7. claude_mpm/core/framework_loader.py +102 -14
  8. claude_mpm/dashboard/static/built/components/agent-inference.js +2 -0
  9. claude_mpm/dashboard/static/built/components/event-processor.js +2 -0
  10. claude_mpm/dashboard/static/built/components/event-viewer.js +2 -0
  11. claude_mpm/dashboard/static/built/components/export-manager.js +2 -0
  12. claude_mpm/dashboard/static/built/components/file-tool-tracker.js +2 -0
  13. claude_mpm/dashboard/static/built/components/hud-library-loader.js +2 -0
  14. claude_mpm/dashboard/static/built/components/hud-manager.js +2 -0
  15. claude_mpm/dashboard/static/built/components/hud-visualizer.js +2 -0
  16. claude_mpm/dashboard/static/built/components/module-viewer.js +2 -0
  17. claude_mpm/dashboard/static/built/components/session-manager.js +2 -0
  18. claude_mpm/dashboard/static/built/components/socket-manager.js +2 -0
  19. claude_mpm/dashboard/static/built/components/ui-state-manager.js +2 -0
  20. claude_mpm/dashboard/static/built/components/working-directory.js +2 -0
  21. claude_mpm/dashboard/static/built/dashboard.js +2 -0
  22. claude_mpm/dashboard/static/built/socket-client.js +2 -0
  23. claude_mpm/dashboard/static/dist/components/event-viewer.js +1 -1
  24. claude_mpm/dashboard/static/dist/components/file-tool-tracker.js +1 -1
  25. claude_mpm/dashboard/static/dist/socket-client.js +1 -1
  26. claude_mpm/dashboard/static/js/components/event-viewer.js +24 -3
  27. claude_mpm/dashboard/static/js/components/file-tool-tracker.js +5 -5
  28. claude_mpm/dashboard/static/js/socket-client.js +25 -5
  29. claude_mpm/hooks/claude_hooks/connection_pool.py +75 -12
  30. claude_mpm/hooks/claude_hooks/hook_handler.py +63 -12
  31. claude_mpm/services/port_manager.py +370 -18
  32. claude_mpm/services/socketio/handlers/connection.py +41 -19
  33. claude_mpm/services/socketio/handlers/hook.py +23 -8
  34. claude_mpm/services/system_instructions_service.py +22 -7
  35. {claude_mpm-4.0.3.dist-info → claude_mpm-4.0.8.dist-info}/METADATA +64 -22
  36. {claude_mpm-4.0.3.dist-info → claude_mpm-4.0.8.dist-info}/RECORD +40 -25
  37. {claude_mpm-4.0.3.dist-info → claude_mpm-4.0.8.dist-info}/WHEEL +0 -0
  38. {claude_mpm-4.0.3.dist-info → claude_mpm-4.0.8.dist-info}/entry_points.txt +0 -0
  39. {claude_mpm-4.0.3.dist-info → claude_mpm-4.0.8.dist-info}/licenses/LICENSE +0 -0
  40. {claude_mpm-4.0.3.dist-info → claude_mpm-4.0.8.dist-info}/top_level.txt +0 -0
@@ -464,8 +464,14 @@ class SocketClient {
464
464
  transformedEvent.subtype = 'user_prompt';
465
465
  } else {
466
466
  // Generic fallback for unknown event names
467
- transformedEvent.type = 'system';
467
+ // Use 'unknown' for type and the actual eventName for subtype
468
+ transformedEvent.type = 'unknown';
468
469
  transformedEvent.subtype = eventName.toLowerCase();
470
+
471
+ // Prevent duplicate type/subtype values
472
+ if (transformedEvent.type === transformedEvent.subtype) {
473
+ transformedEvent.subtype = 'event';
474
+ }
469
475
  }
470
476
 
471
477
  // Remove the 'event' field to avoid confusion
@@ -494,14 +500,28 @@ class SocketClient {
494
500
  transformedEvent.subtype = '';
495
501
  }
496
502
 
503
+ // Store original event name for display purposes (before any transformation)
504
+ if (!eventData.type && eventData.event) {
505
+ transformedEvent.originalEventName = eventData.event;
506
+ } else if (eventData.type) {
507
+ transformedEvent.originalEventName = eventData.type;
508
+ }
509
+
497
510
  // Extract and flatten data fields to top level for dashboard compatibility
498
511
  // The dashboard expects fields like tool_name, agent_type, etc. at the top level
499
512
  if (eventData.data && typeof eventData.data === 'object') {
500
- // Copy all data fields to the top level
513
+ // Protected fields that should never be overwritten by data fields
514
+ const protectedFields = ['type', 'subtype', 'timestamp', 'id', 'event', 'event_type', 'originalEventName'];
515
+
516
+ // Copy all data fields to the top level, except protected ones
501
517
  Object.keys(eventData.data).forEach(key => {
502
- // Always copy data fields to ensure dashboard gets them
503
- // This overwrites any existing values to ensure data fields take precedence
504
- transformedEvent[key] = eventData.data[key];
518
+ // Only copy if not a protected field
519
+ if (!protectedFields.includes(key)) {
520
+ transformedEvent[key] = eventData.data[key];
521
+ } else {
522
+ // Log warning if data field would overwrite a protected field
523
+ console.warn(`Protected field '${key}' in data object was not copied to top level to preserve event structure`);
524
+ }
505
525
  });
506
526
 
507
527
  // Keep the original data object for backward compatibility
@@ -70,30 +70,78 @@ class SocketIOConnectionPool:
70
70
  return None
71
71
 
72
72
  def _create_connection(self, port: int) -> Optional[Any]:
73
- """Create a new Socket.IO connection."""
73
+ """Create a new Socket.IO connection with persistent keep-alive.
74
+
75
+ WHY persistent connections:
76
+ - Maintains connection throughout handler lifecycle
77
+ - Automatic reconnection on disconnect
78
+ - Reduced connection overhead for multiple events
79
+ - Better reliability for event delivery
80
+ """
74
81
  if not SOCKETIO_AVAILABLE:
75
82
  return None
76
83
  try:
77
84
  client = socketio.Client(
78
- reconnection=False,
85
+ reconnection=True, # Enable automatic reconnection
86
+ reconnection_attempts=5, # Try to reconnect up to 5 times
87
+ reconnection_delay=0.5, # Wait 0.5s between reconnection attempts
88
+ reconnection_delay_max=2.0, # Max delay between attempts
79
89
  logger=False,
80
- engineio_logger=False, # Disable auto-reconnect
90
+ engineio_logger=False,
81
91
  )
92
+
93
+ # Set up event handlers for connection lifecycle
94
+ @client.on('connect')
95
+ def on_connect():
96
+ pass # Connection established
97
+
98
+ @client.on('disconnect')
99
+ def on_disconnect():
100
+ pass # Will automatically try to reconnect
101
+
82
102
  client.connect(
83
103
  f"http://localhost:{port}",
84
- wait=True,
104
+ wait=True, # Wait for connection to establish
85
105
  wait_timeout=NetworkConfig.SOCKET_WAIT_TIMEOUT,
106
+ transports=['websocket', 'polling'], # Try WebSocket first, fall back to polling
86
107
  )
108
+
87
109
  if client.connected:
110
+ # Send a keep-alive ping to establish the connection
111
+ try:
112
+ client.emit('ping', {'timestamp': time.time()})
113
+ except:
114
+ pass # Ignore ping errors
88
115
  return client
89
116
  except Exception:
90
117
  pass
91
118
  return None
92
119
 
93
120
  def _is_connection_alive(self, client: Any) -> bool:
94
- """Check if a connection is still alive."""
121
+ """Check if a connection is still alive.
122
+
123
+ WHY enhanced check:
124
+ - Verifies actual connection state
125
+ - Attempts to ping server for liveness check
126
+ - More reliable than just checking connected flag
127
+ """
95
128
  try:
96
- return client and client.connected
129
+ if not client:
130
+ return False
131
+
132
+ # Check basic connection state
133
+ if not client.connected:
134
+ return False
135
+
136
+ # Try a quick ping to verify connection is truly alive
137
+ # This helps detect zombie connections
138
+ try:
139
+ # Just emit a ping, don't wait for response (faster)
140
+ client.emit('ping', {'timestamp': time.time()})
141
+ return True
142
+ except:
143
+ # If ping fails, connection might be dead
144
+ return client.connected # Fall back to basic check
97
145
  except:
98
146
  return False
99
147
 
@@ -106,12 +154,27 @@ class SocketIOConnectionPool:
106
154
  pass
107
155
 
108
156
  def _cleanup_dead_connections(self) -> None:
109
- """Remove dead connections from the pool."""
110
- self.connections = [
111
- conn
112
- for conn in self.connections
113
- if self._is_connection_alive(conn.get("client"))
114
- ]
157
+ """Remove dead connections from the pool and attempt reconnection.
158
+
159
+ WHY proactive reconnection:
160
+ - Maintains pool health
161
+ - Ensures connections are ready when needed
162
+ - Reduces latency for event emission
163
+ """
164
+ alive_connections = []
165
+ for conn in self.connections:
166
+ client = conn.get("client")
167
+ if self._is_connection_alive(client):
168
+ alive_connections.append(conn)
169
+ else:
170
+ # Try to reconnect dead connections
171
+ self._close_connection(client)
172
+ new_client = self._create_connection(conn.get("port", 8765))
173
+ if new_client:
174
+ conn["client"] = new_client
175
+ conn["created"] = time.time()
176
+ alive_connections.append(conn)
177
+ self.connections = alive_connections
115
178
 
116
179
  def close_all(self) -> None:
117
180
  """Close all connections in the pool."""
@@ -17,6 +17,7 @@ import json
17
17
  import os
18
18
  import select
19
19
  import signal
20
+ import subprocess
20
21
  import sys
21
22
  import threading
22
23
  import time
@@ -434,12 +435,13 @@ class ClaudeHookHandler:
434
435
  return int(os.environ.get("CLAUDE_MPM_SOCKETIO_PORT", "8765"))
435
436
 
436
437
  def _emit_socketio_event(self, namespace: str, event: str, data: dict):
437
- """Emit Socket.IO event with improved reliability and logging.
438
+ """Emit Socket.IO event with improved reliability and persistent connections.
438
439
 
439
440
  WHY improved approach:
440
- - Better error handling and recovery
441
- - Comprehensive event logging for debugging
442
- - Automatic reconnection on failure
441
+ - Maintains persistent connections throughout handler lifecycle
442
+ - Better error handling and automatic recovery
443
+ - Connection health monitoring before emission
444
+ - Automatic reconnection for critical events
443
445
  - Validates data before emission
444
446
  """
445
447
  # Always try to emit Socket.IO events if available
@@ -448,15 +450,56 @@ class ClaudeHookHandler:
448
450
  # Get Socket.IO client with dynamic port discovery
449
451
  port = self._discover_socketio_port()
450
452
  client = self.connection_pool.get_connection(port)
453
+
454
+ # If no client available, try to create one
451
455
  if not client:
452
456
  if DEBUG:
453
457
  print(
454
- f"Hook handler: No Socket.IO client available for event: hook.{event}",
458
+ f"Hook handler: No Socket.IO client available, attempting to create connection for event: hook.{event}",
455
459
  file=sys.stderr,
456
460
  )
457
- return
461
+ # Force creation of a new connection
462
+ client = self.connection_pool._create_connection(port)
463
+ if client:
464
+ # Add to pool for future use
465
+ self.connection_pool.connections.append(
466
+ {"port": port, "client": client, "created": time.time()}
467
+ )
468
+ else:
469
+ if DEBUG:
470
+ print(
471
+ f"Hook handler: Failed to create Socket.IO connection for event: hook.{event}",
472
+ file=sys.stderr,
473
+ )
474
+ return
458
475
 
459
476
  try:
477
+ # Verify connection is alive before emitting
478
+ if not client.connected:
479
+ if DEBUG:
480
+ print(
481
+ f"Hook handler: Client not connected, attempting reconnection for event: hook.{event}",
482
+ file=sys.stderr,
483
+ )
484
+ # Try to reconnect
485
+ try:
486
+ client.connect(
487
+ f"http://localhost:{port}",
488
+ wait=True,
489
+ wait_timeout=1.0,
490
+ transports=['websocket', 'polling'],
491
+ )
492
+ except:
493
+ # If reconnection fails, get a fresh client
494
+ client = self.connection_pool._create_connection(port)
495
+ if not client:
496
+ if DEBUG:
497
+ print(
498
+ f"Hook handler: Reconnection failed for event: hook.{event}",
499
+ file=sys.stderr,
500
+ )
501
+ return
502
+
460
503
  # Format event for Socket.IO server
461
504
  claude_event_data = {
462
505
  "type": f"hook.{event}", # Dashboard expects 'hook.' prefix
@@ -480,19 +523,23 @@ class ClaudeHookHandler:
480
523
  file=sys.stderr,
481
524
  )
482
525
 
483
- # Emit synchronously with verification
526
+ # Emit synchronously
484
527
  client.emit("claude_event", claude_event_data)
528
+
529
+ # For critical events, wait a moment to ensure delivery
530
+ if event in ["subagent_stop", "pre_tool"]:
531
+ time.sleep(0.01) # Small delay to ensure event is sent
485
532
 
486
533
  # Verify emission for critical events
487
534
  if event in ["subagent_stop", "pre_tool"] and DEBUG:
488
535
  if client.connected:
489
536
  print(
490
- f"✅ Successfully emitted Socket.IO event: hook.{event}",
537
+ f"✅ Successfully emitted Socket.IO event: hook.{event} (connection still active)",
491
538
  file=sys.stderr,
492
539
  )
493
540
  else:
494
541
  print(
495
- f"⚠️ Event emitted but connection status uncertain: hook.{event}",
542
+ f"⚠️ Event emitted but connection closed after: hook.{event}",
496
543
  file=sys.stderr,
497
544
  )
498
545
 
@@ -507,12 +554,16 @@ class ClaudeHookHandler:
507
554
  f"Hook handler: Attempting immediate reconnection for critical event: hook.{event}",
508
555
  file=sys.stderr,
509
556
  )
510
- # Try to get a new client and emit again
511
- retry_port = int(os.environ.get("CLAUDE_MPM_SOCKETIO_PORT", "8765"))
512
- retry_client = self.connection_pool.get_connection(retry_port)
557
+ # Force get a new client and emit again
558
+ self.connection_pool._cleanup_dead_connections()
559
+ retry_client = self.connection_pool._create_connection(port)
513
560
  if retry_client:
514
561
  try:
515
562
  retry_client.emit("claude_event", claude_event_data)
563
+ # Add to pool for future use
564
+ self.connection_pool.connections.append(
565
+ {"port": port, "client": retry_client, "created": time.time()}
566
+ )
516
567
  if DEBUG:
517
568
  print(
518
569
  f"✅ Successfully re-emitted event after reconnection: hook.{event}",