claude-mpm 4.2.28__py3-none-any.whl → 4.2.32__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/templates/agentic-coder-optimizer.json +233 -0
- claude_mpm/agents/templates/agentic-coder-optimizer.md +44 -0
- claude_mpm/agents/templates/agentic_coder_optimizer.json +20 -4
- claude_mpm/services/cli/unified_dashboard_manager.py +46 -84
- claude_mpm/services/monitor/daemon.py +53 -163
- claude_mpm/services/monitor/daemon_manager.py +739 -0
- {claude_mpm-4.2.28.dist-info → claude_mpm-4.2.32.dist-info}/METADATA +1 -1
- {claude_mpm-4.2.28.dist-info → claude_mpm-4.2.32.dist-info}/RECORD +13 -10
- {claude_mpm-4.2.28.dist-info → claude_mpm-4.2.32.dist-info}/WHEEL +0 -0
- {claude_mpm-4.2.28.dist-info → claude_mpm-4.2.32.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.2.28.dist-info → claude_mpm-4.2.32.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.2.28.dist-info → claude_mpm-4.2.32.dist-info}/top_level.txt +0 -0
@@ -24,6 +24,7 @@ from typing import Optional
|
|
24
24
|
|
25
25
|
from ...core.logging_config import get_logger
|
26
26
|
from ..hook_installer_service import HookInstallerService
|
27
|
+
from .daemon_manager import DaemonManager
|
27
28
|
from .management.health import HealthMonitor
|
28
29
|
from .management.lifecycle import DaemonLifecycle
|
29
30
|
from .server import UnifiedMonitorServer
|
@@ -58,7 +59,15 @@ class UnifiedMonitorDaemon:
|
|
58
59
|
self.daemon_mode = daemon_mode
|
59
60
|
self.logger = get_logger(__name__)
|
60
61
|
|
61
|
-
#
|
62
|
+
# Use new consolidated DaemonManager for all daemon operations
|
63
|
+
self.daemon_manager = DaemonManager(
|
64
|
+
port=port,
|
65
|
+
host=host,
|
66
|
+
pid_file=pid_file or self._get_default_pid_file(),
|
67
|
+
log_file=log_file,
|
68
|
+
)
|
69
|
+
|
70
|
+
# Keep lifecycle for backward compatibility (delegates to daemon_manager)
|
62
71
|
self.lifecycle = DaemonLifecycle(
|
63
72
|
pid_file=pid_file or self._get_default_pid_file(),
|
64
73
|
log_file=log_file,
|
@@ -104,84 +113,13 @@ class UnifiedMonitorDaemon:
|
|
104
113
|
|
105
114
|
def _cleanup_port_conflicts(self) -> bool:
|
106
115
|
"""Try to clean up any processes using our port.
|
107
|
-
|
116
|
+
|
117
|
+
Delegates to the consolidated DaemonManager for consistent behavior.
|
118
|
+
|
108
119
|
Returns:
|
109
120
|
True if cleanup was successful, False otherwise
|
110
121
|
"""
|
111
|
-
|
112
|
-
# Find process using the port
|
113
|
-
import subprocess
|
114
|
-
result = subprocess.run(
|
115
|
-
["lsof", "-ti", f":{self.port}"],
|
116
|
-
capture_output=True,
|
117
|
-
text=True
|
118
|
-
)
|
119
|
-
|
120
|
-
if result.returncode == 0 and result.stdout.strip():
|
121
|
-
pids = result.stdout.strip().split('\n')
|
122
|
-
for pid_str in pids:
|
123
|
-
try:
|
124
|
-
pid = int(pid_str.strip())
|
125
|
-
self.logger.info(f"Found process {pid} using port {self.port}")
|
126
|
-
|
127
|
-
# Check if it's a claude-mpm process
|
128
|
-
process_info = subprocess.run(
|
129
|
-
["ps", "-p", str(pid), "-o", "comm="],
|
130
|
-
capture_output=True,
|
131
|
-
text=True
|
132
|
-
)
|
133
|
-
|
134
|
-
if "python" in process_info.stdout.lower() or "claude" in process_info.stdout.lower():
|
135
|
-
self.logger.info(f"Killing process {pid} (appears to be Python/Claude related)")
|
136
|
-
os.kill(pid, signal.SIGTERM)
|
137
|
-
time.sleep(1)
|
138
|
-
|
139
|
-
# Check if still alive
|
140
|
-
try:
|
141
|
-
os.kill(pid, 0)
|
142
|
-
# Still alive, force kill
|
143
|
-
self.logger.warning(f"Process {pid} didn't terminate, force killing")
|
144
|
-
os.kill(pid, signal.SIGKILL)
|
145
|
-
time.sleep(1)
|
146
|
-
except ProcessLookupError:
|
147
|
-
pass
|
148
|
-
else:
|
149
|
-
self.logger.warning(f"Process {pid} is not a Claude MPM process: {process_info.stdout}")
|
150
|
-
return False
|
151
|
-
except (ValueError, ProcessLookupError) as e:
|
152
|
-
self.logger.debug(f"Error handling PID {pid_str}: {e}")
|
153
|
-
continue
|
154
|
-
|
155
|
-
return True
|
156
|
-
|
157
|
-
except FileNotFoundError:
|
158
|
-
# lsof not available, try alternative method
|
159
|
-
self.logger.debug("lsof not available, using alternative cleanup")
|
160
|
-
|
161
|
-
# Check if there's an orphaned service we can identify
|
162
|
-
is_ours, pid = self.lifecycle.is_our_service(self.host)
|
163
|
-
if is_ours and pid:
|
164
|
-
try:
|
165
|
-
self.logger.info(f"Killing orphaned Claude MPM service (PID: {pid})")
|
166
|
-
os.kill(pid, signal.SIGTERM)
|
167
|
-
time.sleep(1)
|
168
|
-
|
169
|
-
# Check if still alive
|
170
|
-
try:
|
171
|
-
os.kill(pid, 0)
|
172
|
-
os.kill(pid, signal.SIGKILL)
|
173
|
-
time.sleep(1)
|
174
|
-
except ProcessLookupError:
|
175
|
-
pass
|
176
|
-
|
177
|
-
return True
|
178
|
-
except Exception as e:
|
179
|
-
self.logger.error(f"Failed to kill process: {e}")
|
180
|
-
|
181
|
-
except Exception as e:
|
182
|
-
self.logger.error(f"Error during port cleanup: {e}")
|
183
|
-
|
184
|
-
return False
|
122
|
+
return self.daemon_manager.cleanup_port_conflicts()
|
185
123
|
|
186
124
|
def _start_daemon(self, force_restart: bool = False) -> bool:
|
187
125
|
"""Start as background daemon process.
|
@@ -191,108 +129,60 @@ class UnifiedMonitorDaemon:
|
|
191
129
|
"""
|
192
130
|
self.logger.info("Starting unified monitor daemon in background mode")
|
193
131
|
|
194
|
-
#
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
132
|
+
# Always use daemon manager for cleanup first
|
133
|
+
# This ensures consistent behavior and prevents race conditions
|
134
|
+
if force_restart:
|
135
|
+
self.logger.info(
|
136
|
+
"Force restart requested, cleaning up any existing processes..."
|
137
|
+
)
|
138
|
+
if not self.daemon_manager.cleanup_port_conflicts(max_retries=3):
|
139
|
+
self.logger.error(f"Failed to clean up port {self.port}")
|
140
|
+
return False
|
141
|
+
# Wait for port to be fully released
|
142
|
+
time.sleep(2)
|
204
143
|
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
# Stop the existing daemon
|
210
|
-
if self.lifecycle.stop_daemon():
|
211
|
-
# Wait a moment for port to be released
|
212
|
-
time.sleep(2)
|
213
|
-
else:
|
214
|
-
self.logger.error("Failed to stop existing daemon for restart")
|
215
|
-
return False
|
216
|
-
else:
|
217
|
-
self.logger.warning(
|
218
|
-
f"Port {self.port} is in use by another service (PID: {existing_pid}). Cannot force restart."
|
219
|
-
)
|
220
|
-
self.logger.info(
|
221
|
-
"To restart the claude-mpm monitor, first stop the other service or use a different port."
|
222
|
-
)
|
223
|
-
return False
|
224
|
-
else:
|
144
|
+
# Check if already running via daemon manager
|
145
|
+
if self.daemon_manager.is_running():
|
146
|
+
existing_pid = self.daemon_manager.get_pid()
|
147
|
+
if not force_restart:
|
225
148
|
self.logger.warning(f"Daemon already running with PID {existing_pid}")
|
226
149
|
return False
|
150
|
+
# Force restart was already handled above
|
227
151
|
|
228
|
-
# Check for
|
229
|
-
|
230
|
-
|
231
|
-
|
152
|
+
# Check for our service on the port
|
153
|
+
is_ours, pid = self.daemon_manager.is_our_service()
|
154
|
+
if is_ours and pid and not force_restart:
|
155
|
+
self.logger.warning(
|
156
|
+
f"Our service already running on port {self.port} (PID: {pid})"
|
232
157
|
)
|
233
|
-
|
234
|
-
if is_ours and pid:
|
235
|
-
self.logger.info(
|
236
|
-
f"Found orphaned claude-mpm monitor service (PID: {pid}), force restarting"
|
237
|
-
)
|
238
|
-
# Try to kill the orphaned process
|
239
|
-
try:
|
240
|
-
os.kill(pid, signal.SIGTERM)
|
241
|
-
# Wait for it to exit
|
242
|
-
for _ in range(10):
|
243
|
-
try:
|
244
|
-
os.kill(pid, 0) # Check if still exists
|
245
|
-
time.sleep(0.5)
|
246
|
-
except ProcessLookupError:
|
247
|
-
break
|
248
|
-
else:
|
249
|
-
# Force kill if still running
|
250
|
-
os.kill(pid, signal.SIGKILL)
|
251
|
-
time.sleep(1)
|
252
|
-
except Exception as e:
|
253
|
-
self.logger.error(f"Failed to kill orphaned process: {e}")
|
254
|
-
return False
|
255
|
-
|
256
|
-
# Check port availability and clean up if needed
|
257
|
-
port_available, error_msg = self.lifecycle.verify_port_available(self.host)
|
258
|
-
if not port_available:
|
259
|
-
self.logger.warning(f"Port {self.port} is not available: {error_msg}")
|
260
|
-
|
261
|
-
# Try to identify and kill any process using the port
|
262
|
-
self.logger.info("Attempting to clean up processes on port...")
|
263
|
-
cleaned = self._cleanup_port_conflicts()
|
264
|
-
|
265
|
-
if cleaned:
|
266
|
-
# Wait longer for port to be released to avoid race conditions
|
267
|
-
time.sleep(3)
|
268
|
-
# Check again
|
269
|
-
port_available, error_msg = self.lifecycle.verify_port_available(self.host)
|
270
|
-
|
271
|
-
if not port_available:
|
272
|
-
self.logger.error(f"Port {self.port} is still not available after cleanup: {error_msg}")
|
273
|
-
print(f"Error: {error_msg}", file=sys.stderr)
|
274
|
-
print(f"Try 'claude-mpm monitor stop' or use --force flag", file=sys.stderr)
|
275
|
-
return False
|
158
|
+
return False
|
276
159
|
|
277
160
|
# Wait for any pre-warming threads to complete before forking
|
278
161
|
self._wait_for_prewarm_completion()
|
279
162
|
|
280
|
-
#
|
281
|
-
|
163
|
+
# Use daemon manager's daemonize which includes cleanup
|
164
|
+
self.daemon_manager.startup_status_file = None # Reset status file
|
165
|
+
success = self.daemon_manager.daemonize()
|
282
166
|
if not success:
|
283
167
|
return False
|
284
168
|
|
169
|
+
# We're now in the daemon process
|
170
|
+
# Update our PID references
|
171
|
+
self.lifecycle.pid_file = self.daemon_manager.pid_file
|
172
|
+
|
285
173
|
# Start the server in daemon mode
|
286
|
-
# This will run in the child process
|
287
174
|
try:
|
288
175
|
result = self._run_server()
|
289
176
|
if not result:
|
290
177
|
# Report failure before exiting
|
291
|
-
self.
|
178
|
+
self.daemon_manager._report_startup_error("Failed to start server")
|
179
|
+
else:
|
180
|
+
# Report success
|
181
|
+
self.daemon_manager._report_startup_success()
|
292
182
|
return result
|
293
183
|
except Exception as e:
|
294
184
|
# Report any exceptions during startup
|
295
|
-
self.
|
185
|
+
self.daemon_manager._report_startup_error(f"Server startup exception: {e}")
|
296
186
|
raise
|
297
187
|
|
298
188
|
def _start_foreground(self, force_restart: bool = False) -> bool:
|
@@ -302,12 +192,12 @@ class UnifiedMonitorDaemon:
|
|
302
192
|
force_restart: If True, restart existing service if it's ours
|
303
193
|
"""
|
304
194
|
self.logger.info(f"Starting unified monitor daemon on {self.host}:{self.port}")
|
305
|
-
|
306
|
-
#
|
195
|
+
|
196
|
+
# Use daemon manager for consistent port cleanup
|
307
197
|
# This helps with race conditions where old processes haven't fully released the port
|
308
198
|
if force_restart:
|
309
199
|
self.logger.info("Force restart requested, cleaning up port conflicts...")
|
310
|
-
self.
|
200
|
+
self.daemon_manager.cleanup_port_conflicts(max_retries=2)
|
311
201
|
time.sleep(1) # Brief pause to ensure port is released
|
312
202
|
|
313
203
|
# Check if already running (check PID file even in foreground mode)
|
@@ -319,7 +209,7 @@ class UnifiedMonitorDaemon:
|
|
319
209
|
self.logger.debug(
|
320
210
|
f"Checking if existing daemon (PID: {existing_pid}) is our service..."
|
321
211
|
)
|
322
|
-
is_ours, detected_pid = self.
|
212
|
+
is_ours, detected_pid = self.daemon_manager.is_our_service()
|
323
213
|
|
324
214
|
if is_ours:
|
325
215
|
self.logger.info(
|
@@ -351,7 +241,7 @@ class UnifiedMonitorDaemon:
|
|
351
241
|
self.logger.debug(
|
352
242
|
"No PID file found, checking for orphaned claude-mpm service..."
|
353
243
|
)
|
354
|
-
is_ours, pid = self.
|
244
|
+
is_ours, pid = self.daemon_manager.is_our_service()
|
355
245
|
if is_ours and pid:
|
356
246
|
self.logger.info(
|
357
247
|
f"Found orphaned claude-mpm monitor service (PID: {pid}), force restarting"
|