claude-mpm 3.4.0__py3-none-any.whl → 3.4.2__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/cli/commands/memory.py +6 -1
- claude_mpm/core/config.py +160 -0
- claude_mpm/hooks/claude_hooks/hook_wrapper.sh +1 -1
- claude_mpm/scripts/socketio_daemon.py +49 -9
- claude_mpm/scripts/socketio_server_manager.py +370 -45
- claude_mpm/services/__init__.py +18 -0
- claude_mpm/services/agent_memory_manager.py +7 -5
- claude_mpm/services/exceptions.py +677 -0
- claude_mpm/services/health_monitor.py +892 -0
- claude_mpm/services/memory_builder.py +4 -2
- claude_mpm/services/memory_optimizer.py +6 -2
- claude_mpm/services/recovery_manager.py +670 -0
- claude_mpm/services/socketio_server.py +188 -11
- claude_mpm/services/standalone_socketio_server.py +703 -34
- {claude_mpm-3.4.0.dist-info → claude_mpm-3.4.2.dist-info}/METADATA +1 -1
- {claude_mpm-3.4.0.dist-info → claude_mpm-3.4.2.dist-info}/RECORD +21 -18
- /claude_mpm/{web → dashboard}/open_dashboard.py +0 -0
- {claude_mpm-3.4.0.dist-info → claude_mpm-3.4.2.dist-info}/WHEEL +0 -0
- {claude_mpm-3.4.0.dist-info → claude_mpm-3.4.2.dist-info}/entry_points.txt +0 -0
- {claude_mpm-3.4.0.dist-info → claude_mpm-3.4.2.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-3.4.0.dist-info → claude_mpm-3.4.2.dist-info}/top_level.txt +0 -0
|
@@ -31,6 +31,12 @@ try:
|
|
|
31
31
|
except ImportError:
|
|
32
32
|
REQUESTS_AVAILABLE = False
|
|
33
33
|
|
|
34
|
+
try:
|
|
35
|
+
import psutil
|
|
36
|
+
PSUTIL_AVAILABLE = True
|
|
37
|
+
except ImportError:
|
|
38
|
+
PSUTIL_AVAILABLE = False
|
|
39
|
+
|
|
34
40
|
|
|
35
41
|
class ServerManager:
|
|
36
42
|
"""Manages Socket.IO server instances across different deployment modes."""
|
|
@@ -40,30 +46,49 @@ class ServerManager:
|
|
|
40
46
|
self.max_instances = 5
|
|
41
47
|
self.script_dir = Path(__file__).parent
|
|
42
48
|
self.project_root = self.script_dir.parent
|
|
49
|
+
# Daemon PID file location (used by socketio_daemon.py)
|
|
50
|
+
self.daemon_pidfile_path = Path.home() / ".claude-mpm" / "socketio-server.pid"
|
|
51
|
+
# Standalone server PID file location pattern
|
|
52
|
+
self.standalone_pidfile_pattern = "/tmp/claude_mpm_socketio_{port}.pid"
|
|
43
53
|
|
|
44
54
|
def get_server_info(self, port: int) -> Optional[Dict]:
|
|
45
|
-
"""Get server information from a running instance."""
|
|
55
|
+
"""Get server information from a running instance with daemon compatibility."""
|
|
46
56
|
if not REQUESTS_AVAILABLE:
|
|
47
|
-
return
|
|
57
|
+
return self._check_daemon_fallback(port)
|
|
48
58
|
|
|
49
59
|
try:
|
|
50
60
|
response = requests.get(f"http://localhost:{port}/health", timeout=2.0)
|
|
51
61
|
if response.status_code == 200:
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
62
|
+
data = response.json()
|
|
63
|
+
# Check if this is a daemon-style response (no 'pid' field)
|
|
64
|
+
if 'pid' not in data and 'status' in data:
|
|
65
|
+
# Try to get PID from daemon PID file
|
|
66
|
+
daemon_pid = self._get_daemon_pid()
|
|
67
|
+
if daemon_pid:
|
|
68
|
+
data['pid'] = daemon_pid
|
|
69
|
+
data['management_style'] = 'daemon'
|
|
70
|
+
return data
|
|
71
|
+
except Exception as e:
|
|
72
|
+
# If HTTP fails, try daemon fallback
|
|
73
|
+
return self._check_daemon_fallback(port)
|
|
55
74
|
return None
|
|
56
75
|
|
|
57
76
|
def list_running_servers(self) -> List[Dict]:
|
|
58
|
-
"""List all running Socket.IO servers."""
|
|
77
|
+
"""List all running Socket.IO servers including daemon-style servers."""
|
|
59
78
|
running_servers = []
|
|
60
79
|
|
|
80
|
+
# Check standard port range
|
|
61
81
|
for port in range(self.base_port, self.base_port + self.max_instances):
|
|
62
82
|
server_info = self.get_server_info(port)
|
|
63
83
|
if server_info:
|
|
64
84
|
server_info['port'] = port
|
|
65
85
|
running_servers.append(server_info)
|
|
66
86
|
|
|
87
|
+
# Also check for daemon-style server specifically
|
|
88
|
+
daemon_info = self._get_daemon_server_info()
|
|
89
|
+
if daemon_info and not any(s['port'] == daemon_info.get('port', self.base_port) for s in running_servers):
|
|
90
|
+
running_servers.append(daemon_info)
|
|
91
|
+
|
|
67
92
|
return running_servers
|
|
68
93
|
|
|
69
94
|
def find_available_port(self, start_port: int = None) -> int:
|
|
@@ -78,7 +103,7 @@ class ServerManager:
|
|
|
78
103
|
|
|
79
104
|
def start_server(self, port: int = None, server_id: str = None,
|
|
80
105
|
host: str = "localhost") -> bool:
|
|
81
|
-
"""Start a standalone Socket.IO server."""
|
|
106
|
+
"""Start a standalone Socket.IO server with conflict detection."""
|
|
82
107
|
|
|
83
108
|
# Find available port if not specified
|
|
84
109
|
if port is None:
|
|
@@ -89,10 +114,26 @@ class ServerManager:
|
|
|
89
114
|
return False
|
|
90
115
|
|
|
91
116
|
# Check if server is already running on this port
|
|
92
|
-
|
|
93
|
-
|
|
117
|
+
existing_server = self.get_server_info(port)
|
|
118
|
+
if existing_server:
|
|
119
|
+
management_style = existing_server.get('management_style', 'http')
|
|
120
|
+
server_id_existing = existing_server.get('server_id', 'unknown')
|
|
121
|
+
|
|
122
|
+
print(f"❌ Server already running on port {port}")
|
|
123
|
+
print(f" Existing server: {server_id_existing} ({management_style}-managed)")
|
|
124
|
+
|
|
125
|
+
if management_style == 'daemon':
|
|
126
|
+
print(f"💡 To stop daemon server: {self.project_root / 'src' / 'claude_mpm' / 'scripts' / 'socketio_daemon.py'} stop")
|
|
127
|
+
else:
|
|
128
|
+
print(f"💡 To stop server: {sys.executable} {__file__} stop --port {port}")
|
|
129
|
+
|
|
94
130
|
return False
|
|
95
131
|
|
|
132
|
+
# Warn if daemon server exists on default port but we're starting on different port
|
|
133
|
+
if port != self.base_port and self._get_daemon_pid():
|
|
134
|
+
print(f"⚠️ Warning: Daemon server is running on port {self.base_port}, you're starting on port {port}")
|
|
135
|
+
print(f" This may cause conflicts. Consider stopping daemon first.")
|
|
136
|
+
|
|
96
137
|
# Try different ways to start the server based on deployment
|
|
97
138
|
success = False
|
|
98
139
|
|
|
@@ -158,13 +199,20 @@ class ServerManager:
|
|
|
158
199
|
|
|
159
200
|
if success:
|
|
160
201
|
print(f"✅ Server started successfully on {host}:{port}")
|
|
202
|
+
print(f"💡 Management commands:")
|
|
203
|
+
print(f" Status: {sys.executable} {__file__} status")
|
|
204
|
+
print(f" Stop: {sys.executable} {__file__} stop --port {port}")
|
|
161
205
|
return True
|
|
162
206
|
else:
|
|
163
207
|
print(f"❌ Failed to start server on {host}:{port}")
|
|
208
|
+
print(f"💡 Troubleshooting:")
|
|
209
|
+
print(f" • Check if port {port} is already in use: lsof -i :{port}")
|
|
210
|
+
print(f" • Check server status: {sys.executable} {__file__} status")
|
|
211
|
+
print(f" • Try different port: {sys.executable} {__file__} start --port {port + 1}")
|
|
164
212
|
return False
|
|
165
213
|
|
|
166
214
|
def stop_server(self, port: int = None, server_id: str = None) -> bool:
|
|
167
|
-
"""Stop a running Socket.IO server."""
|
|
215
|
+
"""Stop a running Socket.IO server with daemon compatibility."""
|
|
168
216
|
|
|
169
217
|
if port is None and server_id is None:
|
|
170
218
|
print("Must specify either port or server_id")
|
|
@@ -185,35 +233,56 @@ class ServerManager:
|
|
|
185
233
|
# Get server info
|
|
186
234
|
server_info = self.get_server_info(port)
|
|
187
235
|
if not server_info:
|
|
188
|
-
|
|
189
|
-
return
|
|
236
|
+
# Try daemon-specific stop as fallback
|
|
237
|
+
return self._try_daemon_stop(port)
|
|
238
|
+
|
|
239
|
+
# Determine management style
|
|
240
|
+
management_style = server_info.get('management_style', 'http')
|
|
190
241
|
|
|
191
|
-
# Try
|
|
242
|
+
# Try HTTP-based stop first
|
|
192
243
|
pid = server_info.get('pid')
|
|
193
244
|
if pid:
|
|
194
245
|
try:
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
246
|
+
# Validate PID before attempting to kill
|
|
247
|
+
if self._validate_pid(pid):
|
|
248
|
+
os.kill(pid, signal.SIGTERM)
|
|
249
|
+
print(f"✅ Sent termination signal to server (PID: {pid})")
|
|
250
|
+
|
|
251
|
+
# Wait for server to stop
|
|
252
|
+
for i in range(10):
|
|
253
|
+
time.sleep(1)
|
|
254
|
+
if not self.get_server_info(port):
|
|
255
|
+
print(f"✅ Server stopped successfully")
|
|
256
|
+
return True
|
|
257
|
+
if i == 5: # After 5 seconds, show progress
|
|
258
|
+
print(f"⏳ Waiting for server to stop...")
|
|
259
|
+
|
|
260
|
+
# Force kill if still running
|
|
261
|
+
try:
|
|
262
|
+
if self._validate_pid(pid):
|
|
263
|
+
os.kill(pid, signal.SIGKILL)
|
|
264
|
+
print(f"⚠️ Force killed server (PID: {pid})")
|
|
265
|
+
return True
|
|
266
|
+
except OSError:
|
|
267
|
+
pass
|
|
268
|
+
|
|
269
|
+
else:
|
|
270
|
+
print(f"⚠️ PID {pid} is no longer valid, trying daemon stop...")
|
|
271
|
+
return self._try_daemon_stop(port)
|
|
212
272
|
|
|
213
273
|
except OSError as e:
|
|
214
|
-
print(f"Error stopping server: {e}")
|
|
274
|
+
print(f"Error stopping server via PID {pid}: {e}")
|
|
275
|
+
if management_style == 'daemon':
|
|
276
|
+
print("🔄 Trying daemon-style stop...")
|
|
277
|
+
return self._try_daemon_stop(port)
|
|
278
|
+
|
|
279
|
+
# If HTTP method failed, try daemon stop
|
|
280
|
+
if management_style == 'daemon' or not pid:
|
|
281
|
+
print("🔄 Attempting daemon-style stop...")
|
|
282
|
+
return self._try_daemon_stop(port)
|
|
215
283
|
|
|
216
284
|
print(f"❌ Failed to stop server on port {port}")
|
|
285
|
+
print(f"💡 Try using the socketio_daemon.py stop command if this is a daemon-managed server")
|
|
217
286
|
return False
|
|
218
287
|
|
|
219
288
|
def restart_server(self, port: int = None, server_id: str = None) -> bool:
|
|
@@ -232,11 +301,15 @@ class ServerManager:
|
|
|
232
301
|
return False
|
|
233
302
|
|
|
234
303
|
def status(self, verbose: bool = False) -> None:
|
|
235
|
-
"""Show status of all Socket.IO servers."""
|
|
304
|
+
"""Show status of all Socket.IO servers with management style info."""
|
|
236
305
|
running_servers = self.list_running_servers()
|
|
237
306
|
|
|
238
307
|
if not running_servers:
|
|
239
308
|
print("No Socket.IO servers currently running")
|
|
309
|
+
print()
|
|
310
|
+
print("💡 Management options:")
|
|
311
|
+
print(f" • Start with manager: {sys.executable} {__file__} start")
|
|
312
|
+
print(f" • Start with daemon: {self.project_root / 'src' / 'claude_mpm' / 'scripts' / 'socketio_daemon.py'} start")
|
|
240
313
|
return
|
|
241
314
|
|
|
242
315
|
print(f"Found {len(running_servers)} running server(s):")
|
|
@@ -248,10 +321,15 @@ class ServerManager:
|
|
|
248
321
|
version = server.get('server_version', 'unknown')
|
|
249
322
|
uptime = server.get('uptime_seconds', 0)
|
|
250
323
|
clients = server.get('clients_connected', 0)
|
|
324
|
+
management_style = server.get('management_style', 'http')
|
|
325
|
+
|
|
326
|
+
# Different icons based on management style
|
|
327
|
+
icon = "🖥️" if management_style == 'http' else "🔧"
|
|
251
328
|
|
|
252
|
-
print(f"
|
|
329
|
+
print(f"{icon} Server ID: {server_id}")
|
|
253
330
|
print(f" Port: {port}")
|
|
254
331
|
print(f" Version: {version}")
|
|
332
|
+
print(f" Management: {management_style}")
|
|
255
333
|
print(f" Uptime: {self._format_uptime(uptime)}")
|
|
256
334
|
print(f" Clients: {clients}")
|
|
257
335
|
|
|
@@ -259,34 +337,61 @@ class ServerManager:
|
|
|
259
337
|
print(f" PID: {server.get('pid', 'unknown')}")
|
|
260
338
|
print(f" Host: {server.get('host', 'unknown')}")
|
|
261
339
|
|
|
262
|
-
#
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
340
|
+
# Show appropriate stop command
|
|
341
|
+
if management_style == 'daemon':
|
|
342
|
+
print(f" Stop command: {self.project_root / 'src' / 'claude_mpm' / 'scripts' / 'socketio_daemon.py'} stop")
|
|
343
|
+
else:
|
|
344
|
+
print(f" Stop command: {sys.executable} {__file__} stop --port {port}")
|
|
345
|
+
|
|
346
|
+
# Get additional stats (only for HTTP-style servers)
|
|
347
|
+
if management_style == 'http':
|
|
348
|
+
stats = self._get_server_stats(port)
|
|
349
|
+
if stats:
|
|
350
|
+
events_processed = stats.get('events', {}).get('total_processed', 0)
|
|
351
|
+
clients_served = stats.get('connections', {}).get('total_served', 0)
|
|
352
|
+
print(f" Events processed: {events_processed}")
|
|
353
|
+
print(f" Total clients served: {clients_served}")
|
|
269
354
|
|
|
270
355
|
print()
|
|
271
356
|
|
|
272
357
|
def health_check(self, port: int = None) -> bool:
|
|
273
|
-
"""Perform health check on server(s)."""
|
|
358
|
+
"""Perform health check on server(s) with management style awareness."""
|
|
274
359
|
|
|
275
360
|
if port:
|
|
276
361
|
# Check specific server
|
|
277
362
|
server_info = self.get_server_info(port)
|
|
278
363
|
if server_info:
|
|
279
364
|
status = server_info.get('status', 'unknown')
|
|
280
|
-
|
|
281
|
-
|
|
365
|
+
management_style = server_info.get('management_style', 'http')
|
|
366
|
+
server_id = server_info.get('server_id', 'unknown')
|
|
367
|
+
|
|
368
|
+
print(f"Server {server_id} on port {port}: {status} ({management_style}-managed)")
|
|
369
|
+
|
|
370
|
+
# Additional health info for daemon servers
|
|
371
|
+
if management_style == 'daemon':
|
|
372
|
+
pid = server_info.get('pid')
|
|
373
|
+
if pid and self._validate_pid(pid):
|
|
374
|
+
print(f" ✅ Process {pid} is running")
|
|
375
|
+
else:
|
|
376
|
+
print(f" ❌ Process {pid} is not running")
|
|
377
|
+
return False
|
|
378
|
+
|
|
379
|
+
return status in ['healthy', 'running']
|
|
282
380
|
else:
|
|
283
381
|
print(f"No server found on port {port}")
|
|
382
|
+
# Try daemon fallback for default port
|
|
383
|
+
if port == self.base_port:
|
|
384
|
+
daemon_info = self._get_daemon_server_info()
|
|
385
|
+
if daemon_info:
|
|
386
|
+
print(f" Found daemon server: {daemon_info['server_id']}")
|
|
387
|
+
return True
|
|
284
388
|
return False
|
|
285
389
|
else:
|
|
286
390
|
# Check all servers
|
|
287
391
|
running_servers = self.list_running_servers()
|
|
288
392
|
if not running_servers:
|
|
289
393
|
print("No servers running")
|
|
394
|
+
print(f"💡 Start a server with: {sys.executable} {__file__} start")
|
|
290
395
|
return False
|
|
291
396
|
|
|
292
397
|
all_healthy = True
|
|
@@ -294,8 +399,14 @@ class ServerManager:
|
|
|
294
399
|
port = server['port']
|
|
295
400
|
status = server.get('status', 'unknown')
|
|
296
401
|
server_id = server.get('server_id', 'unknown')
|
|
297
|
-
|
|
298
|
-
|
|
402
|
+
management_style = server.get('management_style', 'http')
|
|
403
|
+
|
|
404
|
+
health_status = status in ['healthy', 'running']
|
|
405
|
+
icon = "✅" if health_status else "❌"
|
|
406
|
+
|
|
407
|
+
print(f"{icon} Server {server_id} (port {port}): {status} ({management_style}-managed)")
|
|
408
|
+
|
|
409
|
+
if not health_status:
|
|
299
410
|
all_healthy = False
|
|
300
411
|
|
|
301
412
|
return all_healthy
|
|
@@ -342,6 +453,213 @@ class ServerManager:
|
|
|
342
453
|
except Exception:
|
|
343
454
|
pass
|
|
344
455
|
return None
|
|
456
|
+
|
|
457
|
+
def _check_daemon_fallback(self, port: int) -> Optional[Dict]:
|
|
458
|
+
"""Check for daemon-style server when HTTP fails."""
|
|
459
|
+
if port == self.base_port: # Only check daemon for default port
|
|
460
|
+
return self._get_daemon_server_info()
|
|
461
|
+
return None
|
|
462
|
+
|
|
463
|
+
def _get_daemon_pid(self) -> Optional[int]:
|
|
464
|
+
"""Get PID from daemon PID file."""
|
|
465
|
+
try:
|
|
466
|
+
if self.daemon_pidfile_path.exists():
|
|
467
|
+
with open(self.daemon_pidfile_path, 'r') as f:
|
|
468
|
+
content = f.read().strip()
|
|
469
|
+
if content.isdigit():
|
|
470
|
+
pid = int(content)
|
|
471
|
+
# Validate the PID exists
|
|
472
|
+
if self._validate_pid(pid):
|
|
473
|
+
return pid
|
|
474
|
+
except Exception:
|
|
475
|
+
pass
|
|
476
|
+
return None
|
|
477
|
+
|
|
478
|
+
def _get_daemon_server_info(self) -> Optional[Dict]:
|
|
479
|
+
"""Get server info for daemon-style server."""
|
|
480
|
+
daemon_pid = self._get_daemon_pid()
|
|
481
|
+
if daemon_pid:
|
|
482
|
+
# Basic server info for daemon
|
|
483
|
+
info = {
|
|
484
|
+
'pid': daemon_pid,
|
|
485
|
+
'server_id': 'daemon-socketio',
|
|
486
|
+
'management_style': 'daemon',
|
|
487
|
+
'status': 'running',
|
|
488
|
+
'port': self.base_port,
|
|
489
|
+
'server_version': 'daemon-managed'
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
# Try to get additional process info if psutil is available
|
|
493
|
+
if PSUTIL_AVAILABLE:
|
|
494
|
+
try:
|
|
495
|
+
process = psutil.Process(daemon_pid)
|
|
496
|
+
info.update({
|
|
497
|
+
'uptime_seconds': time.time() - process.create_time(),
|
|
498
|
+
'host': 'localhost',
|
|
499
|
+
'process_name': process.name()
|
|
500
|
+
})
|
|
501
|
+
except:
|
|
502
|
+
pass
|
|
503
|
+
|
|
504
|
+
return info
|
|
505
|
+
return None
|
|
506
|
+
|
|
507
|
+
def _validate_pid(self, pid: int) -> bool:
|
|
508
|
+
"""Validate that a PID represents a running process."""
|
|
509
|
+
try:
|
|
510
|
+
# Check if process exists
|
|
511
|
+
os.kill(pid, 0)
|
|
512
|
+
return True
|
|
513
|
+
except OSError:
|
|
514
|
+
return False
|
|
515
|
+
|
|
516
|
+
def _try_daemon_stop(self, port: int) -> bool:
|
|
517
|
+
"""Try to stop daemon-style server."""
|
|
518
|
+
if port != self.base_port:
|
|
519
|
+
print(f"⚠️ Daemon management only supports default port {self.base_port}, not {port}")
|
|
520
|
+
return False
|
|
521
|
+
|
|
522
|
+
daemon_pid = self._get_daemon_pid()
|
|
523
|
+
if not daemon_pid:
|
|
524
|
+
print(f"❌ No daemon server found (no PID file at {self.daemon_pidfile_path})")
|
|
525
|
+
return False
|
|
526
|
+
|
|
527
|
+
try:
|
|
528
|
+
print(f"🔄 Stopping daemon server (PID: {daemon_pid})...")
|
|
529
|
+
os.kill(daemon_pid, signal.SIGTERM)
|
|
530
|
+
|
|
531
|
+
# Wait for daemon to stop
|
|
532
|
+
for i in range(10):
|
|
533
|
+
time.sleep(1)
|
|
534
|
+
if not self._validate_pid(daemon_pid):
|
|
535
|
+
print(f"✅ Daemon server stopped successfully")
|
|
536
|
+
# Clean up PID file
|
|
537
|
+
try:
|
|
538
|
+
self.daemon_pidfile_path.unlink(missing_ok=True)
|
|
539
|
+
except:
|
|
540
|
+
pass
|
|
541
|
+
return True
|
|
542
|
+
if i == 5:
|
|
543
|
+
print(f"⏳ Waiting for daemon to stop...")
|
|
544
|
+
|
|
545
|
+
# Force kill if still running
|
|
546
|
+
if self._validate_pid(daemon_pid):
|
|
547
|
+
print(f"⚠️ Force killing daemon server...")
|
|
548
|
+
os.kill(daemon_pid, signal.SIGKILL)
|
|
549
|
+
time.sleep(1)
|
|
550
|
+
if not self._validate_pid(daemon_pid):
|
|
551
|
+
print(f"✅ Daemon server force stopped")
|
|
552
|
+
try:
|
|
553
|
+
self.daemon_pidfile_path.unlink(missing_ok=True)
|
|
554
|
+
except:
|
|
555
|
+
pass
|
|
556
|
+
return True
|
|
557
|
+
|
|
558
|
+
except OSError as e:
|
|
559
|
+
print(f"❌ Error stopping daemon server: {e}")
|
|
560
|
+
return False
|
|
561
|
+
|
|
562
|
+
print(f"❌ Failed to stop daemon server")
|
|
563
|
+
return False
|
|
564
|
+
|
|
565
|
+
def diagnose_conflicts(self, port: int = None) -> None:
|
|
566
|
+
"""Diagnose server management conflicts and suggest resolutions."""
|
|
567
|
+
if port is None:
|
|
568
|
+
port = self.base_port
|
|
569
|
+
|
|
570
|
+
print(f"🔍 Diagnosing Socket.IO server management on port {port}")
|
|
571
|
+
print("=" * 60)
|
|
572
|
+
|
|
573
|
+
# Check HTTP-managed server
|
|
574
|
+
http_server = None
|
|
575
|
+
daemon_server = None
|
|
576
|
+
|
|
577
|
+
try:
|
|
578
|
+
if REQUESTS_AVAILABLE:
|
|
579
|
+
response = requests.get(f"http://localhost:{port}/health", timeout=2.0)
|
|
580
|
+
if response.status_code == 200:
|
|
581
|
+
data = response.json()
|
|
582
|
+
if 'pid' in data:
|
|
583
|
+
http_server = data
|
|
584
|
+
except:
|
|
585
|
+
pass
|
|
586
|
+
|
|
587
|
+
# Check daemon-managed server
|
|
588
|
+
daemon_pid = self._get_daemon_pid()
|
|
589
|
+
if daemon_pid and port == self.base_port:
|
|
590
|
+
daemon_server = self._get_daemon_server_info()
|
|
591
|
+
|
|
592
|
+
# Analysis
|
|
593
|
+
print("📊 Server Analysis:")
|
|
594
|
+
|
|
595
|
+
if http_server and daemon_server:
|
|
596
|
+
print("⚠️ CONFLICT DETECTED: Both HTTP and daemon servers found!")
|
|
597
|
+
print(f" HTTP server: PID {http_server.get('pid')}, ID {http_server.get('server_id')}")
|
|
598
|
+
print(f" Daemon server: PID {daemon_server.get('pid')}, ID {daemon_server.get('server_id')}")
|
|
599
|
+
print()
|
|
600
|
+
print("🔧 Resolution Steps:")
|
|
601
|
+
print(" 1. Choose one management approach:")
|
|
602
|
+
print(f" • Keep HTTP: {sys.executable} {__file__} stop --port {port} (stops daemon)")
|
|
603
|
+
print(f" • Keep daemon: Stop HTTP server first, then use daemon commands")
|
|
604
|
+
print()
|
|
605
|
+
|
|
606
|
+
elif http_server:
|
|
607
|
+
print(f"✅ HTTP-managed server found")
|
|
608
|
+
print(f" Server ID: {http_server.get('server_id')}")
|
|
609
|
+
print(f" PID: {http_server.get('pid')}")
|
|
610
|
+
print(f" Status: {http_server.get('status')}")
|
|
611
|
+
print()
|
|
612
|
+
print("🔧 Management Commands:")
|
|
613
|
+
print(f" • Stop: {sys.executable} {__file__} stop --port {port}")
|
|
614
|
+
print(f" • Status: {sys.executable} {__file__} status")
|
|
615
|
+
print()
|
|
616
|
+
|
|
617
|
+
elif daemon_server:
|
|
618
|
+
print(f"✅ Daemon-managed server found")
|
|
619
|
+
print(f" PID: {daemon_server.get('pid')}")
|
|
620
|
+
print(f" PID file: {self.daemon_pidfile_path}")
|
|
621
|
+
print()
|
|
622
|
+
print("🔧 Management Commands:")
|
|
623
|
+
daemon_script = self.project_root / "src" / "claude_mpm" / "scripts" / "socketio_daemon.py"
|
|
624
|
+
print(f" • Stop: {daemon_script} stop")
|
|
625
|
+
print(f" • Status: {daemon_script} status")
|
|
626
|
+
print(f" • Restart: {daemon_script} restart")
|
|
627
|
+
print()
|
|
628
|
+
|
|
629
|
+
else:
|
|
630
|
+
print("❌ No servers found on specified port")
|
|
631
|
+
print()
|
|
632
|
+
print("🔧 Start a server:")
|
|
633
|
+
print(f" • HTTP-managed: {sys.executable} {__file__} start --port {port}")
|
|
634
|
+
daemon_script = self.project_root / "src" / "claude_mpm" / "scripts" / "socketio_daemon.py"
|
|
635
|
+
if port == self.base_port:
|
|
636
|
+
print(f" • Daemon-managed: {daemon_script} start")
|
|
637
|
+
print()
|
|
638
|
+
|
|
639
|
+
# Port conflict check
|
|
640
|
+
try:
|
|
641
|
+
import socket
|
|
642
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
643
|
+
result = s.connect_ex(('localhost', port))
|
|
644
|
+
if result == 0:
|
|
645
|
+
print("🌐 Port Status: IN USE")
|
|
646
|
+
if not http_server and not daemon_server:
|
|
647
|
+
print(f" ⚠️ Port {port} is occupied by unknown process")
|
|
648
|
+
if PSUTIL_AVAILABLE:
|
|
649
|
+
print(" 🔍 Use 'lsof -i :{port}' or 'netstat -tulpn | grep {port}' to identify")
|
|
650
|
+
else:
|
|
651
|
+
print("🌐 Port Status: AVAILABLE")
|
|
652
|
+
except:
|
|
653
|
+
print("🌐 Port Status: UNKNOWN")
|
|
654
|
+
|
|
655
|
+
print()
|
|
656
|
+
print("📚 Management Style Comparison:")
|
|
657
|
+
print(" HTTP-managed:")
|
|
658
|
+
print(" • Pros: Full API, stats, multi-instance support")
|
|
659
|
+
print(" • Cons: More complex, requires HTTP client")
|
|
660
|
+
print(" Daemon-managed:")
|
|
661
|
+
print(" • Pros: Simple, lightweight, traditional daemon")
|
|
662
|
+
print(" • Cons: Single instance, basic management")
|
|
345
663
|
|
|
346
664
|
|
|
347
665
|
def main():
|
|
@@ -379,6 +697,10 @@ def main():
|
|
|
379
697
|
# List command
|
|
380
698
|
subparsers.add_parser('list', help='List running servers')
|
|
381
699
|
|
|
700
|
+
# Diagnose command
|
|
701
|
+
diagnose_parser = subparsers.add_parser('diagnose', help='Diagnose server management conflicts')
|
|
702
|
+
diagnose_parser.add_argument('--port', type=int, default=8765, help='Port to diagnose')
|
|
703
|
+
|
|
382
704
|
args = parser.parse_args()
|
|
383
705
|
|
|
384
706
|
if not args.command:
|
|
@@ -422,6 +744,9 @@ def main():
|
|
|
422
744
|
|
|
423
745
|
elif args.command == 'list':
|
|
424
746
|
manager.status(verbose=False)
|
|
747
|
+
|
|
748
|
+
elif args.command == 'diagnose':
|
|
749
|
+
manager.diagnose_conflicts(port=args.port)
|
|
425
750
|
|
|
426
751
|
|
|
427
752
|
if __name__ == "__main__":
|
claude_mpm/services/__init__.py
CHANGED
|
@@ -21,6 +21,21 @@ def __getattr__(name):
|
|
|
21
21
|
elif name == "ProjectAnalyzer":
|
|
22
22
|
from .project_analyzer import ProjectAnalyzer
|
|
23
23
|
return ProjectAnalyzer
|
|
24
|
+
elif name == "AdvancedHealthMonitor":
|
|
25
|
+
try:
|
|
26
|
+
from .health_monitor import AdvancedHealthMonitor
|
|
27
|
+
return AdvancedHealthMonitor
|
|
28
|
+
except ImportError:
|
|
29
|
+
raise AttributeError(f"Health monitoring not available: {name}")
|
|
30
|
+
elif name == "RecoveryManager":
|
|
31
|
+
try:
|
|
32
|
+
from .recovery_manager import RecoveryManager
|
|
33
|
+
return RecoveryManager
|
|
34
|
+
except ImportError:
|
|
35
|
+
raise AttributeError(f"Recovery management not available: {name}")
|
|
36
|
+
elif name == "StandaloneSocketIOServer":
|
|
37
|
+
from .standalone_socketio_server import StandaloneSocketIOServer
|
|
38
|
+
return StandaloneSocketIOServer
|
|
24
39
|
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
|
|
25
40
|
|
|
26
41
|
__all__ = [
|
|
@@ -30,4 +45,7 @@ __all__ = [
|
|
|
30
45
|
"get_memory_manager",
|
|
31
46
|
"HookService",
|
|
32
47
|
"ProjectAnalyzer",
|
|
48
|
+
"AdvancedHealthMonitor",
|
|
49
|
+
"RecoveryManager",
|
|
50
|
+
"StandaloneSocketIOServer",
|
|
33
51
|
]
|
|
@@ -23,6 +23,7 @@ from typing import Dict, List, Optional, Any
|
|
|
23
23
|
from datetime import datetime
|
|
24
24
|
import re
|
|
25
25
|
import logging
|
|
26
|
+
import os
|
|
26
27
|
|
|
27
28
|
from claude_mpm.core.config import Config
|
|
28
29
|
from claude_mpm.core.mixins import LoggerMixin
|
|
@@ -68,7 +69,7 @@ class AgentMemoryManager:
|
|
|
68
69
|
|
|
69
70
|
Args:
|
|
70
71
|
config: Optional Config object. If not provided, will create default Config.
|
|
71
|
-
working_directory: Optional working directory. If not provided, uses
|
|
72
|
+
working_directory: Optional working directory. If not provided, uses current working directory.
|
|
72
73
|
"""
|
|
73
74
|
# Initialize logger using the same pattern as LoggerMixin
|
|
74
75
|
self._logger_instance = None
|
|
@@ -76,8 +77,9 @@ class AgentMemoryManager:
|
|
|
76
77
|
|
|
77
78
|
self.config = config or Config()
|
|
78
79
|
self.project_root = PathResolver.get_project_root()
|
|
79
|
-
|
|
80
|
-
self.
|
|
80
|
+
# Use current working directory by default, not project root
|
|
81
|
+
self.working_directory = working_directory or Path(os.getcwd())
|
|
82
|
+
self.memories_dir = self.working_directory / ".claude-mpm" / "memories"
|
|
81
83
|
self._ensure_memories_directory()
|
|
82
84
|
|
|
83
85
|
# Initialize memory limits from configuration
|
|
@@ -945,7 +947,7 @@ class AgentMemoryManager:
|
|
|
945
947
|
"""
|
|
946
948
|
try:
|
|
947
949
|
from claude_mpm.services.memory_optimizer import MemoryOptimizer
|
|
948
|
-
optimizer = MemoryOptimizer(self.config)
|
|
950
|
+
optimizer = MemoryOptimizer(self.config, self.working_directory)
|
|
949
951
|
|
|
950
952
|
if agent_id:
|
|
951
953
|
result = optimizer.optimize_agent_memory(agent_id)
|
|
@@ -973,7 +975,7 @@ class AgentMemoryManager:
|
|
|
973
975
|
"""
|
|
974
976
|
try:
|
|
975
977
|
from claude_mpm.services.memory_builder import MemoryBuilder
|
|
976
|
-
builder = MemoryBuilder(self.config)
|
|
978
|
+
builder = MemoryBuilder(self.config, self.working_directory)
|
|
977
979
|
|
|
978
980
|
result = builder.build_from_documentation(force_rebuild)
|
|
979
981
|
self.logger.info("Built memories from documentation")
|