claude-mpm 5.1.9__py3-none-any.whl → 5.4.3__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.
Potentially problematic release.
This version of claude-mpm might be problematic. Click here for more details.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/PM_INSTRUCTIONS.md +46 -0
- claude_mpm/agents/agent_loader.py +10 -17
- claude_mpm/agents/templates/circuit-breakers.md +138 -1
- claude_mpm/cli/commands/agent_state_manager.py +8 -17
- claude_mpm/cli/commands/configure.py +1046 -149
- claude_mpm/cli/commands/configure_agent_display.py +13 -6
- claude_mpm/cli/commands/mpm_init/core.py +158 -1
- claude_mpm/cli/commands/mpm_init/knowledge_extractor.py +481 -0
- claude_mpm/cli/commands/mpm_init/prompts.py +280 -0
- claude_mpm/cli/commands/summarize.py +413 -0
- claude_mpm/cli/executor.py +8 -0
- claude_mpm/cli/parsers/base_parser.py +5 -0
- claude_mpm/cli/startup.py +60 -53
- claude_mpm/commands/{mpm-ticket-organize.md → mpm-organize.md} +4 -5
- claude_mpm/config/agent_sources.py +27 -0
- claude_mpm/core/framework/loaders/agent_loader.py +8 -5
- claude_mpm/core/socketio_pool.py +3 -3
- claude_mpm/core/unified_agent_registry.py +5 -15
- claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/correlation_manager.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/correlation_manager.py +60 -0
- claude_mpm/hooks/claude_hooks/event_handlers.py +35 -2
- claude_mpm/hooks/claude_hooks/hook_handler.py +4 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/connection_manager.py +4 -0
- claude_mpm/scripts/launch_monitor.py +93 -13
- claude_mpm/services/agents/agent_recommendation_service.py +279 -0
- claude_mpm/services/agents/deployment/agent_template_builder.py +3 -2
- claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +322 -53
- claude_mpm/services/agents/git_source_manager.py +20 -0
- claude_mpm/services/agents/sources/git_source_sync_service.py +8 -1
- claude_mpm/services/agents/toolchain_detector.py +6 -5
- claude_mpm/services/analysis/__init__.py +11 -1
- claude_mpm/services/analysis/clone_detector.py +1030 -0
- claude_mpm/services/command_deployment_service.py +0 -2
- claude_mpm/services/event_bus/config.py +3 -1
- claude_mpm/services/monitor/daemon.py +9 -2
- claude_mpm/services/monitor/daemon_manager.py +39 -3
- claude_mpm/services/monitor/server.py +225 -19
- claude_mpm/services/socketio/event_normalizer.py +15 -1
- claude_mpm/services/socketio/server/core.py +160 -21
- claude_mpm/services/version_control/git_operations.py +103 -0
- claude_mpm/utils/agent_filters.py +17 -44
- {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.3.dist-info}/METADATA +1 -77
- {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.3.dist-info}/RECORD +59 -114
- {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.3.dist-info}/entry_points.txt +0 -2
- claude_mpm/dashboard/analysis_runner.py +0 -455
- claude_mpm/dashboard/index.html +0 -13
- claude_mpm/dashboard/open_dashboard.py +0 -66
- claude_mpm/dashboard/static/css/activity.css +0 -1958
- claude_mpm/dashboard/static/css/connection-status.css +0 -370
- claude_mpm/dashboard/static/css/dashboard.css +0 -4701
- claude_mpm/dashboard/static/js/components/activity-tree.js +0 -1871
- claude_mpm/dashboard/static/js/components/agent-hierarchy.js +0 -777
- claude_mpm/dashboard/static/js/components/agent-inference.js +0 -956
- claude_mpm/dashboard/static/js/components/build-tracker.js +0 -333
- claude_mpm/dashboard/static/js/components/code-simple.js +0 -857
- claude_mpm/dashboard/static/js/components/connection-debug.js +0 -654
- claude_mpm/dashboard/static/js/components/diff-viewer.js +0 -891
- claude_mpm/dashboard/static/js/components/event-processor.js +0 -542
- claude_mpm/dashboard/static/js/components/event-viewer.js +0 -1155
- claude_mpm/dashboard/static/js/components/export-manager.js +0 -368
- claude_mpm/dashboard/static/js/components/file-change-tracker.js +0 -443
- claude_mpm/dashboard/static/js/components/file-change-viewer.js +0 -690
- claude_mpm/dashboard/static/js/components/file-tool-tracker.js +0 -724
- claude_mpm/dashboard/static/js/components/file-viewer.js +0 -580
- claude_mpm/dashboard/static/js/components/hud-library-loader.js +0 -211
- claude_mpm/dashboard/static/js/components/hud-manager.js +0 -671
- claude_mpm/dashboard/static/js/components/hud-visualizer.js +0 -1718
- claude_mpm/dashboard/static/js/components/module-viewer.js +0 -2764
- claude_mpm/dashboard/static/js/components/session-manager.js +0 -579
- claude_mpm/dashboard/static/js/components/socket-manager.js +0 -368
- claude_mpm/dashboard/static/js/components/ui-state-manager.js +0 -749
- claude_mpm/dashboard/static/js/components/unified-data-viewer.js +0 -1824
- claude_mpm/dashboard/static/js/components/working-directory.js +0 -920
- claude_mpm/dashboard/static/js/connection-manager.js +0 -536
- claude_mpm/dashboard/static/js/dashboard.js +0 -1914
- claude_mpm/dashboard/static/js/extension-error-handler.js +0 -164
- claude_mpm/dashboard/static/js/socket-client.js +0 -1474
- claude_mpm/dashboard/static/js/tab-isolation-fix.js +0 -185
- claude_mpm/dashboard/static/socket.io.min.js +0 -7
- claude_mpm/dashboard/static/socket.io.v4.8.1.backup.js +0 -7
- claude_mpm/dashboard/templates/code_simple.html +0 -153
- claude_mpm/dashboard/templates/index.html +0 -606
- claude_mpm/dashboard/test_dashboard.html +0 -372
- claude_mpm/scripts/mcp_server.py +0 -75
- claude_mpm/scripts/mcp_wrapper.py +0 -39
- claude_mpm/services/mcp_gateway/__init__.py +0 -159
- claude_mpm/services/mcp_gateway/auto_configure.py +0 -369
- claude_mpm/services/mcp_gateway/config/__init__.py +0 -17
- claude_mpm/services/mcp_gateway/config/config_loader.py +0 -296
- claude_mpm/services/mcp_gateway/config/config_schema.py +0 -243
- claude_mpm/services/mcp_gateway/config/configuration.py +0 -429
- claude_mpm/services/mcp_gateway/core/__init__.py +0 -43
- claude_mpm/services/mcp_gateway/core/base.py +0 -312
- claude_mpm/services/mcp_gateway/core/exceptions.py +0 -253
- claude_mpm/services/mcp_gateway/core/interfaces.py +0 -443
- claude_mpm/services/mcp_gateway/core/process_pool.py +0 -977
- claude_mpm/services/mcp_gateway/core/singleton_manager.py +0 -315
- claude_mpm/services/mcp_gateway/core/startup_verification.py +0 -316
- claude_mpm/services/mcp_gateway/main.py +0 -589
- claude_mpm/services/mcp_gateway/registry/__init__.py +0 -12
- claude_mpm/services/mcp_gateway/registry/service_registry.py +0 -412
- claude_mpm/services/mcp_gateway/registry/tool_registry.py +0 -489
- claude_mpm/services/mcp_gateway/server/__init__.py +0 -15
- claude_mpm/services/mcp_gateway/server/mcp_gateway.py +0 -414
- claude_mpm/services/mcp_gateway/server/stdio_handler.py +0 -372
- claude_mpm/services/mcp_gateway/server/stdio_server.py +0 -712
- claude_mpm/services/mcp_gateway/tools/__init__.py +0 -36
- claude_mpm/services/mcp_gateway/tools/base_adapter.py +0 -485
- claude_mpm/services/mcp_gateway/tools/document_summarizer.py +0 -789
- claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +0 -654
- claude_mpm/services/mcp_gateway/tools/health_check_tool.py +0 -456
- claude_mpm/services/mcp_gateway/tools/hello_world.py +0 -551
- claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +0 -555
- claude_mpm/services/mcp_gateway/utils/__init__.py +0 -14
- claude_mpm/services/mcp_gateway/utils/package_version_checker.py +0 -160
- claude_mpm/services/mcp_gateway/utils/update_preferences.py +0 -170
- {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.3.dist-info}/WHEEL +0 -0
- {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.3.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.3.dist-info}/top_level.txt +0 -0
|
@@ -24,7 +24,6 @@ class CommandDeploymentService(BaseService):
|
|
|
24
24
|
"mpm-agents.md", # Replaced by mpm-agents-list.md
|
|
25
25
|
"mpm-auto-configure.md", # Replaced by mpm-agents-auto-configure.md
|
|
26
26
|
"mpm-config.md", # Replaced by mpm-config-view.md
|
|
27
|
-
"mpm-organize.md", # Replaced by mpm-ticket-organize.md
|
|
28
27
|
"mpm-resume.md", # Replaced by mpm-session-resume.md
|
|
29
28
|
"mpm-ticket.md", # Replaced by mpm-ticket-view.md
|
|
30
29
|
]
|
|
@@ -316,7 +315,6 @@ class CommandDeploymentService(BaseService):
|
|
|
316
315
|
"mpm-agents.md": "mpm-agents-list.md",
|
|
317
316
|
"mpm-auto-configure.md": "mpm-agents-auto-configure.md",
|
|
318
317
|
"mpm-config.md": "mpm-config-view.md",
|
|
319
|
-
"mpm-organize.md": "mpm-ticket-organize.md",
|
|
320
318
|
"mpm-resume.md": "mpm-session-resume.md",
|
|
321
319
|
"mpm-ticket.md": "mpm-ticket-view.md",
|
|
322
320
|
}
|
|
@@ -52,9 +52,11 @@ class EventBusConfig:
|
|
|
52
52
|
)
|
|
53
53
|
|
|
54
54
|
# Relay configuration
|
|
55
|
+
# DirectSocketIORelay disabled by default - events already emit via direct sio.emit()
|
|
56
|
+
# Enable with CLAUDE_MPM_RELAY_ENABLED=true if needed for external consumers
|
|
55
57
|
relay_enabled: bool = field(
|
|
56
58
|
default_factory=lambda: os.environ.get(
|
|
57
|
-
"CLAUDE_MPM_RELAY_ENABLED", "
|
|
59
|
+
"CLAUDE_MPM_RELAY_ENABLED", "false"
|
|
58
60
|
).lower()
|
|
59
61
|
== "true"
|
|
60
62
|
)
|
|
@@ -44,6 +44,7 @@ class UnifiedMonitorDaemon:
|
|
|
44
44
|
daemon_mode: bool = False,
|
|
45
45
|
pid_file: Optional[str] = None,
|
|
46
46
|
log_file: Optional[str] = None,
|
|
47
|
+
enable_hot_reload: bool = False,
|
|
47
48
|
):
|
|
48
49
|
"""Initialize the unified monitor daemon.
|
|
49
50
|
|
|
@@ -53,10 +54,12 @@ class UnifiedMonitorDaemon:
|
|
|
53
54
|
daemon_mode: Whether to run as background daemon
|
|
54
55
|
pid_file: Path to PID file for daemon mode
|
|
55
56
|
log_file: Path to log file for daemon mode
|
|
57
|
+
enable_hot_reload: Enable file watching and hot reload for development
|
|
56
58
|
"""
|
|
57
59
|
self.host = host
|
|
58
60
|
self.port = port
|
|
59
61
|
self.daemon_mode = daemon_mode
|
|
62
|
+
self.enable_hot_reload = enable_hot_reload
|
|
60
63
|
self.logger = get_logger(__name__)
|
|
61
64
|
|
|
62
65
|
# Use new consolidated DaemonManager for all daemon operations
|
|
@@ -75,7 +78,9 @@ class UnifiedMonitorDaemon:
|
|
|
75
78
|
)
|
|
76
79
|
|
|
77
80
|
# Core server
|
|
78
|
-
self.server = UnifiedMonitorServer(
|
|
81
|
+
self.server = UnifiedMonitorServer(
|
|
82
|
+
host=host, port=port, enable_hot_reload=enable_hot_reload
|
|
83
|
+
)
|
|
79
84
|
|
|
80
85
|
# Health monitoring
|
|
81
86
|
self.health_monitor = HealthMonitor(port=port)
|
|
@@ -510,7 +515,9 @@ class UnifiedMonitorDaemon:
|
|
|
510
515
|
|
|
511
516
|
# Recreate the server and health monitor after stop() sets them to None
|
|
512
517
|
self.logger.info(f"Recreating server components for {self.host}:{self.port}")
|
|
513
|
-
self.server = UnifiedMonitorServer(
|
|
518
|
+
self.server = UnifiedMonitorServer(
|
|
519
|
+
host=self.host, port=self.port, enable_hot_reload=self.enable_hot_reload
|
|
520
|
+
)
|
|
514
521
|
self.health_monitor = HealthMonitor(port=self.port)
|
|
515
522
|
|
|
516
523
|
# Reset the shutdown event for the new run
|
|
@@ -34,6 +34,11 @@ from typing import Optional, Tuple
|
|
|
34
34
|
from ...core.enums import OperationResult
|
|
35
35
|
from ...core.logging_config import get_logger
|
|
36
36
|
|
|
37
|
+
# Exit code constants for signal handling
|
|
38
|
+
EXIT_NORMAL = 0
|
|
39
|
+
EXIT_SIGKILL = 137 # 128 + SIGKILL(9) - forced termination
|
|
40
|
+
EXIT_SIGTERM = 143 # 128 + SIGTERM(15) - graceful shutdown
|
|
41
|
+
|
|
37
42
|
|
|
38
43
|
class DaemonManager:
|
|
39
44
|
"""Centralized manager for all daemon lifecycle operations.
|
|
@@ -556,7 +561,7 @@ class DaemonManager:
|
|
|
556
561
|
# Use subprocess for clean daemon startup (v4.2.40)
|
|
557
562
|
# This avoids fork() issues with Python threading
|
|
558
563
|
if self.use_subprocess_daemon():
|
|
559
|
-
return self.start_daemon_subprocess()
|
|
564
|
+
return self.start_daemon_subprocess(force_restart=force_restart)
|
|
560
565
|
# Fallback to traditional fork (kept for compatibility)
|
|
561
566
|
return self.daemonize()
|
|
562
567
|
|
|
@@ -574,12 +579,15 @@ class DaemonManager:
|
|
|
574
579
|
# Otherwise, use subprocess for monitor daemon to avoid threading issues
|
|
575
580
|
return True
|
|
576
581
|
|
|
577
|
-
def start_daemon_subprocess(self) -> bool:
|
|
582
|
+
def start_daemon_subprocess(self, force_restart: bool = False) -> bool:
|
|
578
583
|
"""Start daemon using subprocess.Popen for clean process isolation.
|
|
579
584
|
|
|
580
585
|
This avoids all the fork() + threading issues by starting the monitor
|
|
581
586
|
in a completely fresh process with no inherited threads or locks.
|
|
582
587
|
|
|
588
|
+
Args:
|
|
589
|
+
force_restart: Whether this is a force restart (helps interpret exit codes)
|
|
590
|
+
|
|
583
591
|
Returns:
|
|
584
592
|
True if daemon started successfully
|
|
585
593
|
"""
|
|
@@ -652,7 +660,35 @@ class DaemonManager:
|
|
|
652
660
|
# Check if process is still running
|
|
653
661
|
returncode = process.poll()
|
|
654
662
|
if returncode is not None:
|
|
655
|
-
# Process exited -
|
|
663
|
+
# Process exited - interpret exit code with context
|
|
664
|
+
# Exit codes 137 (SIGKILL) and 143 (SIGTERM) are common during daemon replacement
|
|
665
|
+
if returncode == EXIT_SIGKILL:
|
|
666
|
+
# SIGKILL - process was forcefully terminated
|
|
667
|
+
if force_restart:
|
|
668
|
+
# This is expected during force restart - old daemon was killed
|
|
669
|
+
self.logger.info(
|
|
670
|
+
f"Previous monitor instance replaced (exit {EXIT_SIGKILL}: SIGKILL during force restart)"
|
|
671
|
+
)
|
|
672
|
+
else:
|
|
673
|
+
# Unexpected SIGKILL - something else killed our new daemon
|
|
674
|
+
self.logger.warning(
|
|
675
|
+
f"Monitor subprocess terminated unexpectedly (exit {EXIT_SIGKILL}: SIGKILL). "
|
|
676
|
+
f"Check {self.log_file} for details."
|
|
677
|
+
)
|
|
678
|
+
return False
|
|
679
|
+
if returncode == EXIT_SIGTERM:
|
|
680
|
+
# SIGTERM - graceful shutdown requested
|
|
681
|
+
self.logger.info(
|
|
682
|
+
f"Monitor subprocess cleanly terminated (exit {EXIT_SIGTERM}: SIGTERM, graceful shutdown)"
|
|
683
|
+
)
|
|
684
|
+
return False
|
|
685
|
+
if returncode == EXIT_NORMAL:
|
|
686
|
+
# Normal exit
|
|
687
|
+
self.logger.info(
|
|
688
|
+
f"Monitor subprocess exited normally (exit code {EXIT_NORMAL})"
|
|
689
|
+
)
|
|
690
|
+
return False
|
|
691
|
+
# Unexpected exit code - this IS an error
|
|
656
692
|
self.logger.error(
|
|
657
693
|
f"Monitor daemon subprocess exited prematurely with code {returncode}"
|
|
658
694
|
)
|
|
@@ -15,7 +15,6 @@ DESIGN DECISIONS:
|
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
17
|
import asyncio
|
|
18
|
-
import contextlib
|
|
19
18
|
import os
|
|
20
19
|
import threading
|
|
21
20
|
import time
|
|
@@ -25,6 +24,8 @@ from typing import Dict, Optional
|
|
|
25
24
|
|
|
26
25
|
import socketio
|
|
27
26
|
from aiohttp import web
|
|
27
|
+
from watchdog.events import FileSystemEventHandler
|
|
28
|
+
from watchdog.observers import Observer
|
|
28
29
|
|
|
29
30
|
from ...core.enums import ServiceState
|
|
30
31
|
from ...core.logging_config import get_logger
|
|
@@ -45,6 +46,91 @@ except ImportError:
|
|
|
45
46
|
EVENTBUS_AVAILABLE = False
|
|
46
47
|
|
|
47
48
|
|
|
49
|
+
class SvelteBuildWatcher(FileSystemEventHandler):
|
|
50
|
+
"""File watcher for Svelte build directory changes.
|
|
51
|
+
|
|
52
|
+
Watches for file changes in svelte-build directory and triggers
|
|
53
|
+
hot reload via Socket.IO event emission.
|
|
54
|
+
|
|
55
|
+
STABILITY FIX: Added thread lock and stop() method to prevent timer leaks.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def __init__(
|
|
59
|
+
self, sio: socketio.AsyncServer, loop: asyncio.AbstractEventLoop, logger
|
|
60
|
+
):
|
|
61
|
+
"""Initialize the file watcher.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
sio: Socket.IO server instance for emitting events
|
|
65
|
+
loop: Event loop for async operations
|
|
66
|
+
logger: Logger instance
|
|
67
|
+
"""
|
|
68
|
+
super().__init__()
|
|
69
|
+
self.sio = sio
|
|
70
|
+
self.loop = loop
|
|
71
|
+
self.logger = logger
|
|
72
|
+
self.debounce_timer = None
|
|
73
|
+
self.debounce_delay = 0.5 # Wait 500ms after last change
|
|
74
|
+
self._timer_lock = threading.Lock() # STABILITY FIX: Prevent race condition
|
|
75
|
+
|
|
76
|
+
def stop(self):
|
|
77
|
+
"""Stop the watcher and cancel any pending timers.
|
|
78
|
+
|
|
79
|
+
STABILITY FIX: Ensures timer is cancelled on shutdown.
|
|
80
|
+
"""
|
|
81
|
+
with self._timer_lock:
|
|
82
|
+
if self.debounce_timer:
|
|
83
|
+
self.debounce_timer.cancel()
|
|
84
|
+
self.debounce_timer = None
|
|
85
|
+
|
|
86
|
+
def on_any_event(self, event):
|
|
87
|
+
"""Handle any file system event.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
event: File system event from watchdog
|
|
91
|
+
"""
|
|
92
|
+
# Ignore directory events and temporary files
|
|
93
|
+
if event.is_directory or event.src_path.endswith((".tmp", ".swp", "~")):
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
self.logger.debug(
|
|
97
|
+
f"File change detected: {event.event_type} - {event.src_path}"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# STABILITY FIX: Use lock to prevent timer race condition
|
|
101
|
+
with self._timer_lock:
|
|
102
|
+
# Cancel existing timer
|
|
103
|
+
if self.debounce_timer:
|
|
104
|
+
self.debounce_timer.cancel()
|
|
105
|
+
|
|
106
|
+
# Schedule reload after debounce delay
|
|
107
|
+
self.debounce_timer = threading.Timer(
|
|
108
|
+
self.debounce_delay, self._trigger_reload
|
|
109
|
+
)
|
|
110
|
+
self.debounce_timer.start()
|
|
111
|
+
|
|
112
|
+
def _trigger_reload(self):
|
|
113
|
+
"""Trigger hot reload by emitting Socket.IO event."""
|
|
114
|
+
try:
|
|
115
|
+
# Schedule the async emit in the event loop
|
|
116
|
+
asyncio.run_coroutine_threadsafe(self._emit_reload_event(), self.loop)
|
|
117
|
+
self.logger.info("Hot reload triggered - Svelte build changed")
|
|
118
|
+
except Exception as e:
|
|
119
|
+
self.logger.error(f"Error triggering reload: {e}")
|
|
120
|
+
|
|
121
|
+
async def _emit_reload_event(self):
|
|
122
|
+
"""Emit the reload event to all connected clients."""
|
|
123
|
+
if self.sio:
|
|
124
|
+
await self.sio.emit(
|
|
125
|
+
"reload",
|
|
126
|
+
{
|
|
127
|
+
"type": "reload",
|
|
128
|
+
"timestamp": datetime.now(timezone.utc).isoformat() + "Z",
|
|
129
|
+
"reason": "svelte-build-updated",
|
|
130
|
+
},
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
48
134
|
class UnifiedMonitorServer:
|
|
49
135
|
"""Unified server that combines HTTP dashboard and Socket.IO functionality.
|
|
50
136
|
|
|
@@ -52,15 +138,19 @@ class UnifiedMonitorServer:
|
|
|
52
138
|
Replaces multiple competing server implementations with one stable solution.
|
|
53
139
|
"""
|
|
54
140
|
|
|
55
|
-
def __init__(
|
|
141
|
+
def __init__(
|
|
142
|
+
self, host: str = "localhost", port: int = 8765, enable_hot_reload: bool = False
|
|
143
|
+
):
|
|
56
144
|
"""Initialize the unified monitor server.
|
|
57
145
|
|
|
58
146
|
Args:
|
|
59
147
|
host: Host to bind to
|
|
60
148
|
port: Port to bind to
|
|
149
|
+
enable_hot_reload: Enable file watching and hot reload for development
|
|
61
150
|
"""
|
|
62
151
|
self.host = host
|
|
63
152
|
self.port = port
|
|
153
|
+
self.enable_hot_reload = enable_hot_reload
|
|
64
154
|
self.logger = get_logger(__name__)
|
|
65
155
|
|
|
66
156
|
# Core components
|
|
@@ -78,6 +168,10 @@ class UnifiedMonitorServer:
|
|
|
78
168
|
# High-performance event emitter
|
|
79
169
|
self.event_emitter = None
|
|
80
170
|
|
|
171
|
+
# File watching (optional for dev mode)
|
|
172
|
+
self.file_observer: Optional[Observer] = None
|
|
173
|
+
self.file_watcher: Optional[SvelteBuildWatcher] = None
|
|
174
|
+
|
|
81
175
|
# State
|
|
82
176
|
self.running = False
|
|
83
177
|
self.loop = None
|
|
@@ -184,6 +278,9 @@ class UnifiedMonitorServer:
|
|
|
184
278
|
|
|
185
279
|
time.sleep(0.1)
|
|
186
280
|
|
|
281
|
+
# STABILITY FIX: Give tasks more time to clean up before closing
|
|
282
|
+
time.sleep(0.5)
|
|
283
|
+
|
|
187
284
|
# Clear the event loop from the thread BEFORE closing
|
|
188
285
|
# This prevents other code from accidentally using it
|
|
189
286
|
asyncio.set_event_loop(None)
|
|
@@ -229,6 +326,10 @@ class UnifiedMonitorServer:
|
|
|
229
326
|
self.heartbeat_task = asyncio.create_task(self._heartbeat_loop())
|
|
230
327
|
self.logger.info("Heartbeat task started (3-minute interval)")
|
|
231
328
|
|
|
329
|
+
# Setup file watching for hot reload (if enabled)
|
|
330
|
+
if self.enable_hot_reload:
|
|
331
|
+
self._setup_file_watcher()
|
|
332
|
+
|
|
232
333
|
# Setup HTTP routes
|
|
233
334
|
self._setup_http_routes()
|
|
234
335
|
|
|
@@ -304,20 +405,64 @@ class UnifiedMonitorServer:
|
|
|
304
405
|
self.logger.error(f"Error setting up event emitter: {e}")
|
|
305
406
|
raise
|
|
306
407
|
|
|
408
|
+
def _setup_file_watcher(self):
|
|
409
|
+
"""Setup file watcher for Svelte build directory.
|
|
410
|
+
|
|
411
|
+
Watches for changes in svelte-build and triggers hot reload.
|
|
412
|
+
Only enabled when enable_hot_reload is True.
|
|
413
|
+
"""
|
|
414
|
+
try:
|
|
415
|
+
dashboard_dir = Path(__file__).resolve().parent.parent.parent / "dashboard"
|
|
416
|
+
svelte_build_dir = dashboard_dir / "static" / "svelte-build"
|
|
417
|
+
|
|
418
|
+
if not svelte_build_dir.exists():
|
|
419
|
+
self.logger.warning(
|
|
420
|
+
f"Svelte build directory not found: {svelte_build_dir}. "
|
|
421
|
+
"Hot reload disabled."
|
|
422
|
+
)
|
|
423
|
+
return
|
|
424
|
+
|
|
425
|
+
# Create file watcher with Socket.IO reference
|
|
426
|
+
self.file_watcher = SvelteBuildWatcher(
|
|
427
|
+
sio=self.sio, loop=self.loop, logger=self.logger
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
# Create observer and schedule watching
|
|
431
|
+
self.file_observer = Observer()
|
|
432
|
+
self.file_observer.schedule(
|
|
433
|
+
self.file_watcher, str(svelte_build_dir), recursive=True
|
|
434
|
+
)
|
|
435
|
+
self.file_observer.start()
|
|
436
|
+
|
|
437
|
+
self.logger.info(f"🔥 Hot reload enabled - watching {svelte_build_dir}")
|
|
438
|
+
|
|
439
|
+
except Exception as e:
|
|
440
|
+
self.logger.error(f"Error setting up file watcher: {e}")
|
|
441
|
+
# Don't raise - hot reload is optional
|
|
442
|
+
|
|
307
443
|
def _setup_http_routes(self):
|
|
308
444
|
"""Setup HTTP routes for the dashboard."""
|
|
309
445
|
try:
|
|
310
|
-
# Dashboard static files
|
|
311
|
-
dashboard_dir = Path(__file__).parent.parent.parent / "dashboard"
|
|
446
|
+
# Dashboard static files - use .resolve() for absolute path
|
|
447
|
+
dashboard_dir = Path(__file__).resolve().parent.parent.parent / "dashboard"
|
|
448
|
+
static_dir = dashboard_dir / "static"
|
|
312
449
|
|
|
313
|
-
# Main dashboard route
|
|
450
|
+
# Main dashboard route - serve Svelte dashboard
|
|
314
451
|
async def dashboard_index(request):
|
|
315
|
-
|
|
316
|
-
if
|
|
317
|
-
with
|
|
452
|
+
svelte_index = static_dir / "svelte-build" / "index.html"
|
|
453
|
+
if svelte_index.exists():
|
|
454
|
+
with svelte_index.open(encoding="utf-8") as f:
|
|
318
455
|
content = f.read()
|
|
319
456
|
return web.Response(text=content, content_type="text/html")
|
|
320
|
-
|
|
457
|
+
|
|
458
|
+
# Log error with path details for debugging
|
|
459
|
+
self.logger.error(
|
|
460
|
+
f"Dashboard index.html not found at: {svelte_index.resolve()}"
|
|
461
|
+
)
|
|
462
|
+
return web.Response(
|
|
463
|
+
text=f"Dashboard not found. Expected location: {svelte_index.resolve()}",
|
|
464
|
+
status=404,
|
|
465
|
+
)
|
|
321
466
|
|
|
322
467
|
# Health check
|
|
323
468
|
async def health_check(request):
|
|
@@ -325,7 +470,8 @@ class UnifiedMonitorServer:
|
|
|
325
470
|
version = "1.0.0"
|
|
326
471
|
try:
|
|
327
472
|
version_file = (
|
|
328
|
-
Path(__file__).parent.parent.parent.parent.parent
|
|
473
|
+
Path(__file__).resolve().parent.parent.parent.parent.parent
|
|
474
|
+
/ "VERSION"
|
|
329
475
|
)
|
|
330
476
|
if version_file.exists():
|
|
331
477
|
version = version_file.read_text().strip()
|
|
@@ -546,12 +692,43 @@ class UnifiedMonitorServer:
|
|
|
546
692
|
"/monitor/events", lambda r: monitor_page_handler(r)
|
|
547
693
|
)
|
|
548
694
|
|
|
549
|
-
#
|
|
550
|
-
|
|
695
|
+
# Serve Svelte _app assets (compiled JS/CSS)
|
|
696
|
+
svelte_build_dir = static_dir / "svelte-build"
|
|
697
|
+
if svelte_build_dir.exists():
|
|
698
|
+
svelte_app_dir = svelte_build_dir / "_app"
|
|
699
|
+
if svelte_app_dir.exists():
|
|
700
|
+
# Serve _app assets with proper caching
|
|
701
|
+
async def app_assets_handler(request):
|
|
702
|
+
"""Serve Svelte _app assets."""
|
|
703
|
+
from aiohttp.web_fileresponse import FileResponse
|
|
704
|
+
|
|
705
|
+
rel_path = request.match_info["filepath"]
|
|
706
|
+
file_path = svelte_app_dir / rel_path
|
|
707
|
+
|
|
708
|
+
if not file_path.exists() or not file_path.is_file():
|
|
709
|
+
raise web.HTTPNotFound()
|
|
710
|
+
|
|
711
|
+
response = FileResponse(file_path)
|
|
712
|
+
|
|
713
|
+
# Add cache headers for immutable assets
|
|
714
|
+
if "/immutable/" in str(rel_path):
|
|
715
|
+
response.headers["Cache-Control"] = (
|
|
716
|
+
"public, max-age=31536000, immutable"
|
|
717
|
+
)
|
|
718
|
+
else:
|
|
719
|
+
response.headers["Cache-Control"] = (
|
|
720
|
+
"no-cache, no-store, must-revalidate"
|
|
721
|
+
)
|
|
722
|
+
|
|
723
|
+
return response
|
|
724
|
+
|
|
725
|
+
self.app.router.add_get("/_app/{filepath:.*}", app_assets_handler)
|
|
726
|
+
|
|
727
|
+
# Legacy static files (for backward compatibility)
|
|
551
728
|
if static_dir.exists():
|
|
552
729
|
|
|
553
730
|
async def static_handler(request):
|
|
554
|
-
"""Serve static files with cache-control headers for development."""
|
|
731
|
+
"""Serve legacy static files with cache-control headers for development."""
|
|
555
732
|
|
|
556
733
|
from aiohttp.web_fileresponse import FileResponse
|
|
557
734
|
|
|
@@ -576,10 +753,13 @@ class UnifiedMonitorServer:
|
|
|
576
753
|
|
|
577
754
|
self.app.router.add_get("/static/{filepath:.*}", static_handler)
|
|
578
755
|
|
|
579
|
-
#
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
756
|
+
# Log dashboard availability
|
|
757
|
+
if svelte_build_dir.exists():
|
|
758
|
+
self.logger.info(
|
|
759
|
+
f"✅ Svelte dashboard available at / (root) (build: {svelte_build_dir})"
|
|
760
|
+
)
|
|
761
|
+
else:
|
|
762
|
+
self.logger.warning(f"Svelte build not found at: {svelte_build_dir}")
|
|
583
763
|
|
|
584
764
|
self.logger.info("HTTP routes registered successfully")
|
|
585
765
|
|
|
@@ -691,11 +871,37 @@ class UnifiedMonitorServer:
|
|
|
691
871
|
async def _cleanup_async(self):
|
|
692
872
|
"""Cleanup async resources."""
|
|
693
873
|
try:
|
|
874
|
+
# Stop file observer if running
|
|
875
|
+
# STABILITY FIX: Ensure watcher is stopped and verify observer termination
|
|
876
|
+
if self.file_observer:
|
|
877
|
+
try:
|
|
878
|
+
# Stop the watcher first to cancel pending timers
|
|
879
|
+
if self.file_watcher:
|
|
880
|
+
self.file_watcher.stop()
|
|
881
|
+
|
|
882
|
+
# Stop the observer
|
|
883
|
+
self.file_observer.stop()
|
|
884
|
+
self.file_observer.join(timeout=2)
|
|
885
|
+
|
|
886
|
+
# Verify observer actually stopped
|
|
887
|
+
if self.file_observer.is_alive():
|
|
888
|
+
self.logger.warning("File observer did not stop cleanly")
|
|
889
|
+
|
|
890
|
+
self.logger.debug("File observer stopped")
|
|
891
|
+
except Exception as e:
|
|
892
|
+
self.logger.debug(f"Error stopping file observer: {e}")
|
|
893
|
+
finally:
|
|
894
|
+
self.file_observer = None
|
|
895
|
+
self.file_watcher = None
|
|
896
|
+
|
|
694
897
|
# Cancel heartbeat task if running
|
|
898
|
+
# STABILITY FIX: Add timeout to prevent infinite wait on cancellation
|
|
695
899
|
if self.heartbeat_task and not self.heartbeat_task.done():
|
|
696
900
|
self.heartbeat_task.cancel()
|
|
697
|
-
|
|
698
|
-
await self.heartbeat_task
|
|
901
|
+
try:
|
|
902
|
+
await asyncio.wait_for(self.heartbeat_task, timeout=2.0)
|
|
903
|
+
except (asyncio.CancelledError, asyncio.TimeoutError):
|
|
904
|
+
pass
|
|
699
905
|
self.logger.debug("Heartbeat task cancelled")
|
|
700
906
|
|
|
701
907
|
# Close the Socket.IO server first to stop accepting new connections
|
|
@@ -78,10 +78,13 @@ class NormalizedEvent:
|
|
|
78
78
|
subtype: str = "" # Specific event type
|
|
79
79
|
timestamp: str = "" # ISO format timestamp
|
|
80
80
|
data: Dict[str, Any] = field(default_factory=dict) # Event payload
|
|
81
|
+
correlation_id: Optional[str] = (
|
|
82
|
+
None # For correlating related events (e.g., pre_tool/post_tool)
|
|
83
|
+
)
|
|
81
84
|
|
|
82
85
|
def to_dict(self) -> Dict[str, Any]:
|
|
83
86
|
"""Convert to dictionary for emission."""
|
|
84
|
-
|
|
87
|
+
result = {
|
|
85
88
|
"event": self.event,
|
|
86
89
|
"source": self.source,
|
|
87
90
|
"type": self.type,
|
|
@@ -89,6 +92,10 @@ class NormalizedEvent:
|
|
|
89
92
|
"timestamp": self.timestamp,
|
|
90
93
|
"data": self.data,
|
|
91
94
|
}
|
|
95
|
+
# Include correlation_id if present
|
|
96
|
+
if self.correlation_id:
|
|
97
|
+
result["correlation_id"] = self.correlation_id
|
|
98
|
+
return result
|
|
92
99
|
|
|
93
100
|
|
|
94
101
|
class EventNormalizer:
|
|
@@ -218,6 +225,11 @@ class EventNormalizer:
|
|
|
218
225
|
# Get or generate timestamp
|
|
219
226
|
timestamp = self._extract_timestamp(event_data)
|
|
220
227
|
|
|
228
|
+
# Extract correlation_id if present
|
|
229
|
+
correlation_id = None
|
|
230
|
+
if isinstance(event_data, dict):
|
|
231
|
+
correlation_id = event_data.get("correlation_id")
|
|
232
|
+
|
|
221
233
|
# Create normalized event
|
|
222
234
|
normalized = NormalizedEvent(
|
|
223
235
|
event="claude_event",
|
|
@@ -226,6 +238,7 @@ class EventNormalizer:
|
|
|
226
238
|
subtype=subtype,
|
|
227
239
|
timestamp=timestamp,
|
|
228
240
|
data=data,
|
|
241
|
+
correlation_id=correlation_id,
|
|
229
242
|
)
|
|
230
243
|
|
|
231
244
|
self.stats["normalized"] += 1
|
|
@@ -281,6 +294,7 @@ class EventNormalizer:
|
|
|
281
294
|
"timestamp", datetime.now(timezone.utc).isoformat()
|
|
282
295
|
),
|
|
283
296
|
data=event_data.get("data", {}),
|
|
297
|
+
correlation_id=event_data.get("correlation_id"),
|
|
284
298
|
)
|
|
285
299
|
|
|
286
300
|
def _extract_event_info(self, event_data: Any) -> Tuple[str, str, Dict[str, Any]]:
|