claude-mpm 4.1.7__py3-none-any.whl → 4.1.10__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 (109) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/INSTRUCTIONS.md +26 -1
  3. claude_mpm/agents/OUTPUT_STYLE.md +73 -0
  4. claude_mpm/agents/agents_metadata.py +57 -0
  5. claude_mpm/agents/templates/.claude-mpm/memories/README.md +17 -0
  6. claude_mpm/agents/templates/.claude-mpm/memories/engineer_memories.md +3 -0
  7. claude_mpm/agents/templates/agent-manager.json +263 -17
  8. claude_mpm/agents/templates/agent-manager.md +248 -10
  9. claude_mpm/agents/templates/agentic_coder_optimizer.json +222 -0
  10. claude_mpm/agents/templates/code_analyzer.json +18 -8
  11. claude_mpm/agents/templates/engineer.json +1 -1
  12. claude_mpm/agents/templates/logs/prompts/agent_engineer_20250826_014258_728.md +39 -0
  13. claude_mpm/agents/templates/qa.json +1 -1
  14. claude_mpm/agents/templates/research.json +1 -1
  15. claude_mpm/cli/__init__.py +4 -0
  16. claude_mpm/cli/commands/__init__.py +6 -0
  17. claude_mpm/cli/commands/analyze.py +547 -0
  18. claude_mpm/cli/commands/analyze_code.py +524 -0
  19. claude_mpm/cli/commands/configure.py +223 -25
  20. claude_mpm/cli/commands/configure_tui.py +65 -61
  21. claude_mpm/cli/commands/debug.py +1387 -0
  22. claude_mpm/cli/parsers/analyze_code_parser.py +170 -0
  23. claude_mpm/cli/parsers/analyze_parser.py +135 -0
  24. claude_mpm/cli/parsers/base_parser.py +29 -0
  25. claude_mpm/cli/parsers/configure_parser.py +23 -0
  26. claude_mpm/cli/parsers/debug_parser.py +319 -0
  27. claude_mpm/config/socketio_config.py +21 -21
  28. claude_mpm/constants.py +3 -1
  29. claude_mpm/core/framework_loader.py +148 -6
  30. claude_mpm/core/log_manager.py +16 -13
  31. claude_mpm/core/logger.py +1 -1
  32. claude_mpm/core/unified_agent_registry.py +1 -1
  33. claude_mpm/dashboard/.claude-mpm/socketio-instances.json +1 -0
  34. claude_mpm/dashboard/analysis_runner.py +428 -0
  35. claude_mpm/dashboard/static/built/components/activity-tree.js +2 -0
  36. claude_mpm/dashboard/static/built/components/agent-inference.js +1 -1
  37. claude_mpm/dashboard/static/built/components/event-viewer.js +1 -1
  38. claude_mpm/dashboard/static/built/components/file-tool-tracker.js +1 -1
  39. claude_mpm/dashboard/static/built/components/module-viewer.js +1 -1
  40. claude_mpm/dashboard/static/built/components/session-manager.js +1 -1
  41. claude_mpm/dashboard/static/built/components/working-directory.js +1 -1
  42. claude_mpm/dashboard/static/built/dashboard.js +1 -1
  43. claude_mpm/dashboard/static/built/socket-client.js +1 -1
  44. claude_mpm/dashboard/static/css/activity.css +549 -0
  45. claude_mpm/dashboard/static/css/code-tree.css +846 -0
  46. claude_mpm/dashboard/static/css/dashboard.css +245 -0
  47. claude_mpm/dashboard/static/dist/components/activity-tree.js +2 -0
  48. claude_mpm/dashboard/static/dist/components/code-tree.js +2 -0
  49. claude_mpm/dashboard/static/dist/components/code-viewer.js +2 -0
  50. claude_mpm/dashboard/static/dist/components/event-viewer.js +1 -1
  51. claude_mpm/dashboard/static/dist/components/session-manager.js +1 -1
  52. claude_mpm/dashboard/static/dist/components/working-directory.js +1 -1
  53. claude_mpm/dashboard/static/dist/dashboard.js +1 -1
  54. claude_mpm/dashboard/static/dist/socket-client.js +1 -1
  55. claude_mpm/dashboard/static/js/components/activity-tree.js +1139 -0
  56. claude_mpm/dashboard/static/js/components/code-tree.js +1357 -0
  57. claude_mpm/dashboard/static/js/components/code-viewer.js +480 -0
  58. claude_mpm/dashboard/static/js/components/event-viewer.js +11 -0
  59. claude_mpm/dashboard/static/js/components/session-manager.js +40 -4
  60. claude_mpm/dashboard/static/js/components/socket-manager.js +12 -0
  61. claude_mpm/dashboard/static/js/components/ui-state-manager.js +4 -0
  62. claude_mpm/dashboard/static/js/components/working-directory.js +17 -1
  63. claude_mpm/dashboard/static/js/dashboard.js +39 -0
  64. claude_mpm/dashboard/static/js/socket-client.js +414 -20
  65. claude_mpm/dashboard/templates/index.html +184 -4
  66. claude_mpm/hooks/claude_hooks/hook_handler.py +182 -5
  67. claude_mpm/hooks/claude_hooks/installer.py +728 -0
  68. claude_mpm/scripts/claude-hook-handler.sh +161 -0
  69. claude_mpm/scripts/socketio_daemon.py +121 -8
  70. claude_mpm/services/agents/deployment/agent_config_provider.py +127 -27
  71. claude_mpm/services/agents/deployment/agent_lifecycle_manager_refactored.py +2 -2
  72. claude_mpm/services/agents/deployment/agent_record_service.py +1 -2
  73. claude_mpm/services/agents/memory/memory_format_service.py +1 -5
  74. claude_mpm/services/cli/agent_cleanup_service.py +1 -2
  75. claude_mpm/services/cli/agent_dependency_service.py +1 -1
  76. claude_mpm/services/cli/agent_validation_service.py +3 -4
  77. claude_mpm/services/cli/dashboard_launcher.py +2 -3
  78. claude_mpm/services/cli/startup_checker.py +0 -10
  79. claude_mpm/services/core/cache_manager.py +1 -2
  80. claude_mpm/services/core/path_resolver.py +1 -4
  81. claude_mpm/services/core/service_container.py +2 -2
  82. claude_mpm/services/diagnostics/checks/instructions_check.py +2 -5
  83. claude_mpm/services/event_bus/direct_relay.py +98 -20
  84. claude_mpm/services/infrastructure/monitoring/__init__.py +11 -11
  85. claude_mpm/services/infrastructure/monitoring.py +11 -11
  86. claude_mpm/services/project/architecture_analyzer.py +1 -1
  87. claude_mpm/services/project/dependency_analyzer.py +4 -4
  88. claude_mpm/services/project/language_analyzer.py +3 -3
  89. claude_mpm/services/project/metrics_collector.py +3 -6
  90. claude_mpm/services/socketio/handlers/__init__.py +2 -0
  91. claude_mpm/services/socketio/handlers/code_analysis.py +170 -0
  92. claude_mpm/services/socketio/handlers/registry.py +2 -0
  93. claude_mpm/services/socketio/server/connection_manager.py +95 -65
  94. claude_mpm/services/socketio/server/core.py +125 -17
  95. claude_mpm/services/socketio/server/main.py +44 -5
  96. claude_mpm/services/visualization/__init__.py +19 -0
  97. claude_mpm/services/visualization/mermaid_generator.py +938 -0
  98. claude_mpm/tools/__main__.py +208 -0
  99. claude_mpm/tools/code_tree_analyzer.py +778 -0
  100. claude_mpm/tools/code_tree_builder.py +632 -0
  101. claude_mpm/tools/code_tree_events.py +318 -0
  102. claude_mpm/tools/socketio_debug.py +671 -0
  103. {claude_mpm-4.1.7.dist-info → claude_mpm-4.1.10.dist-info}/METADATA +1 -1
  104. {claude_mpm-4.1.7.dist-info → claude_mpm-4.1.10.dist-info}/RECORD +108 -77
  105. claude_mpm/agents/schema/agent_schema.json +0 -314
  106. {claude_mpm-4.1.7.dist-info → claude_mpm-4.1.10.dist-info}/WHEEL +0 -0
  107. {claude_mpm-4.1.7.dist-info → claude_mpm-4.1.10.dist-info}/entry_points.txt +0 -0
  108. {claude_mpm-4.1.7.dist-info → claude_mpm-4.1.10.dist-info}/licenses/LICENSE +0 -0
  109. {claude_mpm-4.1.7.dist-info → claude_mpm-4.1.10.dist-info}/top_level.txt +0 -0
