claude-mpm 3.1.3__py3-none-any.whl → 3.2.1__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 (79) hide show
  1. claude_mpm/__init__.py +3 -3
  2. claude_mpm/__main__.py +0 -17
  3. claude_mpm/agents/INSTRUCTIONS.md +81 -18
  4. claude_mpm/agents/backups/INSTRUCTIONS.md +238 -0
  5. claude_mpm/agents/base_agent.json +1 -1
  6. claude_mpm/agents/templates/pm.json +25 -0
  7. claude_mpm/agents/templates/research.json +2 -1
  8. claude_mpm/cli/__init__.py +19 -23
  9. claude_mpm/cli/commands/__init__.py +3 -1
  10. claude_mpm/cli/commands/agents.py +7 -18
  11. claude_mpm/cli/commands/info.py +5 -10
  12. claude_mpm/cli/commands/memory.py +232 -0
  13. claude_mpm/cli/commands/run.py +501 -28
  14. claude_mpm/cli/commands/tickets.py +10 -17
  15. claude_mpm/cli/commands/ui.py +15 -37
  16. claude_mpm/cli/parser.py +91 -1
  17. claude_mpm/cli/utils.py +9 -28
  18. claude_mpm/config/socketio_config.py +256 -0
  19. claude_mpm/constants.py +9 -0
  20. claude_mpm/core/__init__.py +2 -2
  21. claude_mpm/core/agent_registry.py +4 -4
  22. claude_mpm/core/claude_runner.py +919 -0
  23. claude_mpm/core/config.py +21 -1
  24. claude_mpm/core/factories.py +1 -1
  25. claude_mpm/core/hook_manager.py +196 -0
  26. claude_mpm/core/pm_hook_interceptor.py +205 -0
  27. claude_mpm/core/service_registry.py +1 -1
  28. claude_mpm/core/simple_runner.py +323 -33
  29. claude_mpm/core/socketio_pool.py +582 -0
  30. claude_mpm/core/websocket_handler.py +233 -0
  31. claude_mpm/deployment_paths.py +261 -0
  32. claude_mpm/hooks/builtin/memory_hooks_example.py +67 -0
  33. claude_mpm/hooks/claude_hooks/hook_handler.py +667 -679
  34. claude_mpm/hooks/claude_hooks/hook_wrapper.sh +9 -4
  35. claude_mpm/hooks/memory_integration_hook.py +312 -0
  36. claude_mpm/models/__init__.py +9 -91
  37. claude_mpm/orchestration/__init__.py +1 -1
  38. claude_mpm/scripts/claude-mpm-socketio +32 -0
  39. claude_mpm/scripts/claude_mpm_monitor.html +567 -0
  40. claude_mpm/scripts/install_socketio_server.py +407 -0
  41. claude_mpm/scripts/launch_monitor.py +132 -0
  42. claude_mpm/scripts/manage_version.py +479 -0
  43. claude_mpm/scripts/socketio_daemon.py +181 -0
  44. claude_mpm/scripts/socketio_server_manager.py +428 -0
  45. claude_mpm/services/__init__.py +5 -0
  46. claude_mpm/services/agent_lifecycle_manager.py +76 -25
  47. claude_mpm/services/agent_memory_manager.py +684 -0
  48. claude_mpm/services/agent_modification_tracker.py +98 -17
  49. claude_mpm/services/agent_persistence_service.py +33 -13
  50. claude_mpm/services/agent_registry.py +82 -43
  51. claude_mpm/services/hook_service.py +362 -0
  52. claude_mpm/services/socketio_client_manager.py +474 -0
  53. claude_mpm/services/socketio_server.py +698 -0
  54. claude_mpm/services/standalone_socketio_server.py +631 -0
  55. claude_mpm/services/ticket_manager.py +4 -5
  56. claude_mpm/services/{ticket_manager_dependency_injection.py → ticket_manager_di.py} +12 -39
  57. claude_mpm/services/{legacy_ticketing_service.py → ticketing_service_original.py} +9 -16
  58. claude_mpm/services/version_control/semantic_versioning.py +9 -10
  59. claude_mpm/services/websocket_server.py +376 -0
  60. claude_mpm/utils/dependency_manager.py +211 -0
  61. claude_mpm/utils/import_migration_example.py +80 -0
  62. claude_mpm/utils/path_operations.py +0 -20
  63. claude_mpm/web/open_dashboard.py +34 -0
  64. {claude_mpm-3.1.3.dist-info → claude_mpm-3.2.1.dist-info}/METADATA +20 -9
  65. {claude_mpm-3.1.3.dist-info → claude_mpm-3.2.1.dist-info}/RECORD +70 -50
  66. claude_mpm-3.2.1.dist-info/entry_points.txt +7 -0
  67. claude_mpm/cli_old.py +0 -728
  68. claude_mpm/models/common.py +0 -41
  69. claude_mpm/models/lifecycle.py +0 -97
  70. claude_mpm/models/modification.py +0 -126
  71. claude_mpm/models/persistence.py +0 -57
  72. claude_mpm/models/registry.py +0 -91
  73. claude_mpm/security/__init__.py +0 -8
  74. claude_mpm/security/bash_validator.py +0 -393
  75. claude_mpm-3.1.3.dist-info/entry_points.txt +0 -4
  76. /claude_mpm/{cli_enhancements.py → experimental/cli_enhancements.py} +0 -0
  77. {claude_mpm-3.1.3.dist-info → claude_mpm-3.2.1.dist-info}/WHEEL +0 -0
  78. {claude_mpm-3.1.3.dist-info → claude_mpm-3.2.1.dist-info}/licenses/LICENSE +0 -0
  79. {claude_mpm-3.1.3.dist-info → claude_mpm-3.2.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,698 @@
1
+ """Socket.IO server for real-time monitoring of Claude MPM sessions.
2
+
3
+ WHY: This provides a Socket.IO-based alternative to the WebSocket server,
4
+ offering improved connection reliability and automatic reconnection.
5
+ Socket.IO handles connection drops gracefully and provides better
6
+ cross-platform compatibility.
7
+ """
8
+
9
+ import asyncio
10
+ import json
11
+ import logging
12
+ import os
13
+ import threading
14
+ import time
15
+ from datetime import datetime
16
+ from typing import Set, Dict, Any, Optional, List
17
+ from collections import deque
18
+
19
+ try:
20
+ import socketio
21
+ import aiohttp
22
+ from aiohttp import web
23
+ SOCKETIO_AVAILABLE = True
24
+ try:
25
+ version = getattr(socketio, '__version__', 'unknown')
26
+ print(f"Socket.IO server using python-socketio v{version}")
27
+ except:
28
+ print("Socket.IO server using python-socketio (version unavailable)")
29
+ except ImportError:
30
+ SOCKETIO_AVAILABLE = False
31
+ socketio = None
32
+ aiohttp = None
33
+ web = None
34
+ print("WARNING: python-socketio or aiohttp package not available")
35
+
36
+ from ..core.logger import get_logger
37
+ from ..deployment_paths import get_project_root, get_scripts_dir
38
+
39
+
40
+ class SocketIOClientProxy:
41
+ """Proxy that connects to an existing Socket.IO server as a client.
42
+
43
+ WHY: In exec mode, a persistent Socket.IO server runs in a separate process.
44
+ The hook handler in the Claude process needs a Socket.IO-like interface
45
+ but shouldn't start another server. This proxy provides that interface
46
+ while the actual events are handled by the persistent server.
47
+ """
48
+
49
+ def __init__(self, host: str = "localhost", port: int = 8765):
50
+ self.host = host
51
+ self.port = port
52
+ self.logger = get_logger("socketio_client_proxy")
53
+ self.running = True # Always "running" for compatibility
54
+ self._sio_client = None
55
+ self._client_thread = None
56
+
57
+ def start(self):
58
+ """Start the Socket.IO client connection to the persistent server."""
59
+ self.logger.debug(f"SocketIOClientProxy: Connecting to server on {self.host}:{self.port}")
60
+ if SOCKETIO_AVAILABLE:
61
+ self._start_client()
62
+
63
+ def stop(self):
64
+ """Stop the Socket.IO client connection."""
65
+ self.logger.debug(f"SocketIOClientProxy: Disconnecting from server")
66
+ if self._sio_client:
67
+ self._sio_client.disconnect()
68
+
69
+ def _start_client(self):
70
+ """Start Socket.IO client in a background thread."""
71
+ def run_client():
72
+ loop = asyncio.new_event_loop()
73
+ asyncio.set_event_loop(loop)
74
+ try:
75
+ loop.run_until_complete(self._connect_and_run())
76
+ except Exception as e:
77
+ self.logger.error(f"SocketIOClientProxy client thread error: {e}")
78
+
79
+ self._client_thread = threading.Thread(target=run_client, daemon=True)
80
+ self._client_thread.start()
81
+ # Give it a moment to connect
82
+ time.sleep(0.2)
83
+
84
+ async def _connect_and_run(self):
85
+ """Connect to the persistent Socket.IO server and keep connection alive."""
86
+ try:
87
+ self._sio_client = socketio.AsyncClient()
88
+
89
+ @self._sio_client.event
90
+ async def connect():
91
+ self.logger.info(f"SocketIOClientProxy: Connected to server at http://{self.host}:{self.port}")
92
+
93
+ @self._sio_client.event
94
+ async def disconnect():
95
+ self.logger.info(f"SocketIOClientProxy: Disconnected from server")
96
+
97
+ # Connect to the server
98
+ await self._sio_client.connect(f'http://127.0.0.1:{self.port}')
99
+
100
+ # Keep the connection alive until stopped
101
+ while self.running:
102
+ await asyncio.sleep(1)
103
+
104
+ except Exception as e:
105
+ self.logger.error(f"SocketIOClientProxy: Connection error: {e}")
106
+ self._sio_client = None
107
+
108
+ def broadcast_event(self, event_type: str, data: Dict[str, Any]):
109
+ """Send event to the persistent Socket.IO server."""
110
+ if not SOCKETIO_AVAILABLE:
111
+ return
112
+
113
+ # Ensure client is started
114
+ if not self._client_thread or not self._client_thread.is_alive():
115
+ self.logger.debug(f"SocketIOClientProxy: Starting client for {event_type}")
116
+ self._start_client()
117
+
118
+ if self._sio_client and self._sio_client.connected:
119
+ try:
120
+ event = {
121
+ "type": event_type,
122
+ "timestamp": datetime.now().isoformat(),
123
+ "data": data
124
+ }
125
+
126
+ # Send asynchronously using emit
127
+ asyncio.create_task(
128
+ self._sio_client.emit('claude_event', event)
129
+ )
130
+
131
+ self.logger.debug(f"SocketIOClientProxy: Sent event {event_type}")
132
+ except Exception as e:
133
+ self.logger.error(f"SocketIOClientProxy: Failed to send event {event_type}: {e}")
134
+ else:
135
+ self.logger.warning(f"SocketIOClientProxy: Client not ready for {event_type}")
136
+
137
+ # Compatibility methods for WebSocketServer interface
138
+ def session_started(self, session_id: str, launch_method: str, working_dir: str):
139
+ self.logger.debug(f"SocketIOClientProxy: Session started {session_id}")
140
+
141
+ def session_ended(self):
142
+ self.logger.debug(f"SocketIOClientProxy: Session ended")
143
+
144
+ def claude_status_changed(self, status: str, pid: Optional[int] = None, message: str = ""):
145
+ self.logger.debug(f"SocketIOClientProxy: Claude status {status}")
146
+
147
+ def agent_delegated(self, agent: str, task: str, status: str = "started"):
148
+ self.logger.debug(f"SocketIOClientProxy: Agent {agent} delegated")
149
+
150
+ def todo_updated(self, todos: List[Dict[str, Any]]):
151
+ self.logger.debug(f"SocketIOClientProxy: Todo updated ({len(todos)} todos)")
152
+
153
+
154
+ class SocketIOServer:
155
+ """Socket.IO server for broadcasting Claude MPM events.
156
+
157
+ WHY: Socket.IO provides better connection reliability than raw WebSockets,
158
+ with automatic reconnection, fallback transports, and better error handling.
159
+ It maintains the same event interface as WebSocketServer for compatibility.
160
+ """
161
+
162
+ def __init__(self, host: str = "localhost", port: int = 8765):
163
+ self.host = host
164
+ self.port = port
165
+ self.logger = get_logger("socketio_server")
166
+ self.clients: Set[str] = set() # Store session IDs instead of connection objects
167
+ self.event_history: deque = deque(maxlen=1000) # Keep last 1000 events
168
+ self.sio = None
169
+ self.app = None
170
+ self.runner = None
171
+ self.site = None
172
+ self.loop = None
173
+ self.thread = None
174
+ self.running = False
175
+
176
+ # Session state
177
+ self.session_id = None
178
+ self.session_start = None
179
+ self.claude_status = "stopped"
180
+ self.claude_pid = None
181
+
182
+ if not SOCKETIO_AVAILABLE:
183
+ self.logger.warning("Socket.IO support not available. Install 'python-socketio' and 'aiohttp' packages to enable.")
184
+
185
+ def start(self):
186
+ """Start the Socket.IO server in a background thread."""
187
+ if not SOCKETIO_AVAILABLE:
188
+ self.logger.debug("Socket.IO server skipped - required packages not installed")
189
+ return
190
+
191
+ if self.running:
192
+ self.logger.debug(f"Socket.IO server already running on port {self.port}")
193
+ return
194
+
195
+ self.running = True
196
+ self.thread = threading.Thread(target=self._run_server, daemon=True)
197
+ self.thread.start()
198
+ self.logger.info(f"🚀 Socket.IO server STARTING on http://{self.host}:{self.port}")
199
+ self.logger.info(f"🔧 Thread created: {self.thread.name} (daemon={self.thread.daemon})")
200
+
201
+ # Give server a moment to start
202
+ time.sleep(0.1)
203
+
204
+ if self.thread.is_alive():
205
+ self.logger.info(f"✅ Socket.IO server thread is alive and running")
206
+ else:
207
+ self.logger.error(f"❌ Socket.IO server thread failed to start!")
208
+
209
+ def stop(self):
210
+ """Stop the Socket.IO server."""
211
+ self.running = False
212
+ if self.loop:
213
+ asyncio.run_coroutine_threadsafe(self._shutdown(), self.loop)
214
+ if self.thread:
215
+ self.thread.join(timeout=5)
216
+ self.logger.info("Socket.IO server stopped")
217
+
218
+ def _run_server(self):
219
+ """Run the server event loop."""
220
+ self.logger.info(f"🔄 _run_server starting on thread: {threading.current_thread().name}")
221
+ self.loop = asyncio.new_event_loop()
222
+ asyncio.set_event_loop(self.loop)
223
+ self.logger.info(f"📡 Event loop created and set for Socket.IO server")
224
+
225
+ try:
226
+ self.logger.info(f"🎯 About to start _serve() coroutine")
227
+ self.loop.run_until_complete(self._serve())
228
+ except Exception as e:
229
+ self.logger.error(f"❌ Socket.IO server error in _run_server: {e}")
230
+ import traceback
231
+ self.logger.error(f"Stack trace: {traceback.format_exc()}")
232
+ finally:
233
+ self.logger.info(f"🔚 Socket.IO server _run_server shutting down")
234
+ self.loop.close()
235
+
236
+ async def _serve(self):
237
+ """Start the Socket.IO server."""
238
+ try:
239
+ self.logger.info(f"🔌 _serve() starting - attempting to bind to {self.host}:{self.port}")
240
+
241
+ # Create Socket.IO server with improved configuration
242
+ self.sio = socketio.AsyncServer(
243
+ cors_allowed_origins="*",
244
+ ping_timeout=120,
245
+ ping_interval=30,
246
+ max_http_buffer_size=1000000,
247
+ allow_upgrades=True,
248
+ transports=['websocket', 'polling'],
249
+ logger=False, # Reduce noise in logs
250
+ engineio_logger=False
251
+ )
252
+
253
+ # Create aiohttp web application
254
+ self.app = web.Application()
255
+ self.sio.attach(self.app)
256
+
257
+ # Add HTTP routes
258
+ self.app.router.add_get('/health', self._handle_health)
259
+ self.app.router.add_get('/status', self._handle_health)
260
+
261
+ # Register event handlers
262
+ self._register_events()
263
+
264
+ # Start the server
265
+ self.runner = web.AppRunner(self.app)
266
+ await self.runner.setup()
267
+
268
+ self.site = web.TCPSite(self.runner, self.host, self.port)
269
+ await self.site.start()
270
+
271
+ self.logger.info(f"🎉 Socket.IO server SUCCESSFULLY listening on http://{self.host}:{self.port}")
272
+
273
+ # Keep server running
274
+ loop_count = 0
275
+ while self.running:
276
+ await asyncio.sleep(0.1)
277
+ loop_count += 1
278
+ if loop_count % 100 == 0: # Log every 10 seconds
279
+ self.logger.debug(f"🔄 Socket.IO server heartbeat - {len(self.clients)} clients connected")
280
+
281
+ except Exception as e:
282
+ self.logger.error(f"❌ Failed to start Socket.IO server: {e}")
283
+ import traceback
284
+ self.logger.error(f"Stack trace: {traceback.format_exc()}")
285
+ raise
286
+
287
+ async def _shutdown(self):
288
+ """Shutdown the server."""
289
+ if self.site:
290
+ await self.site.stop()
291
+ if self.runner:
292
+ await self.runner.cleanup()
293
+
294
+ async def _handle_health(self, request):
295
+ """Handle health check requests."""
296
+ return web.json_response({
297
+ "status": "healthy",
298
+ "server": "claude-mpm-python-socketio",
299
+ "timestamp": datetime.utcnow().isoformat() + "Z",
300
+ "port": self.port,
301
+ "host": self.host,
302
+ "clients_connected": len(self.clients)
303
+ })
304
+
305
+
306
+ def _register_events(self):
307
+ """Register Socket.IO event handlers."""
308
+
309
+ @self.sio.event
310
+ async def connect(sid, environ, *args):
311
+ """Handle client connection."""
312
+ self.clients.add(sid)
313
+ client_addr = environ.get('REMOTE_ADDR', 'unknown')
314
+ user_agent = environ.get('HTTP_USER_AGENT', 'unknown')
315
+ self.logger.info(f"🔗 NEW CLIENT CONNECTED: {sid} from {client_addr}")
316
+ self.logger.info(f"📱 User Agent: {user_agent[:100]}...")
317
+ self.logger.info(f"📈 Total clients now: {len(self.clients)}")
318
+
319
+ # Send initial status immediately with enhanced data
320
+ status_data = {
321
+ "server": "claude-mpm-python-socketio",
322
+ "timestamp": datetime.utcnow().isoformat() + "Z",
323
+ "clients_connected": len(self.clients),
324
+ "session_id": self.session_id,
325
+ "claude_status": self.claude_status,
326
+ "claude_pid": self.claude_pid,
327
+ "server_version": "2.0.0",
328
+ "client_id": sid
329
+ }
330
+
331
+ try:
332
+ await self.sio.emit('status', status_data, room=sid)
333
+ await self.sio.emit('welcome', {
334
+ "message": "Connected to Claude MPM Socket.IO server",
335
+ "client_id": sid,
336
+ "server_time": datetime.utcnow().isoformat() + "Z"
337
+ }, room=sid)
338
+
339
+ # Automatically send the last 50 events to new clients
340
+ await self._send_event_history(sid, limit=50)
341
+
342
+ self.logger.debug(f"✅ Sent welcome messages and event history to client {sid}")
343
+ except Exception as e:
344
+ self.logger.error(f"❌ Failed to send welcome to client {sid}: {e}")
345
+ import traceback
346
+ self.logger.error(f"Full traceback: {traceback.format_exc()}")
347
+
348
+ @self.sio.event
349
+ async def disconnect(sid):
350
+ """Handle client disconnection."""
351
+ if sid in self.clients:
352
+ self.clients.remove(sid)
353
+ self.logger.info(f"🔌 CLIENT DISCONNECTED: {sid}")
354
+ self.logger.info(f"📉 Total clients now: {len(self.clients)}")
355
+ else:
356
+ self.logger.warning(f"⚠️ Attempted to disconnect unknown client: {sid}")
357
+
358
+ @self.sio.event
359
+ async def get_status(sid):
360
+ """Handle status request."""
361
+ # Send compatible status event (not claude_event)
362
+ status_data = {
363
+ "server": "claude-mpm-python-socketio",
364
+ "timestamp": datetime.utcnow().isoformat() + "Z",
365
+ "clients_connected": len(self.clients),
366
+ "session_id": self.session_id,
367
+ "claude_status": self.claude_status,
368
+ "claude_pid": self.claude_pid
369
+ }
370
+ await self.sio.emit('status', status_data, room=sid)
371
+ self.logger.debug(f"Sent status response to client {sid}")
372
+
373
+ @self.sio.event
374
+ async def get_history(sid, data=None):
375
+ """Handle history request."""
376
+ params = data or {}
377
+ event_types = params.get("event_types", [])
378
+ limit = min(params.get("limit", 100), len(self.event_history))
379
+
380
+ await self._send_event_history(sid, event_types=event_types, limit=limit)
381
+
382
+ @self.sio.event
383
+ async def request_history(sid, data=None):
384
+ """Handle legacy history request (for client compatibility)."""
385
+ # This handles the 'request.history' event that the client currently emits
386
+ params = data or {}
387
+ event_types = params.get("event_types", [])
388
+ limit = min(params.get("limit", 50), len(self.event_history))
389
+
390
+ await self._send_event_history(sid, event_types=event_types, limit=limit)
391
+
392
+ @self.sio.event
393
+ async def subscribe(sid, data=None):
394
+ """Handle subscription request."""
395
+ channels = data.get("channels", ["*"]) if data else ["*"]
396
+ await self.sio.emit('subscribed', {
397
+ "channels": channels
398
+ }, room=sid)
399
+
400
+ @self.sio.event
401
+ async def claude_event(sid, data):
402
+ """Handle events from client proxies."""
403
+ # Store in history
404
+ self.event_history.append(data)
405
+ self.logger.debug(f"📚 Event from client stored in history (total: {len(self.event_history)})")
406
+
407
+ # Re-broadcast to all other clients
408
+ await self.sio.emit('claude_event', data, skip_sid=sid)
409
+
410
+ async def _send_current_status(self, sid: str):
411
+ """Send current system status to a client."""
412
+ try:
413
+ status = {
414
+ "type": "system.status",
415
+ "timestamp": datetime.utcnow().isoformat() + "Z",
416
+ "data": {
417
+ "session_id": self.session_id,
418
+ "session_start": self.session_start,
419
+ "claude_status": self.claude_status,
420
+ "claude_pid": self.claude_pid,
421
+ "connected_clients": len(self.clients),
422
+ "websocket_port": self.port,
423
+ "instance_info": {
424
+ "port": self.port,
425
+ "host": self.host,
426
+ "working_dir": os.getcwd() if self.session_id else None
427
+ }
428
+ }
429
+ }
430
+ await self.sio.emit('claude_event', status, room=sid)
431
+ self.logger.debug("Sent status to client")
432
+ except Exception as e:
433
+ self.logger.error(f"Failed to send status to client: {e}")
434
+ raise
435
+
436
+ async def _send_event_history(self, sid: str, event_types: list = None, limit: int = 50):
437
+ """Send event history to a specific client.
438
+
439
+ WHY: When clients connect to the dashboard, they need context from recent events
440
+ to understand what's been happening. This sends the most recent events in
441
+ chronological order (oldest first) so the dashboard displays them properly.
442
+
443
+ Args:
444
+ sid: Socket.IO session ID of the client
445
+ event_types: Optional list of event types to filter by
446
+ limit: Maximum number of events to send (default: 50)
447
+ """
448
+ try:
449
+ if not self.event_history:
450
+ self.logger.debug(f"No event history to send to client {sid}")
451
+ return
452
+
453
+ # Limit to reasonable number to avoid overwhelming client
454
+ limit = min(limit, 100)
455
+
456
+ # Get the most recent events, filtered by type if specified
457
+ history = []
458
+ for event in reversed(self.event_history):
459
+ if not event_types or event.get("type") in event_types:
460
+ history.append(event)
461
+ if len(history) >= limit:
462
+ break
463
+
464
+ # Reverse to get chronological order (oldest first)
465
+ history = list(reversed(history))
466
+
467
+ if history:
468
+ # Send as 'history' event that the client expects
469
+ await self.sio.emit('history', {
470
+ "events": history,
471
+ "count": len(history),
472
+ "total_available": len(self.event_history)
473
+ }, room=sid)
474
+
475
+ self.logger.info(f"📚 Sent {len(history)} historical events to client {sid}")
476
+ else:
477
+ self.logger.debug(f"No matching events found for client {sid} with filters: {event_types}")
478
+
479
+ except Exception as e:
480
+ self.logger.error(f"❌ Failed to send event history to client {sid}: {e}")
481
+ import traceback
482
+ self.logger.error(f"Stack trace: {traceback.format_exc()}")
483
+
484
+ def broadcast_event(self, event_type: str, data: Dict[str, Any]):
485
+ """Broadcast an event to all connected clients."""
486
+ if not SOCKETIO_AVAILABLE:
487
+ self.logger.debug(f"⚠️ Socket.IO broadcast skipped - packages not available")
488
+ return
489
+
490
+ event = {
491
+ "type": event_type,
492
+ "timestamp": datetime.utcnow().isoformat() + "Z",
493
+ "data": data
494
+ }
495
+
496
+ self.logger.info(f"📤 BROADCASTING EVENT: {event_type}")
497
+ self.logger.debug(f"📄 Event data: {json.dumps(data, indent=2)[:200]}...")
498
+
499
+ # Store in history
500
+ self.event_history.append(event)
501
+ self.logger.debug(f"📚 Event stored in history (total: {len(self.event_history)})")
502
+
503
+ # Check if we have clients and event loop
504
+ if not self.clients:
505
+ self.logger.warning(f"⚠️ No Socket.IO clients connected - event will not be delivered")
506
+ return
507
+
508
+ if not self.loop or not self.sio:
509
+ self.logger.error(f"❌ No event loop or Socket.IO instance available - cannot broadcast event")
510
+ return
511
+
512
+ self.logger.info(f"🎯 Broadcasting to {len(self.clients)} clients via event loop")
513
+
514
+ # Broadcast to clients with timeout and error handling
515
+ try:
516
+ future = asyncio.run_coroutine_threadsafe(
517
+ self.sio.emit('claude_event', event),
518
+ self.loop
519
+ )
520
+ # Wait for completion with timeout to detect issues
521
+ try:
522
+ future.result(timeout=2.0) # 2 second timeout
523
+ self.logger.debug(f"📨 Successfully broadcasted {event_type} to {len(self.clients)} clients")
524
+ except asyncio.TimeoutError:
525
+ self.logger.warning(f"⏰ Broadcast timeout for event {event_type} - continuing anyway")
526
+ except Exception as emit_error:
527
+ self.logger.error(f"❌ Broadcast emit error for {event_type}: {emit_error}")
528
+ except Exception as e:
529
+ self.logger.error(f"❌ Failed to submit broadcast to event loop: {e}")
530
+ import traceback
531
+ self.logger.error(f"Stack trace: {traceback.format_exc()}")
532
+
533
+ # Convenience methods for common events (same interface as WebSocketServer)
534
+
535
+ def session_started(self, session_id: str, launch_method: str, working_dir: str):
536
+ """Notify that a session has started."""
537
+ self.session_id = session_id
538
+ self.session_start = datetime.utcnow().isoformat() + "Z"
539
+ self.broadcast_event("session.start", {
540
+ "session_id": session_id,
541
+ "start_time": self.session_start,
542
+ "launch_method": launch_method,
543
+ "working_directory": working_dir,
544
+ "websocket_port": self.port,
545
+ "instance_info": {
546
+ "port": self.port,
547
+ "host": self.host,
548
+ "working_dir": working_dir
549
+ }
550
+ })
551
+
552
+ def session_ended(self):
553
+ """Notify that a session has ended."""
554
+ if self.session_id:
555
+ duration = None
556
+ if self.session_start:
557
+ start = datetime.fromisoformat(self.session_start.replace("Z", "+00:00"))
558
+ duration = (datetime.utcnow() - start.replace(tzinfo=None)).total_seconds()
559
+
560
+ self.broadcast_event("session.end", {
561
+ "session_id": self.session_id,
562
+ "end_time": datetime.utcnow().isoformat() + "Z",
563
+ "duration_seconds": duration
564
+ })
565
+
566
+ self.session_id = None
567
+ self.session_start = None
568
+
569
+ def claude_status_changed(self, status: str, pid: Optional[int] = None, message: str = ""):
570
+ """Notify Claude status change."""
571
+ self.claude_status = status
572
+ self.claude_pid = pid
573
+ self.broadcast_event("claude.status", {
574
+ "status": status,
575
+ "pid": pid,
576
+ "message": message
577
+ })
578
+
579
+ def claude_output(self, content: str, stream: str = "stdout"):
580
+ """Broadcast Claude output."""
581
+ self.broadcast_event("claude.output", {
582
+ "content": content,
583
+ "stream": stream
584
+ })
585
+
586
+ def agent_delegated(self, agent: str, task: str, status: str = "started"):
587
+ """Notify agent delegation."""
588
+ self.broadcast_event("agent.delegation", {
589
+ "agent": agent,
590
+ "task": task,
591
+ "status": status,
592
+ "timestamp": datetime.utcnow().isoformat() + "Z"
593
+ })
594
+
595
+ def todo_updated(self, todos: List[Dict[str, Any]]):
596
+ """Notify todo list update."""
597
+ stats = {
598
+ "total": len(todos),
599
+ "completed": sum(1 for t in todos if t.get("status") == "completed"),
600
+ "in_progress": sum(1 for t in todos if t.get("status") == "in_progress"),
601
+ "pending": sum(1 for t in todos if t.get("status") == "pending")
602
+ }
603
+
604
+ self.broadcast_event("todo.update", {
605
+ "todos": todos,
606
+ "stats": stats
607
+ })
608
+
609
+ def ticket_created(self, ticket_id: str, title: str, priority: str = "medium"):
610
+ """Notify ticket creation."""
611
+ self.broadcast_event("ticket.created", {
612
+ "id": ticket_id,
613
+ "title": title,
614
+ "priority": priority,
615
+ "created_at": datetime.utcnow().isoformat() + "Z"
616
+ })
617
+
618
+ def memory_loaded(self, agent_id: str, memory_size: int, sections_count: int):
619
+ """Notify when agent memory is loaded from file."""
620
+ self.broadcast_event("memory:loaded", {
621
+ "agent_id": agent_id,
622
+ "memory_size": memory_size,
623
+ "sections_count": sections_count,
624
+ "timestamp": datetime.utcnow().isoformat() + "Z"
625
+ })
626
+
627
+ def memory_created(self, agent_id: str, template_type: str):
628
+ """Notify when new agent memory is created from template."""
629
+ self.broadcast_event("memory:created", {
630
+ "agent_id": agent_id,
631
+ "template_type": template_type,
632
+ "timestamp": datetime.utcnow().isoformat() + "Z"
633
+ })
634
+
635
+ def memory_updated(self, agent_id: str, learning_type: str, content: str, section: str):
636
+ """Notify when learning is added to agent memory."""
637
+ self.broadcast_event("memory:updated", {
638
+ "agent_id": agent_id,
639
+ "learning_type": learning_type,
640
+ "content": content,
641
+ "section": section,
642
+ "timestamp": datetime.utcnow().isoformat() + "Z"
643
+ })
644
+
645
+ def memory_injected(self, agent_id: str, context_size: int):
646
+ """Notify when agent memory is injected into context."""
647
+ self.broadcast_event("memory:injected", {
648
+ "agent_id": agent_id,
649
+ "context_size": context_size,
650
+ "timestamp": datetime.utcnow().isoformat() + "Z"
651
+ })
652
+
653
+
654
+ # Global instance for easy access
655
+ _socketio_server: Optional[SocketIOServer] = None
656
+
657
+
658
+ def get_socketio_server() -> SocketIOServer:
659
+ """Get or create the global Socket.IO server instance.
660
+
661
+ WHY: In exec mode, a persistent Socket.IO server may already be running
662
+ in a separate process. We need to detect this and create a client proxy
663
+ instead of trying to start another server.
664
+ """
665
+ global _socketio_server
666
+ if _socketio_server is None:
667
+ # Check if a Socket.IO server is already running on the default port
668
+ import socket
669
+ try:
670
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
671
+ s.settimeout(0.5)
672
+ result = s.connect_ex(('127.0.0.1', 8765))
673
+ if result == 0:
674
+ # Server is already running - create a client proxy
675
+ _socketio_server = SocketIOClientProxy(port=8765)
676
+ else:
677
+ # No server running - create a real server
678
+ _socketio_server = SocketIOServer()
679
+ except Exception:
680
+ # On any error, create a real server
681
+ _socketio_server = SocketIOServer()
682
+
683
+ return _socketio_server
684
+
685
+
686
+ def start_socketio_server():
687
+ """Start the global Socket.IO server."""
688
+ server = get_socketio_server()
689
+ server.start()
690
+ return server
691
+
692
+
693
+ def stop_socketio_server():
694
+ """Stop the global Socket.IO server."""
695
+ global _socketio_server
696
+ if _socketio_server:
697
+ _socketio_server.stop()
698
+ _socketio_server = None