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.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/templates/ticketing.json +1 -1
- claude_mpm/cli/commands/monitor.py +131 -9
- claude_mpm/cli/commands/tickets.py +61 -26
- claude_mpm/cli/parsers/monitor_parser.py +22 -2
- claude_mpm/constants.py +4 -1
- claude_mpm/core/framework_loader.py +102 -14
- claude_mpm/dashboard/static/built/components/agent-inference.js +2 -0
- claude_mpm/dashboard/static/built/components/event-processor.js +2 -0
- claude_mpm/dashboard/static/built/components/event-viewer.js +2 -0
- claude_mpm/dashboard/static/built/components/export-manager.js +2 -0
- claude_mpm/dashboard/static/built/components/file-tool-tracker.js +2 -0
- claude_mpm/dashboard/static/built/components/hud-library-loader.js +2 -0
- claude_mpm/dashboard/static/built/components/hud-manager.js +2 -0
- claude_mpm/dashboard/static/built/components/hud-visualizer.js +2 -0
- claude_mpm/dashboard/static/built/components/module-viewer.js +2 -0
- claude_mpm/dashboard/static/built/components/session-manager.js +2 -0
- claude_mpm/dashboard/static/built/components/socket-manager.js +2 -0
- claude_mpm/dashboard/static/built/components/ui-state-manager.js +2 -0
- claude_mpm/dashboard/static/built/components/working-directory.js +2 -0
- claude_mpm/dashboard/static/built/dashboard.js +2 -0
- claude_mpm/dashboard/static/built/socket-client.js +2 -0
- claude_mpm/dashboard/static/dist/components/event-viewer.js +1 -1
- claude_mpm/dashboard/static/dist/components/file-tool-tracker.js +1 -1
- claude_mpm/dashboard/static/dist/socket-client.js +1 -1
- claude_mpm/dashboard/static/js/components/event-viewer.js +24 -3
- claude_mpm/dashboard/static/js/components/file-tool-tracker.js +5 -5
- claude_mpm/dashboard/static/js/socket-client.js +25 -5
- claude_mpm/hooks/claude_hooks/connection_pool.py +75 -12
- claude_mpm/hooks/claude_hooks/hook_handler.py +63 -12
- claude_mpm/services/port_manager.py +370 -18
- claude_mpm/services/socketio/handlers/connection.py +41 -19
- claude_mpm/services/socketio/handlers/hook.py +23 -8
- claude_mpm/services/system_instructions_service.py +22 -7
- {claude_mpm-4.0.3.dist-info → claude_mpm-4.0.8.dist-info}/METADATA +64 -22
- {claude_mpm-4.0.3.dist-info → claude_mpm-4.0.8.dist-info}/RECORD +40 -25
- {claude_mpm-4.0.3.dist-info → claude_mpm-4.0.8.dist-info}/WHEEL +0 -0
- {claude_mpm-4.0.3.dist-info → claude_mpm-4.0.8.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.0.3.dist-info → claude_mpm-4.0.8.dist-info}/licenses/LICENSE +0 -0
- {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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
503
|
-
|
|
504
|
-
|
|
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=
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
|
438
|
+
"""Emit Socket.IO event with improved reliability and persistent connections.
|
|
438
439
|
|
|
439
440
|
WHY improved approach:
|
|
440
|
-
-
|
|
441
|
-
-
|
|
442
|
-
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
#
|
|
511
|
-
|
|
512
|
-
retry_client = self.connection_pool.
|
|
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}",
|