@@ -10,6 +10,7 @@ of client states, proper event delivery, and automatic recovery mechanisms.
10
10
  """
11
11
 
12
12
  import asyncio
13
+ import contextlib
13
14
  import time
14
15
  from collections import deque
15
16
  from dataclasses import dataclass, field
@@ -70,11 +71,11 @@ class ClientConnection:
70
71
  metrics: ConnectionMetrics = field(default_factory=ConnectionMetrics)
71
72
  metadata: Dict[str, Any] = field(default_factory=dict)
72
73
 
73
- def is_healthy(self, timeout: float = 180.0) -> bool:
74
+ def is_healthy(self, timeout: float = 90.0) -> bool:
74
75
  """Check if connection is healthy based on activity.
75
-
76
+
76
77
  Args:
77
- timeout: Seconds before considering connection unhealthy (default 180s)
78
+ timeout: Seconds before considering connection unhealthy (default 90s)
78
79
  """
79
80
  if self.state != ConnectionState.CONNECTED:
80
81
  return False
@@ -91,9 +92,9 @@ class ClientConnection:
91
92
  self.connected_at,
92
93
  )
93
94
 
94
- # Add grace period for network hiccups (additional 10% of timeout)
95
- grace_period = timeout * 1.1
96
- return (now - last_activity) < grace_period
95
+ # More aggressive timeout for stale detection (no grace period)
96
+ # This helps identify truly stale connections faster
97
+ return (now - last_activity) < timeout
97
98
 
98
99
  def calculate_quality(self) -> float:
99
100
  """Calculate connection quality score (0-1)."""
@@ -148,7 +149,7 @@ class ConnectionManager:
148
149
  - Automatic event replay on reconnection
149
150
  """
