claude-mpm 4.0.31__py3-none-any.whl → 4.0.34__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/BASE_AGENT_TEMPLATE.md +33 -25
- claude_mpm/agents/INSTRUCTIONS.md +14 -10
- claude_mpm/agents/templates/documentation.json +51 -34
- claude_mpm/agents/templates/research.json +0 -11
- claude_mpm/cli/__init__.py +63 -26
- claude_mpm/cli/commands/agent_manager.py +10 -8
- claude_mpm/core/framework_loader.py +272 -113
- claude_mpm/dashboard/static/css/dashboard.css +449 -0
- claude_mpm/dashboard/static/dist/components/agent-inference.js +1 -1
- 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/components/module-viewer.js +1 -1
- claude_mpm/dashboard/static/dist/components/session-manager.js +1 -1
- claude_mpm/dashboard/static/dist/dashboard.js +1 -1
- claude_mpm/dashboard/static/dist/socket-client.js +1 -1
- claude_mpm/dashboard/static/js/components/agent-hierarchy.js +774 -0
- claude_mpm/dashboard/static/js/components/agent-inference.js +257 -3
- claude_mpm/dashboard/static/js/components/build-tracker.js +289 -0
- claude_mpm/dashboard/static/js/components/event-viewer.js +168 -39
- claude_mpm/dashboard/static/js/components/file-tool-tracker.js +17 -0
- claude_mpm/dashboard/static/js/components/session-manager.js +23 -3
- claude_mpm/dashboard/static/js/components/socket-manager.js +2 -0
- claude_mpm/dashboard/static/js/dashboard.js +207 -31
- claude_mpm/dashboard/static/js/socket-client.js +85 -6
- claude_mpm/dashboard/templates/index.html +1 -0
- claude_mpm/hooks/claude_hooks/connection_pool.py +12 -2
- claude_mpm/hooks/claude_hooks/event_handlers.py +81 -19
- claude_mpm/hooks/claude_hooks/hook_handler.py +72 -10
- claude_mpm/hooks/claude_hooks/hook_handler_eventbus.py +398 -0
- claude_mpm/hooks/claude_hooks/response_tracking.py +10 -0
- claude_mpm/services/agents/deployment/agent_deployment.py +86 -37
- claude_mpm/services/agents/deployment/agent_template_builder.py +18 -10
- claude_mpm/services/agents/deployment/agents_directory_resolver.py +10 -25
- claude_mpm/services/agents/deployment/multi_source_deployment_service.py +189 -3
- claude_mpm/services/agents/deployment/pipeline/steps/target_directory_step.py +3 -2
- claude_mpm/services/agents/deployment/strategies/system_strategy.py +10 -3
- claude_mpm/services/agents/deployment/strategies/user_strategy.py +10 -14
- claude_mpm/services/agents/deployment/system_instructions_deployer.py +8 -13
- claude_mpm/services/agents/memory/agent_memory_manager.py +141 -184
- claude_mpm/services/agents/memory/content_manager.py +182 -232
- claude_mpm/services/agents/memory/template_generator.py +4 -40
- claude_mpm/services/event_bus/__init__.py +18 -0
- claude_mpm/services/event_bus/event_bus.py +334 -0
- claude_mpm/services/event_bus/relay.py +301 -0
- claude_mpm/services/events/__init__.py +44 -0
- claude_mpm/services/events/consumers/__init__.py +18 -0
- claude_mpm/services/events/consumers/dead_letter.py +296 -0
- claude_mpm/services/events/consumers/logging.py +183 -0
- claude_mpm/services/events/consumers/metrics.py +242 -0
- claude_mpm/services/events/consumers/socketio.py +376 -0
- claude_mpm/services/events/core.py +470 -0
- claude_mpm/services/events/interfaces.py +230 -0
- claude_mpm/services/events/producers/__init__.py +14 -0
- claude_mpm/services/events/producers/hook.py +269 -0
- claude_mpm/services/events/producers/system.py +327 -0
- claude_mpm/services/mcp_gateway/core/process_pool.py +411 -0
- claude_mpm/services/mcp_gateway/server/stdio_server.py +13 -0
- claude_mpm/services/monitor_build_service.py +345 -0
- claude_mpm/services/socketio/event_normalizer.py +667 -0
- claude_mpm/services/socketio/handlers/connection.py +78 -20
- claude_mpm/services/socketio/handlers/hook.py +14 -5
- claude_mpm/services/socketio/migration_utils.py +329 -0
- claude_mpm/services/socketio/server/broadcaster.py +26 -33
- claude_mpm/services/socketio/server/core.py +4 -3
- {claude_mpm-4.0.31.dist-info → claude_mpm-4.0.34.dist-info}/METADATA +4 -3
- {claude_mpm-4.0.31.dist-info → claude_mpm-4.0.34.dist-info}/RECORD +71 -50
- {claude_mpm-4.0.31.dist-info → claude_mpm-4.0.34.dist-info}/WHEEL +0 -0
- {claude_mpm-4.0.31.dist-info → claude_mpm-4.0.34.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.0.31.dist-info → claude_mpm-4.0.34.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.0.31.dist-info → claude_mpm-4.0.34.dist-info}/top_level.txt +0 -0
|
@@ -161,8 +161,13 @@ class ConnectionEventHandler(BaseEventHandler):
|
|
|
161
161
|
|
|
162
162
|
for sid in list(self.clients):
|
|
163
163
|
try:
|
|
164
|
-
# Send ping and record time
|
|
165
|
-
await self.sio.emit('ping', {
|
|
164
|
+
# Send ping and record time (using new schema)
|
|
165
|
+
await self.sio.emit('ping', {
|
|
166
|
+
'type': 'system',
|
|
167
|
+
'subtype': 'ping',
|
|
168
|
+
'timestamp': current_time,
|
|
169
|
+
'source': 'server'
|
|
170
|
+
}, room=sid)
|
|
166
171
|
self.last_ping_times[sid] = current_time
|
|
167
172
|
|
|
168
173
|
# Update connection metrics
|
|
@@ -284,16 +289,35 @@ class ConnectionEventHandler(BaseEventHandler):
|
|
|
284
289
|
self.logger.info(f"📱 User Agent: {user_agent[:100]}...")
|
|
285
290
|
self.logger.info(f"📈 Total clients now: {len(self.clients)}")
|
|
286
291
|
|
|
287
|
-
#
|
|
292
|
+
# Get monitor build info
|
|
293
|
+
monitor_build_info = {}
|
|
294
|
+
try:
|
|
295
|
+
from ....services.monitor_build_service import get_monitor_build_service
|
|
296
|
+
monitor_service = get_monitor_build_service()
|
|
297
|
+
monitor_build_info = monitor_service.get_build_info_sync()
|
|
298
|
+
except Exception as e:
|
|
299
|
+
self.logger.debug(f"Could not get monitor build info: {e}")
|
|
300
|
+
monitor_build_info = {
|
|
301
|
+
"monitor": {"version": "1.0.0", "build": 1, "formatted_build": "0001"},
|
|
302
|
+
"mpm": {"version": "unknown", "build": "unknown"}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
# Send initial status immediately with enhanced data (using new schema)
|
|
288
306
|
status_data = {
|
|
289
|
-
"
|
|
307
|
+
"type": "connection",
|
|
308
|
+
"subtype": "status",
|
|
290
309
|
"timestamp": datetime.utcnow().isoformat() + "Z",
|
|
291
|
-
"
|
|
310
|
+
"source": "server",
|
|
292
311
|
"session_id": self.server.session_id,
|
|
293
|
-
"
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
312
|
+
"data": {
|
|
313
|
+
"server": "claude-mpm-python-socketio",
|
|
314
|
+
"clients_connected": len(self.clients),
|
|
315
|
+
"claude_status": self.server.claude_status,
|
|
316
|
+
"claude_pid": self.server.claude_pid,
|
|
317
|
+
"server_version": "2.0.0",
|
|
318
|
+
"client_id": sid,
|
|
319
|
+
"build_info": monitor_build_info,
|
|
320
|
+
}
|
|
297
321
|
}
|
|
298
322
|
|
|
299
323
|
try:
|
|
@@ -302,9 +326,17 @@ class ConnectionEventHandler(BaseEventHandler):
|
|
|
302
326
|
sid,
|
|
303
327
|
"welcome",
|
|
304
328
|
{
|
|
305
|
-
"
|
|
306
|
-
"
|
|
307
|
-
"
|
|
329
|
+
"type": "connection",
|
|
330
|
+
"subtype": "welcome",
|
|
331
|
+
"timestamp": datetime.utcnow().isoformat() + "Z",
|
|
332
|
+
"source": "server",
|
|
333
|
+
"session_id": self.server.session_id,
|
|
334
|
+
"data": {
|
|
335
|
+
"message": "Connected to Claude MPM Socket.IO server",
|
|
336
|
+
"client_id": sid,
|
|
337
|
+
"server_time": datetime.utcnow().isoformat() + "Z",
|
|
338
|
+
"build_info": monitor_build_info,
|
|
339
|
+
}
|
|
308
340
|
},
|
|
309
341
|
)
|
|
310
342
|
|
|
@@ -349,12 +381,17 @@ class ConnectionEventHandler(BaseEventHandler):
|
|
|
349
381
|
to update their UI or verify connection health.
|
|
350
382
|
"""
|
|
351
383
|
status_data = {
|
|
352
|
-
"
|
|
384
|
+
"type": "connection",
|
|
385
|
+
"subtype": "status",
|
|
353
386
|
"timestamp": datetime.utcnow().isoformat() + "Z",
|
|
354
|
-
"
|
|
387
|
+
"source": "server",
|
|
355
388
|
"session_id": self.server.session_id,
|
|
356
|
-
"
|
|
357
|
-
|
|
389
|
+
"data": {
|
|
390
|
+
"server": "claude-mpm-python-socketio",
|
|
391
|
+
"clients_connected": len(self.clients),
|
|
392
|
+
"claude_status": self.server.claude_status,
|
|
393
|
+
"claude_pid": self.server.claude_pid,
|
|
394
|
+
}
|
|
358
395
|
}
|
|
359
396
|
await self.emit_to_client(sid, "status", status_data)
|
|
360
397
|
|
|
@@ -395,7 +432,13 @@ class ConnectionEventHandler(BaseEventHandler):
|
|
|
395
432
|
for filtered event streaming.
|
|
396
433
|
"""
|
|
397
434
|
channels = data.get("channels", ["*"]) if data else ["*"]
|
|
398
|
-
await self.emit_to_client(sid, "subscribed", {
|
|
435
|
+
await self.emit_to_client(sid, "subscribed", {
|
|
436
|
+
"type": "connection",
|
|
437
|
+
"subtype": "subscribed",
|
|
438
|
+
"timestamp": datetime.utcnow().isoformat() + "Z",
|
|
439
|
+
"source": "server",
|
|
440
|
+
"data": {"channels": channels}
|
|
441
|
+
})
|
|
399
442
|
|
|
400
443
|
@self.sio.event
|
|
401
444
|
@timeout_handler(timeout_seconds=5.0)
|
|
@@ -409,10 +452,24 @@ class ConnectionEventHandler(BaseEventHandler):
|
|
|
409
452
|
self.logger.info(f"🔵 Received claude_event from {sid}: {data}")
|
|
410
453
|
|
|
411
454
|
# Check if this is a hook event and route to HookEventHandler
|
|
412
|
-
# Hook events have
|
|
455
|
+
# Hook events can have either:
|
|
456
|
+
# 1. Normalized format: type="hook", subtype="pre_tool"
|
|
457
|
+
# 2. Legacy format: type="hook.pre_tool"
|
|
413
458
|
if isinstance(data, dict):
|
|
414
459
|
event_type = data.get("type", "")
|
|
415
|
-
|
|
460
|
+
event_subtype = data.get("subtype", "")
|
|
461
|
+
|
|
462
|
+
# Check for both normalized and legacy formats
|
|
463
|
+
is_hook_event = False
|
|
464
|
+
if isinstance(event_type, str):
|
|
465
|
+
# Legacy format: type="hook.something"
|
|
466
|
+
if event_type.startswith("hook."):
|
|
467
|
+
is_hook_event = True
|
|
468
|
+
# Normalized format: type="hook" with any subtype
|
|
469
|
+
elif event_type == "hook":
|
|
470
|
+
is_hook_event = True
|
|
471
|
+
|
|
472
|
+
if is_hook_event:
|
|
416
473
|
# Get the hook handler if available
|
|
417
474
|
hook_handler = None
|
|
418
475
|
# Check if event_registry exists and has handlers
|
|
@@ -502,7 +559,8 @@ class ConnectionEventHandler(BaseEventHandler):
|
|
|
502
559
|
elif event_name == 'ToolCall':
|
|
503
560
|
normalized['type'] = 'tool'
|
|
504
561
|
elif event_name == 'UserPrompt':
|
|
505
|
-
normalized['type'] = 'hook
|
|
562
|
+
normalized['type'] = 'hook'
|
|
563
|
+
normalized['subtype'] = 'user_prompt'
|
|
506
564
|
else:
|
|
507
565
|
# Default to system type for unknown events
|
|
508
566
|
normalized['type'] = 'system'
|
|
@@ -41,14 +41,23 @@ class HookEventHandler(BaseEventHandler):
|
|
|
41
41
|
return
|
|
42
42
|
|
|
43
43
|
# Extract hook event details
|
|
44
|
-
# Hook events come
|
|
44
|
+
# Hook events can come in two formats:
|
|
45
|
+
# 1. Normalized: { type: "hook", subtype: "user_prompt", ... }
|
|
46
|
+
# 2. Legacy: { type: "hook.user_prompt", ... }
|
|
45
47
|
event_type = data.get("type", "")
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
event_subtype = data.get("subtype", "")
|
|
49
|
+
|
|
50
|
+
# Determine the actual hook event name
|
|
51
|
+
hook_event = ""
|
|
52
|
+
if event_type == "hook" and event_subtype:
|
|
53
|
+
# Normalized format: use subtype directly
|
|
54
|
+
hook_event = event_subtype
|
|
55
|
+
elif isinstance(event_type, str) and event_type.startswith("hook."):
|
|
56
|
+
# Legacy format: extract from dotted type
|
|
49
57
|
hook_event = event_type[5:] # Remove "hook." prefix
|
|
50
58
|
else:
|
|
51
|
-
|
|
59
|
+
# Fallback: check for 'event' field (another legacy format)
|
|
60
|
+
hook_event = data.get("event", "")
|
|
52
61
|
|
|
53
62
|
hook_data = data.get("data", {})
|
|
54
63
|
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Migration utilities for Socket.IO event schema migration.
|
|
3
|
+
|
|
4
|
+
WHY: This module provides utilities to help migrate from the old event
|
|
5
|
+
formats to the new normalized schema, ensuring backward compatibility
|
|
6
|
+
during the transition period.
|
|
7
|
+
|
|
8
|
+
DESIGN DECISION: Provide both transformation and validation utilities
|
|
9
|
+
to help identify and fix inconsistent event formats across the codebase.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
14
|
+
import json
|
|
15
|
+
|
|
16
|
+
from ...core.logging_config import get_logger
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class EventMigrationHelper:
|
|
20
|
+
"""Helper class for migrating events to the new schema.
|
|
21
|
+
|
|
22
|
+
WHY: Provides utilities to identify old format events and
|
|
23
|
+
transform them to the new normalized schema.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self):
|
|
27
|
+
self.logger = get_logger(self.__class__.__name__)
|
|
28
|
+
self.migration_stats = {
|
|
29
|
+
"old_format_detected": 0,
|
|
30
|
+
"transformed": 0,
|
|
31
|
+
"validation_failed": 0,
|
|
32
|
+
"already_normalized": 0
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
def is_old_format(self, event_data: Any) -> bool:
|
|
36
|
+
"""Check if an event is in the old format.
|
|
37
|
+
|
|
38
|
+
WHY: Need to identify which events need migration.
|
|
39
|
+
"""
|
|
40
|
+
if not isinstance(event_data, dict):
|
|
41
|
+
return True # Non-dict events are definitely old format
|
|
42
|
+
|
|
43
|
+
# Check for new format fields
|
|
44
|
+
required_new_fields = {"event", "type", "subtype", "timestamp", "data"}
|
|
45
|
+
has_all_new_fields = all(field in event_data for field in required_new_fields)
|
|
46
|
+
|
|
47
|
+
if has_all_new_fields:
|
|
48
|
+
self.migration_stats["already_normalized"] += 1
|
|
49
|
+
return False
|
|
50
|
+
|
|
51
|
+
# Old format indicators
|
|
52
|
+
old_format_indicators = [
|
|
53
|
+
# Hook format with "hook." prefix in type
|
|
54
|
+
"type" in event_data and isinstance(event_data.get("type"), str) and event_data["type"].startswith("hook."),
|
|
55
|
+
# Missing subtype field
|
|
56
|
+
"type" in event_data and "subtype" not in event_data,
|
|
57
|
+
# Event field used differently
|
|
58
|
+
"event" in event_data and event_data.get("event") != "claude_event",
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
if any(old_format_indicators):
|
|
62
|
+
self.migration_stats["old_format_detected"] += 1
|
|
63
|
+
return True
|
|
64
|
+
|
|
65
|
+
return False
|
|
66
|
+
|
|
67
|
+
def transform_to_new_format(self, event_data: Any) -> Dict[str, Any]:
|
|
68
|
+
"""Transform an old format event to the new schema.
|
|
69
|
+
|
|
70
|
+
WHY: Provides a migration path from old to new format.
|
|
71
|
+
"""
|
|
72
|
+
# Import here to avoid circular dependency
|
|
73
|
+
from .event_normalizer import EventNormalizer
|
|
74
|
+
|
|
75
|
+
normalizer = EventNormalizer()
|
|
76
|
+
normalized = normalizer.normalize(event_data)
|
|
77
|
+
|
|
78
|
+
self.migration_stats["transformed"] += 1
|
|
79
|
+
return normalized.to_dict()
|
|
80
|
+
|
|
81
|
+
def validate_event_schema(self, event_data: Dict[str, Any]) -> Tuple[bool, List[str]]:
|
|
82
|
+
"""Validate an event against the new schema.
|
|
83
|
+
|
|
84
|
+
WHY: Ensures events conform to the expected structure
|
|
85
|
+
before being sent to clients.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Tuple of (is_valid, list_of_errors)
|
|
89
|
+
"""
|
|
90
|
+
errors = []
|
|
91
|
+
|
|
92
|
+
# Check required fields
|
|
93
|
+
required_fields = {"event", "type", "subtype", "timestamp", "data"}
|
|
94
|
+
missing_fields = required_fields - set(event_data.keys())
|
|
95
|
+
if missing_fields:
|
|
96
|
+
errors.append(f"Missing required fields: {missing_fields}")
|
|
97
|
+
|
|
98
|
+
# Validate field types
|
|
99
|
+
if "event" in event_data and event_data["event"] != "claude_event":
|
|
100
|
+
errors.append(f"Invalid event name: {event_data['event']} (should be 'claude_event')")
|
|
101
|
+
|
|
102
|
+
if "type" in event_data and not isinstance(event_data["type"], str):
|
|
103
|
+
errors.append(f"Invalid type field: should be string, got {type(event_data['type'])}")
|
|
104
|
+
|
|
105
|
+
if "subtype" in event_data and not isinstance(event_data["subtype"], str):
|
|
106
|
+
errors.append(f"Invalid subtype field: should be string, got {type(event_data['subtype'])}")
|
|
107
|
+
|
|
108
|
+
if "timestamp" in event_data:
|
|
109
|
+
timestamp = event_data["timestamp"]
|
|
110
|
+
if not isinstance(timestamp, str) or "T" not in timestamp:
|
|
111
|
+
errors.append(f"Invalid timestamp format: {timestamp} (should be ISO format)")
|
|
112
|
+
|
|
113
|
+
if "data" in event_data and not isinstance(event_data["data"], dict):
|
|
114
|
+
errors.append(f"Invalid data field: should be dict, got {type(event_data['data'])}")
|
|
115
|
+
|
|
116
|
+
if errors:
|
|
117
|
+
self.migration_stats["validation_failed"] += 1
|
|
118
|
+
return False, errors
|
|
119
|
+
|
|
120
|
+
return True, []
|
|
121
|
+
|
|
122
|
+
def get_migration_report(self) -> str:
|
|
123
|
+
"""Generate a report of migration statistics.
|
|
124
|
+
|
|
125
|
+
WHY: Helps track migration progress and identify issues.
|
|
126
|
+
"""
|
|
127
|
+
report = "Event Migration Report\n"
|
|
128
|
+
report += "=" * 50 + "\n"
|
|
129
|
+
report += f"Old format detected: {self.migration_stats['old_format_detected']}\n"
|
|
130
|
+
report += f"Events transformed: {self.migration_stats['transformed']}\n"
|
|
131
|
+
report += f"Validation failures: {self.migration_stats['validation_failed']}\n"
|
|
132
|
+
report += f"Already normalized: {self.migration_stats['already_normalized']}\n"
|
|
133
|
+
return report
|
|
134
|
+
|
|
135
|
+
def reset_stats(self):
|
|
136
|
+
"""Reset migration statistics."""
|
|
137
|
+
self.migration_stats = {
|
|
138
|
+
"old_format_detected": 0,
|
|
139
|
+
"transformed": 0,
|
|
140
|
+
"validation_failed": 0,
|
|
141
|
+
"already_normalized": 0
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class EventTypeMapper:
|
|
146
|
+
"""Maps old event types to new type/subtype categories.
|
|
147
|
+
|
|
148
|
+
WHY: Provides a consistent mapping from legacy event names
|
|
149
|
+
to the new categorized structure.
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
# Comprehensive mapping of old event formats to new categories
|
|
153
|
+
TYPE_MAPPINGS = {
|
|
154
|
+
# Hook events with "hook." prefix
|
|
155
|
+
"hook.pre_tool": ("hook", "pre_tool"),
|
|
156
|
+
"hook.post_tool": ("hook", "post_tool"),
|
|
157
|
+
"hook.pre_response": ("hook", "pre_response"),
|
|
158
|
+
"hook.post_response": ("hook", "post_response"),
|
|
159
|
+
"hook.start": ("hook", "start"),
|
|
160
|
+
"hook.stop": ("hook", "stop"),
|
|
161
|
+
"hook.subagent_start": ("hook", "subagent_start"),
|
|
162
|
+
"hook.subagent_stop": ("hook", "subagent_stop"),
|
|
163
|
+
|
|
164
|
+
# Hook events without prefix
|
|
165
|
+
"pre_tool": ("hook", "pre_tool"),
|
|
166
|
+
"post_tool": ("hook", "post_tool"),
|
|
167
|
+
"pre_response": ("hook", "pre_response"),
|
|
168
|
+
"post_response": ("hook", "post_response"),
|
|
169
|
+
|
|
170
|
+
# System events
|
|
171
|
+
"system_heartbeat": ("system", "heartbeat"),
|
|
172
|
+
"heartbeat": ("system", "heartbeat"),
|
|
173
|
+
"system_status": ("system", "status"),
|
|
174
|
+
|
|
175
|
+
# Session events
|
|
176
|
+
"session_started": ("session", "started"),
|
|
177
|
+
"session_ended": ("session", "ended"),
|
|
178
|
+
|
|
179
|
+
# File events
|
|
180
|
+
"file_changed": ("file", "changed"),
|
|
181
|
+
"file_created": ("file", "created"),
|
|
182
|
+
"file_deleted": ("file", "deleted"),
|
|
183
|
+
|
|
184
|
+
# Connection events
|
|
185
|
+
"client_connected": ("connection", "connected"),
|
|
186
|
+
"client_disconnected": ("connection", "disconnected"),
|
|
187
|
+
|
|
188
|
+
# Memory events
|
|
189
|
+
"memory_loaded": ("memory", "loaded"),
|
|
190
|
+
"memory_created": ("memory", "created"),
|
|
191
|
+
"memory_updated": ("memory", "updated"),
|
|
192
|
+
"memory_injected": ("memory", "injected"),
|
|
193
|
+
|
|
194
|
+
# Git events
|
|
195
|
+
"git_operation": ("git", "operation"),
|
|
196
|
+
"git_commit": ("git", "commit"),
|
|
197
|
+
"git_push": ("git", "push"),
|
|
198
|
+
|
|
199
|
+
# Todo events
|
|
200
|
+
"todo_updated": ("todo", "updated"),
|
|
201
|
+
"todo_created": ("todo", "created"),
|
|
202
|
+
|
|
203
|
+
# Ticket events
|
|
204
|
+
"ticket_created": ("ticket", "created"),
|
|
205
|
+
"ticket_updated": ("ticket", "updated"),
|
|
206
|
+
|
|
207
|
+
# Agent events
|
|
208
|
+
"agent_delegated": ("agent", "delegated"),
|
|
209
|
+
"agent_completed": ("agent", "completed"),
|
|
210
|
+
|
|
211
|
+
# Claude events
|
|
212
|
+
"claude_status": ("claude", "status"),
|
|
213
|
+
"claude_output": ("claude", "output"),
|
|
214
|
+
|
|
215
|
+
# Error events
|
|
216
|
+
"error": ("error", "general"),
|
|
217
|
+
"error_occurred": ("error", "occurred"),
|
|
218
|
+
|
|
219
|
+
# Performance events
|
|
220
|
+
"performance": ("performance", "metric"),
|
|
221
|
+
"performance_metric": ("performance", "metric"),
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
@classmethod
|
|
225
|
+
def map_event_type(cls, old_type: str) -> Tuple[str, str]:
|
|
226
|
+
"""Map an old event type to new type/subtype.
|
|
227
|
+
|
|
228
|
+
WHY: Provides consistent categorization for all events.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
old_type: The old event type string
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
Tuple of (type, subtype)
|
|
235
|
+
"""
|
|
236
|
+
# Direct mapping
|
|
237
|
+
if old_type in cls.TYPE_MAPPINGS:
|
|
238
|
+
return cls.TYPE_MAPPINGS[old_type]
|
|
239
|
+
|
|
240
|
+
# Try to infer from patterns
|
|
241
|
+
old_lower = old_type.lower()
|
|
242
|
+
|
|
243
|
+
# Hook events
|
|
244
|
+
if "hook" in old_lower or old_lower.startswith("pre_") or old_lower.startswith("post_"):
|
|
245
|
+
# Remove "hook." prefix if present
|
|
246
|
+
clean_type = old_type.replace("hook.", "")
|
|
247
|
+
return "hook", clean_type
|
|
248
|
+
|
|
249
|
+
# System events
|
|
250
|
+
if "system" in old_lower or "heartbeat" in old_lower:
|
|
251
|
+
return "system", old_type.replace("system_", "")
|
|
252
|
+
|
|
253
|
+
# Session events
|
|
254
|
+
if "session" in old_lower:
|
|
255
|
+
if "start" in old_lower:
|
|
256
|
+
return "session", "started"
|
|
257
|
+
elif "end" in old_lower:
|
|
258
|
+
return "session", "ended"
|
|
259
|
+
return "session", "generic"
|
|
260
|
+
|
|
261
|
+
# File events
|
|
262
|
+
if "file" in old_lower:
|
|
263
|
+
if "create" in old_lower:
|
|
264
|
+
return "file", "created"
|
|
265
|
+
elif "delete" in old_lower:
|
|
266
|
+
return "file", "deleted"
|
|
267
|
+
elif "change" in old_lower or "modify" in old_lower:
|
|
268
|
+
return "file", "changed"
|
|
269
|
+
return "file", "generic"
|
|
270
|
+
|
|
271
|
+
# Default mapping
|
|
272
|
+
return "unknown", old_type
|
|
273
|
+
|
|
274
|
+
@classmethod
|
|
275
|
+
def get_event_category(cls, event_type: str, event_subtype: str) -> str:
|
|
276
|
+
"""Get a human-readable category for an event.
|
|
277
|
+
|
|
278
|
+
WHY: Helps with filtering and display in UIs.
|
|
279
|
+
"""
|
|
280
|
+
categories = {
|
|
281
|
+
"hook": "Claude Hooks",
|
|
282
|
+
"system": "System Status",
|
|
283
|
+
"session": "Session Management",
|
|
284
|
+
"file": "File Operations",
|
|
285
|
+
"connection": "Client Connections",
|
|
286
|
+
"memory": "Memory System",
|
|
287
|
+
"git": "Git Operations",
|
|
288
|
+
"todo": "Todo Management",
|
|
289
|
+
"ticket": "Ticket System",
|
|
290
|
+
"agent": "Agent Delegation",
|
|
291
|
+
"claude": "Claude Process",
|
|
292
|
+
"error": "Errors",
|
|
293
|
+
"performance": "Performance Metrics",
|
|
294
|
+
"unknown": "Uncategorized"
|
|
295
|
+
}
|
|
296
|
+
return categories.get(event_type, "Other")
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def create_backward_compatible_event(normalized_event: Dict[str, Any]) -> Dict[str, Any]:
|
|
300
|
+
"""Create a backward-compatible version of a normalized event.
|
|
301
|
+
|
|
302
|
+
WHY: During migration, some clients may still expect the old format.
|
|
303
|
+
This creates an event that works with both old and new clients.
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
normalized_event: Event in the new normalized format
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
Event that includes both old and new format fields
|
|
310
|
+
"""
|
|
311
|
+
# Start with the normalized event
|
|
312
|
+
compat_event = normalized_event.copy()
|
|
313
|
+
|
|
314
|
+
# Add old format fields based on type/subtype
|
|
315
|
+
event_type = normalized_event.get("type", "")
|
|
316
|
+
event_subtype = normalized_event.get("subtype", "")
|
|
317
|
+
|
|
318
|
+
# For hook events, add the old "hook." prefix format
|
|
319
|
+
if event_type == "hook":
|
|
320
|
+
compat_event["type_legacy"] = f"hook.{event_subtype}"
|
|
321
|
+
|
|
322
|
+
# For other events, use the old naming convention
|
|
323
|
+
elif event_type in ["session", "file", "memory", "git", "todo", "ticket", "agent", "claude"]:
|
|
324
|
+
compat_event["type_legacy"] = f"{event_type}_{event_subtype}"
|
|
325
|
+
|
|
326
|
+
# Add event_type field for really old clients
|
|
327
|
+
compat_event["event_type"] = compat_event.get("type_legacy", f"{event_type}_{event_subtype}")
|
|
328
|
+
|
|
329
|
+
return compat_event
|
|
@@ -17,6 +17,7 @@ from datetime import datetime
|
|
|
17
17
|
from typing import Any, Deque, Dict, List, Optional, Set
|
|
18
18
|
|
|
19
19
|
from ....core.logging_config import get_logger
|
|
20
|
+
from ..event_normalizer import EventNormalizer
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
@dataclass
|
|
@@ -178,6 +179,9 @@ class SocketIOEventBroadcaster:
|
|
|
178
179
|
self.retry_queue = RetryQueue(max_size=1000)
|
|
179
180
|
self.retry_task = None
|
|
180
181
|
self.retry_interval = 2.0 # Process retry queue every 2 seconds
|
|
182
|
+
|
|
183
|
+
# Initialize event normalizer for consistent schema
|
|
184
|
+
self.normalizer = EventNormalizer()
|
|
181
185
|
|
|
182
186
|
def start_retry_processor(self):
|
|
183
187
|
"""Start the background retry processor.
|
|
@@ -274,21 +278,25 @@ class SocketIOEventBroadcaster:
|
|
|
274
278
|
"""Retry broadcasting a failed event.
|
|
275
279
|
|
|
276
280
|
WHY: Isolated retry logic allows for special handling
|
|
277
|
-
and metrics tracking of retry attempts.
|
|
281
|
+
and metrics tracking of retry attempts. Uses normalizer
|
|
282
|
+
to ensure consistent schema.
|
|
278
283
|
"""
|
|
279
284
|
try:
|
|
280
285
|
self.logger.debug(
|
|
281
286
|
f"🔄 Retrying {event.event_type} (attempt {event.attempt_count + 1}/{event.max_retries})"
|
|
282
287
|
)
|
|
283
288
|
|
|
284
|
-
# Reconstruct the
|
|
285
|
-
|
|
289
|
+
# Reconstruct the raw event
|
|
290
|
+
raw_event = {
|
|
286
291
|
"type": event.event_type,
|
|
287
292
|
"timestamp": datetime.now().isoformat(),
|
|
288
|
-
"data": event.data,
|
|
289
|
-
"retry_attempt": event.attempt_count + 1
|
|
293
|
+
"data": {**event.data, "retry_attempt": event.attempt_count + 1},
|
|
290
294
|
}
|
|
291
295
|
|
|
296
|
+
# Normalize the event
|
|
297
|
+
normalized = self.normalizer.normalize(raw_event)
|
|
298
|
+
full_event = normalized.to_dict()
|
|
299
|
+
|
|
292
300
|
# Attempt broadcast
|
|
293
301
|
if event.skip_sid:
|
|
294
302
|
await self.sio.emit("claude_event", full_event, skip_sid=event.skip_sid)
|
|
@@ -309,16 +317,22 @@ class SocketIOEventBroadcaster:
|
|
|
309
317
|
"""Broadcast an event to all connected clients with retry support.
|
|
310
318
|
|
|
311
319
|
WHY: Enhanced with retry queue to ensure reliable delivery
|
|
312
|
-
even during transient network issues.
|
|
320
|
+
even during transient network issues. Now uses EventNormalizer
|
|
321
|
+
to ensure consistent event schema.
|
|
313
322
|
"""
|
|
314
323
|
if not self.sio:
|
|
315
324
|
return
|
|
316
325
|
|
|
317
|
-
event
|
|
326
|
+
# Create raw event for normalization
|
|
327
|
+
raw_event = {
|
|
318
328
|
"type": event_type,
|
|
319
329
|
"timestamp": datetime.now().isoformat(),
|
|
320
330
|
"data": data,
|
|
321
331
|
}
|
|
332
|
+
|
|
333
|
+
# Normalize the event to consistent schema
|
|
334
|
+
normalized = self.normalizer.normalize(raw_event)
|
|
335
|
+
event = normalized.to_dict()
|
|
322
336
|
|
|
323
337
|
# Buffer the event for reliability AND add to event history for new clients
|
|
324
338
|
with self.buffer_lock:
|
|
@@ -329,7 +343,7 @@ class SocketIOEventBroadcaster:
|
|
|
329
343
|
# Access through server reference to maintain single history source
|
|
330
344
|
if hasattr(self, 'server') and hasattr(self.server, 'event_history'):
|
|
331
345
|
self.server.event_history.append(event)
|
|
332
|
-
self.logger.debug(f"Added {
|
|
346
|
+
self.logger.debug(f"Added {event['type']}/{event['subtype']} to history (total: {len(self.server.event_history)})")
|
|
333
347
|
|
|
334
348
|
# Broadcast to all connected clients
|
|
335
349
|
broadcast_success = False
|
|
@@ -521,29 +535,8 @@ class SocketIOEventBroadcaster:
|
|
|
521
535
|
|
|
522
536
|
WHY: System events are separate from hook events to provide
|
|
523
537
|
server health monitoring independent of Claude activity.
|
|
538
|
+
Now uses broadcast_event for consistency with buffering and normalization.
|
|
524
539
|
"""
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
# Create system event with consistent format
|
|
529
|
-
event = {
|
|
530
|
-
"type": "system",
|
|
531
|
-
"event": "heartbeat",
|
|
532
|
-
"timestamp": datetime.now().isoformat(),
|
|
533
|
-
"data": heartbeat_data,
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
# Broadcast to all connected clients
|
|
537
|
-
try:
|
|
538
|
-
if self.loop and not self.loop.is_closed():
|
|
539
|
-
future = asyncio.run_coroutine_threadsafe(
|
|
540
|
-
self.sio.emit("system_event", event), self.loop
|
|
541
|
-
)
|
|
542
|
-
self.logger.debug(
|
|
543
|
-
f"Broadcasted system heartbeat - clients: {len(self.connected_clients)}, "
|
|
544
|
-
f"uptime: {heartbeat_data.get('uptime_seconds', 0)}s"
|
|
545
|
-
)
|
|
546
|
-
else:
|
|
547
|
-
self.logger.warning("Cannot broadcast heartbeat: server loop not available")
|
|
548
|
-
except Exception as e:
|
|
549
|
-
self.logger.error(f"Failed to broadcast system heartbeat: {e}")
|
|
540
|
+
# Use the standard broadcast_event method which handles normalization,
|
|
541
|
+
# buffering, and retry logic
|
|
542
|
+
self.broadcast_event("heartbeat", heartbeat_data)
|
|
@@ -414,11 +414,12 @@ class SocketIOServerCore:
|
|
|
414
414
|
except Exception as e:
|
|
415
415
|
self.logger.debug(f"Could not get active sessions: {e}")
|
|
416
416
|
|
|
417
|
-
# Prepare heartbeat data
|
|
417
|
+
# Prepare heartbeat data (using new schema)
|
|
418
418
|
heartbeat_data = {
|
|
419
419
|
"type": "system",
|
|
420
|
-
"
|
|
420
|
+
"subtype": "heartbeat",
|
|
421
421
|
"timestamp": datetime.now().isoformat(),
|
|
422
|
+
"source": "server",
|
|
422
423
|
"data": {
|
|
423
424
|
"uptime_seconds": uptime_seconds,
|
|
424
425
|
"connected_clients": len(self.connected_clients),
|
|
@@ -435,7 +436,7 @@ class SocketIOServerCore:
|
|
|
435
436
|
if self.main_server and hasattr(self.main_server, 'event_history'):
|
|
436
437
|
self.main_server.event_history.append(heartbeat_data)
|
|
437
438
|
|
|
438
|
-
# Emit heartbeat to all connected clients
|
|
439
|
+
# Emit heartbeat to all connected clients (already using new schema)
|
|
439
440
|
await self.sio.emit("system_event", heartbeat_data)
|
|
440
441
|
|
|
441
442
|
self.logger.info(
|