150
151
 
151
- def __init__(self, max_buffer_size: int = None, event_ttl: int = None):
152
+ def __init__(self, max_buffer_size: Optional[int] = None, event_ttl: Optional[int] = None):
152
153
  """
153
154
  Initialize connection manager with centralized configuration.
154
155
 
@@ -157,17 +158,19 @@ class ConnectionManager:
157
158
  event_ttl: Time-to-live for buffered events in seconds (uses config if None)
158
159
  """
159
160
  from ....config.socketio_config import CONNECTION_CONFIG
160
-
161
+
161
162
  self.logger = get_logger(__name__)
162
163
  self.connections: Dict[str, ClientConnection] = {}
163
164
  self.client_mapping: Dict[str, str] = {} # client_id -> current sid
164
-
165
+
165
166
  # Use centralized configuration with optional overrides
166
- self.max_buffer_size = max_buffer_size or CONNECTION_CONFIG['max_events_buffer']
167
- self.event_ttl = event_ttl or CONNECTION_CONFIG['event_ttl']
167
+ self.max_buffer_size = max_buffer_size or CONNECTION_CONFIG["max_events_buffer"]
168
+ self.event_ttl = event_ttl or CONNECTION_CONFIG["event_ttl"]
168
169
  self.global_sequence = 0
169
- self.health_check_interval = CONNECTION_CONFIG['health_check_interval'] # 30 seconds
170
- self.stale_timeout = CONNECTION_CONFIG['stale_timeout'] # 180 seconds (was 90)
170
+ self.health_check_interval = CONNECTION_CONFIG[
171
+ "health_check_interval"
172
+ ] # 30 seconds
173
+ self.stale_timeout = CONNECTION_CONFIG["stale_timeout"] # 180 seconds (was 90)
171
174
  self.health_task = None
172
175
  self._lock = asyncio.Lock()
173
176
 
@@ -175,7 +178,7 @@ class ConnectionManager:
175
178
  self, sid: str, client_id: Optional[str] = None
176
179
  ) -> ClientConnection:
177
180
  """
178
- Register a new connection or reconnection.
181
+ Register a new connection or reconnection with retry logic.
179
182
 
180
183
  Args:
181
184
  sid: Socket ID
@@ -184,54 +187,84 @@ class ConnectionManager:
184
187
  Returns:
185
188
  ClientConnection object
186
189
  """
187
- async with self._lock:
188
- now = time.time()
189
-
190
- # Check if this is a reconnection
191
- if client_id and client_id in self.client_mapping:
192
- old_sid = self.client_mapping[client_id]
193
- if old_sid in self.connections:
194
- old_conn = self.connections[old_sid]
190
+ max_retries = 3
191
+ retry_delay = 0.1 # Start with 100ms
195
192
 
196
- # Create new connection with history
197
- conn = ClientConnection(
198
- sid=sid,
199
- client_id=client_id,
200
- state=ConnectionState.CONNECTED,
201
- connected_at=now,
202
- event_buffer=old_conn.event_buffer,
203
- event_sequence=old_conn.event_sequence,
204
- last_acked_sequence=old_conn.last_acked_sequence,
205
- metrics=old_conn.metrics,
206
- )
193
+ for attempt in range(max_retries):
194
+ try:
195
+ async with self._lock:
196
+ now = time.time()
197
+
198
+ # Check if this is a reconnection
199
+ if client_id and client_id in self.client_mapping:
200
+ old_sid = self.client_mapping[client_id]
201
+ if old_sid in self.connections:
202
+ old_conn = self.connections[old_sid]
203
+
204
+ # Create new connection with history
205
+ conn = ClientConnection(
206
+ sid=sid,
207
+ client_id=client_id,
208
+ state=ConnectionState.CONNECTED,
209
+ connected_at=now,
210
+ event_buffer=old_conn.event_buffer,
211
+ event_sequence=old_conn.event_sequence,
212
+ last_acked_sequence=old_conn.last_acked_sequence,
213
+ metrics=old_conn.metrics,
214
+ )
207
215
 
208
- # Update metrics
209
- conn.metrics.reconnect_count += 1
210
- conn.metrics.connect_count += 1
211
- if old_conn.disconnected_at:
212
- conn.metrics.total_downtime += now - old_conn.disconnected_at
216
+ # Update metrics
217
+ conn.metrics.reconnect_count += 1
218
+ conn.metrics.connect_count += 1
219
+ if old_conn.disconnected_at:
220
+ conn.metrics.total_downtime += (
221
+ now - old_conn.disconnected_at
222
+ )
213
223
 
214
- # Clean up old connection
215
- del self.connections[old_sid]
224
+ # Clean up old connection
225
+ del self.connections[old_sid]
216
226
 
217
- self.logger.info(
218
- f"Client {client_id} reconnected (new sid: {sid}, "
219
- f"buffered events: {len(conn.event_buffer)})"
220
- )
221
- else:
222
- # No old connection found, create new
223
- client_id = client_id or str(uuid4())
224
- conn = self._create_new_connection(sid, client_id, now)
225
- else:
226
- # New client
227
- client_id = client_id or str(uuid4())
228
- conn = self._create_new_connection(sid, client_id, now)
227
+ self.logger.info(
228
+ f"Client {client_id} reconnected (new sid: {sid}, "
229
+ f"buffered events: {len(conn.event_buffer)})"
230
+ )
231
+ else:
232
+ # No old connection found, create new
233
+ client_id = client_id or str(uuid4())
234
+ conn = self._create_new_connection(sid, client_id, now)
235
+ else:
236
+ # New client
237
+ client_id = client_id or str(uuid4())
238
+ conn = self._create_new_connection(sid, client_id, now)
229
239
 
230
- # Register connection
231
- self.connections[sid] = conn
232
- self.client_mapping[client_id] = sid
240
+ # Register connection with validation
241
+ if conn and conn.state == ConnectionState.CONNECTED:
242
+ self.connections[sid] = conn
243
+ self.client_mapping[client_id] = sid
244
+ return conn
245
+ raise ValueError(f"Invalid connection state for {sid}")
233
246
 
234
- return conn
247
+ except Exception as e:
248
+ self.logger.warning(
249
+ f"Failed to register connection {sid} (attempt {attempt + 1}/{max_retries}): {e}"
250
+ )
251
+ if attempt < max_retries - 1:
252
+ await asyncio.sleep(retry_delay)
253
+ retry_delay *= 2 # Exponential backoff
254
+ else:
255
+ # Final attempt failed, create minimal connection
256
+ self.logger.error(
257
+ f"All attempts failed for {sid}, creating minimal connection"
258
+ )
259
+ conn = ClientConnection(
260
+ sid=sid,
261
+ client_id=client_id or str(uuid4()),
262
+ state=ConnectionState.CONNECTED,
263
+ connected_at=time.time(),
264
+ )
265
+ self.connections[sid] = conn
266
+ return conn
267
+ return None
235
268
 
236
269
  def _create_new_connection(
237
270
  self, sid: str, client_id: str, now: float
@@ -403,10 +436,8 @@ class ConnectionManager:
403
436
  """Stop the health monitoring task."""
404
437
  if self.health_task:
405
438
  self.health_task.cancel()
406
- try:
439
+ with contextlib.suppress(asyncio.CancelledError):
407
440
  await self.health_task
408
- except asyncio.CancelledError:
409
- pass
410
441
  self.health_task = None
411
442
  self.logger.info("Stopped connection health monitoring")
412
443
 
@@ -460,7 +491,7 @@ class ConnectionManager:
460
491
  conn.connected_at,
461
492
  )
462
493
  time_since_activity = now - last_activity
463
-
494
+
464
495
  # Only mark as stale if significantly over timeout (2x)
465
496
  if time_since_activity > (self.stale_timeout * 2):
466
497
  conn.state = ConnectionState.STALE
@@ -476,15 +507,14 @@ class ConnectionManager:
476
507
  f"Connection {conn.client_id} borderline "
477
508
  f"(last activity: {time_since_activity:.1f}s ago)"
478
509
  )
479
-
510
+
480
511
  elif conn.state == ConnectionState.DISCONNECTED:
481
512
  report["disconnected"] += 1
482
513
 
483
514
  # Clean up old disconnected connections (be conservative)
484
- if (
485
- conn.disconnected_at
486
- and (now - conn.disconnected_at) > (self.event_ttl * 2) # Double the TTL
487
- ):
515
+ if conn.disconnected_at and (now - conn.disconnected_at) > (
516
+ self.event_ttl * 2
517
+ ): # Double the TTL
488
518
  to_clean.append(sid)
489
519
 
490
520
  # Clean up old connections
@@ -160,22 +160,38 @@ class SocketIOServerCore:
160
160
  try:
161
161
  # Import centralized configuration for consistency
162
162
  from ....config.socketio_config import CONNECTION_CONFIG
163
-
163
+
164
164
  # Create Socket.IO server with centralized configuration
165
165
  # CRITICAL: These values MUST match client settings to prevent disconnections
166
166
  self.sio = socketio.AsyncServer(
167
167
  cors_allowed_origins="*",
168
168
  logger=False, # Disable Socket.IO's own logging
169
169
  engineio_logger=False,
170
- ping_interval=CONNECTION_CONFIG['ping_interval'], # 45 seconds from config
171
- ping_timeout=CONNECTION_CONFIG['ping_timeout'], # 20 seconds from config
172
- max_http_buffer_size=CONNECTION_CONFIG['max_http_buffer_size'], # 100MB from config
170
+ ping_interval=CONNECTION_CONFIG[
171
+ "ping_interval"
172
+ ], # 45 seconds from config
173
+ ping_timeout=CONNECTION_CONFIG[
174
+ "ping_timeout"
175
+ ], # 20 seconds from config
176
+ max_http_buffer_size=CONNECTION_CONFIG[
177
+ "max_http_buffer_size"
178
+ ], # 100MB from config
173
179
  )
174
180
 
175
181
  # Create aiohttp application
176
182
  self.app = web.Application()
177
183
  self.sio.attach(self.app)
178
184
 
185
+ # CRITICAL: Register event handlers BEFORE starting the server
186
+ # This ensures handlers are ready when clients connect
187
+ if self.main_server and hasattr(self.main_server, "_register_events_async"):
188
+ self.logger.info(
189
+ "Registering Socket.IO event handlers before server start"
190
+ )
191
+ await self.main_server._register_events_async()
192
+ else:
193
+ self.logger.warning("Main server not available for event registration")
194
+
179
195
  # Setup HTTP API endpoints for receiving events from hook handlers
180
196
  self._setup_http_api()
181
197
 
@@ -202,11 +218,14 @@ class SocketIOServerCore:
202
218
 
203
219
  # Conditionally start heartbeat task based on configuration
204
220
  from ....config.socketio_config import CONNECTION_CONFIG
205
- if CONNECTION_CONFIG.get('enable_extra_heartbeat', False):
221
+
222
+ if CONNECTION_CONFIG.get("enable_extra_heartbeat", False):
206
223
  self.heartbeat_task = asyncio.create_task(self._heartbeat_loop())
207
224
  self.logger.info("Started system heartbeat task")
208
225
  else:
209
- self.logger.info("System heartbeat disabled (using Socket.IO ping/pong instead)")
226
+ self.logger.info(
227
+ "System heartbeat disabled (using Socket.IO ping/pong instead)"
228
+ )
210
229
 
211
230
  # Keep the server running
212
231
  while self.running:
@@ -254,24 +273,113 @@ class SocketIOServerCore:
254
273
  # Parse JSON payload
255
274
  event_data = await request.json()
256
275
 
257
- # Log receipt if debugging
258
- event_type = event_data.get("subtype", "unknown")
259
- self.logger.debug(f"Received HTTP event: {event_type}")
276
+ # Log receipt with more detail
277
+ event_type = (
278
+ event_data.get("subtype")
279
+ or event_data.get("hook_event_name")
280
+ or "unknown"
281
+ )
282
+ self.logger.info(f"📨 Received HTTP event: {event_type}")
283
+ self.logger.debug(f"Event data keys: {list(event_data.keys())}")
284
+ self.logger.debug(f"Connected clients: {len(self.connected_clients)}")
285
+
286
+ # Transform hook event format to claude_event format if needed
287
+ if "hook_event_name" in event_data and "event" not in event_data:
288
+ # This is a raw hook event, transform it
289
+ from claude_mpm.services.socketio.event_normalizer import (
290
+ EventNormalizer,
291
+ )
292
+
293
+ normalizer = EventNormalizer()
294
+
295
+ # Create the format expected by normalizer
296
+ raw_event = {
297
+ "type": "hook",
298
+ "subtype": event_data.get("hook_event_name", "unknown")
299
+ .lower()
300
+ .replace("submit", "")
301
+ .replace("use", "_use"),
302
+ "timestamp": event_data.get("timestamp"),
303
+ "data": event_data.get("hook_input_data", {}),
304
+ "source": "claude_hooks",
305
+ "session_id": event_data.get("session_id"),
306
+ }
307
+
308
+ # Map hook event names to dashboard subtypes
309
+ subtype_map = {
310
+ "UserPromptSubmit": "user_prompt",
311
+ "PreToolUse": "pre_tool",
312
+ "PostToolUse": "post_tool",
313
+ "Stop": "stop",
314
+ "SubagentStop": "subagent_stop",
315
+ "AssistantResponse": "assistant_response",
316
+ }
317
+ raw_event["subtype"] = subtype_map.get(
318
+ event_data.get("hook_event_name"), "unknown"
319
+ )
320
+
321
+ normalized = normalizer.normalize(raw_event, source="hook")
322
+ event_data = normalized.to_dict()
323
+ self.logger.debug(
324
+ f"Normalized event: type={event_data.get('type')}, subtype={event_data.get('subtype')}"
325
+ )
260
326
 
261
327
  # Broadcast to all connected dashboard clients via SocketIO
262
328
  if self.sio:
263
- # The event is already in claude_event format from the hook handler
264
- await self.sio.emit("claude_event", event_data)
329
+ # CRITICAL: Use the main server's broadcaster for proper event handling
330
+ # The broadcaster handles retries, connection management, and buffering
331
+ if (
332
+ self.main_server
333
+ and hasattr(self.main_server, "broadcaster")
334
+ and self.main_server.broadcaster
335
+ ):
336
+ # The broadcaster expects raw event data and will normalize it
337
+ # Since we already normalized it, we need to pass it in a way that won't double-normalize
338
+ # We'll emit directly through the broadcaster's sio with proper handling
339
+
340
+ # Add to event buffer and history
341
+ with self.buffer_lock:
342
+ self.event_buffer.append(event_data)
343
+ self.stats["events_buffered"] = len(self.event_buffer)
344
+
345
+ # Add to main server's event history
346
+ if hasattr(self.main_server, "event_history"):
347
+ self.main_server.event_history.append(event_data)
348
+
349
+ # Use the broadcaster's sio to emit (it's the same as self.sio)
350
+ # This ensures the event goes through the proper channels
351
+ await self.sio.emit("claude_event", event_data)
352
+
353
+ # Update broadcaster stats
354
+ if hasattr(self.main_server.broadcaster, "stats"):
355
+ self.main_server.broadcaster.stats["events_sent"] = (
356
+ self.main_server.broadcaster.stats.get("events_sent", 0)
357
+ + 1
358
+ )
359
+
360
+ self.logger.info(
361
+ f"✅ Event broadcasted: {event_data.get('subtype', 'unknown')} to {len(self.connected_clients)} clients"
362
+ )
363
+ self.logger.debug(
364
+ f"Connected client IDs: {list(self.connected_clients) if self.connected_clients else 'None'}"
365
+ )
366
+ else:
367
+ # Fallback: Direct emit if broadcaster not available (shouldn't happen)
368
+ self.logger.warning(
369
+ "Broadcaster not available, using direct emit"
370
+ )
371
+ await self.sio.emit("claude_event", event_data)
265
372
 
266
- # Update stats
267
- self.stats["events_sent"] = self.stats.get("events_sent", 0) + 1
373
+ # Update stats manually if using fallback
374
+ self.stats["events_sent"] = self.stats.get("events_sent", 0) + 1
268
375
 
269
- # Add to event buffer for late-joining clients
270
- with self.buffer_lock:
271
- self.event_buffer.append(event_data)
272
- self.stats["events_buffered"] = len(self.event_buffer)
376
+ # Add to event buffer for late-joining clients
377
+ with self.buffer_lock:
378
+ self.event_buffer.append(event_data)
379
+ self.stats["events_buffered"] = len(self.event_buffer)
273
380
 
274
381
  # Return 204 No Content for success
382
+ self.logger.debug(f"✅ HTTP event processed successfully: {event_type}")
275
383
  return web.Response(status=204)
276
384
 
277
385
  except Exception as e:
@@ -111,7 +111,8 @@ class SocketIOServer(SocketIOServiceInterface):
111
111
  flush=True,
112
112
  )
113
113
 
114
- # Start the core server
114
+ # CRITICAL: Start the core server first to create sio instance
115
+ # Event handlers will be registered inside _start_server before accepting connections
115
116
  self.core.start_sync()
116
117
 
117
118
  # Initialize connection manager for robust connection tracking
@@ -163,8 +164,9 @@ class SocketIOServer(SocketIOServiceInterface):
163
164
  self.connection_manager.start_health_monitoring(), self.core.loop
164
165
  )
165
166
 
166
- # Register events
167
- self._register_events()
167
+ # Register events if not already done in async context
168
+ if self.core.sio and not self.event_registry:
169
+ self._register_events()
168
170
 
169
171
  # Setup EventBus integration
170
172
  # WHY: This connects the EventBus to the Socket.IO server, allowing
@@ -263,10 +265,16 @@ class SocketIOServer(SocketIOServiceInterface):
263
265
  except Exception as e:
264
266
  self.logger.error(f"Error during EventBus teardown: {e}")
265
267
 
266
- # Stop health monitoring in connection handler
268
+ # Stop code analysis handler
267
269
  if self.event_registry:
268
- from ..handlers import ConnectionEventHandler
270
+ from ..handlers import CodeAnalysisEventHandler, ConnectionEventHandler
269
271
 
272
+ # Stop analysis runner
273
+ analysis_handler = self.event_registry.get_handler(CodeAnalysisEventHandler)
274
+ if analysis_handler and hasattr(analysis_handler, "cleanup"):
275
+ analysis_handler.cleanup()
276
+
277
+ # Stop health monitoring in connection handler
270
278
  conn_handler = self.event_registry.get_handler(ConnectionEventHandler)
271
279
  if conn_handler and hasattr(conn_handler, "stop_health_monitoring"):
272
280
  conn_handler.stop_health_monitoring()
@@ -281,6 +289,12 @@ class SocketIOServer(SocketIOServiceInterface):
281
289
  handlers in a modular way. Each handler focuses on a specific domain,
282
290
  reducing complexity and improving maintainability.
283
291
  """
292
+ if not self.core.sio:
293
+ self.logger.error(
294
+ "Cannot register events - Socket.IO server not initialized"
295
+ )
296
+ return
297
+
284
298
  # Initialize the event handler registry
285
299
  self.event_registry = EventHandlerRegistry(self)
286
300
  self.event_registry.initialize()
@@ -294,6 +308,31 @@ class SocketIOServer(SocketIOServiceInterface):
294
308
 
295
309
  self.logger.info("All Socket.IO events registered via handler system")
296
310
 
311
+ async def _register_events_async(self):
312
+ """Async version of event registration for calling from async context.
313
+
314
+ WHY: This allows us to register events from within the async _start_server
315
+ method before the server starts accepting connections.
316
+ """
317
+ if not self.core.sio:
318
+ self.logger.error(
319
+ "Cannot register events - Socket.IO server not initialized"
320
+ )
321
+ return
322
+
323
+ # Initialize the event handler registry
324
+ self.event_registry = EventHandlerRegistry(self)
325
+ self.event_registry.initialize()
326
+
327
+ # Register all events from all handlers
328
+ self.event_registry.register_all_events()
329
+
330
+ # Keep handler instances for HTTP endpoint compatibility
331
+ self.file_handler = self.event_registry.get_handler(FileEventHandler)
332
+ self.git_handler = self.event_registry.get_handler(GitEventHandler)
333
+
334
+ self.logger.info("All Socket.IO events registered via handler system (async)")
335
+
297
336
  # Delegate broadcasting methods to the broadcaster
298
337
  def broadcast_event(self, event_type: str, data: Dict[str, Any]):
299
338
  """Broadcast an event to all connected clients."""
@@ -0,0 +1,19 @@
1
+ """
2
+ Visualization Services for Claude MPM
3
+ =====================================
4
+
5
+ This module provides visualization services for code analysis,
6
+ including Mermaid diagram generation for various code structures.
7
+ """
8
+
9
+ from .mermaid_generator import (
10
+ DiagramConfig,
11
+ DiagramType,
12
+ MermaidGeneratorService,
13
+ )
14
+
15
+ __all__ = [
16
+ "DiagramConfig",
17
+ "DiagramType",
18
+ "MermaidGeneratorService",
19
+ ]