claude-mpm 4.5.6__py3-none-any.whl → 4.5.11__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 (62) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/__init__.py +20 -5
  3. claude_mpm/agents/BASE_OPS.md +10 -0
  4. claude_mpm/agents/PM_INSTRUCTIONS.md +28 -4
  5. claude_mpm/agents/agent_loader.py +19 -2
  6. claude_mpm/agents/base_agent_loader.py +5 -5
  7. claude_mpm/agents/templates/agent-manager.json +3 -3
  8. claude_mpm/agents/templates/agentic-coder-optimizer.json +3 -3
  9. claude_mpm/agents/templates/api_qa.json +1 -1
  10. claude_mpm/agents/templates/clerk-ops.json +3 -3
  11. claude_mpm/agents/templates/code_analyzer.json +3 -3
  12. claude_mpm/agents/templates/dart_engineer.json +294 -0
  13. claude_mpm/agents/templates/data_engineer.json +3 -3
  14. claude_mpm/agents/templates/documentation.json +2 -2
  15. claude_mpm/agents/templates/engineer.json +2 -2
  16. claude_mpm/agents/templates/gcp_ops_agent.json +2 -2
  17. claude_mpm/agents/templates/imagemagick.json +1 -1
  18. claude_mpm/agents/templates/local_ops_agent.json +363 -49
  19. claude_mpm/agents/templates/memory_manager.json +2 -2
  20. claude_mpm/agents/templates/nextjs_engineer.json +2 -2
  21. claude_mpm/agents/templates/ops.json +2 -2
  22. claude_mpm/agents/templates/php-engineer.json +1 -1
  23. claude_mpm/agents/templates/project_organizer.json +1 -1
  24. claude_mpm/agents/templates/prompt-engineer.json +6 -4
  25. claude_mpm/agents/templates/python_engineer.json +2 -2
  26. claude_mpm/agents/templates/qa.json +1 -1
  27. claude_mpm/agents/templates/react_engineer.json +3 -3
  28. claude_mpm/agents/templates/refactoring_engineer.json +3 -3
  29. claude_mpm/agents/templates/research.json +2 -2
  30. claude_mpm/agents/templates/security.json +2 -2
  31. claude_mpm/agents/templates/ticketing.json +2 -2
  32. claude_mpm/agents/templates/typescript_engineer.json +2 -2
  33. claude_mpm/agents/templates/vercel_ops_agent.json +2 -2
  34. claude_mpm/agents/templates/version_control.json +2 -2
  35. claude_mpm/agents/templates/web_qa.json +6 -6
  36. claude_mpm/agents/templates/web_ui.json +3 -3
  37. claude_mpm/cli/__init__.py +49 -19
  38. claude_mpm/cli/commands/configure.py +591 -7
  39. claude_mpm/cli/parsers/configure_parser.py +5 -0
  40. claude_mpm/core/__init__.py +53 -17
  41. claude_mpm/core/config.py +1 -1
  42. claude_mpm/core/log_manager.py +7 -0
  43. claude_mpm/hooks/claude_hooks/response_tracking.py +16 -11
  44. claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +9 -11
  45. claude_mpm/services/__init__.py +140 -156
  46. claude_mpm/services/agents/deployment/deployment_config_loader.py +21 -0
  47. claude_mpm/services/agents/loading/base_agent_manager.py +12 -2
  48. claude_mpm/services/async_session_logger.py +112 -96
  49. claude_mpm/services/claude_session_logger.py +63 -61
  50. claude_mpm/services/mcp_config_manager.py +328 -38
  51. claude_mpm/services/mcp_gateway/__init__.py +98 -94
  52. claude_mpm/services/monitor/event_emitter.py +1 -1
  53. claude_mpm/services/orphan_detection.py +791 -0
  54. claude_mpm/services/project_port_allocator.py +601 -0
  55. claude_mpm/services/response_tracker.py +17 -6
  56. claude_mpm/services/session_manager.py +176 -0
  57. {claude_mpm-4.5.6.dist-info → claude_mpm-4.5.11.dist-info}/METADATA +1 -1
  58. {claude_mpm-4.5.6.dist-info → claude_mpm-4.5.11.dist-info}/RECORD +62 -58
  59. {claude_mpm-4.5.6.dist-info → claude_mpm-4.5.11.dist-info}/WHEEL +0 -0
  60. {claude_mpm-4.5.6.dist-info → claude_mpm-4.5.11.dist-info}/entry_points.txt +0 -0
  61. {claude_mpm-4.5.6.dist-info → claude_mpm-4.5.11.dist-info}/licenses/LICENSE +0 -0
  62. {claude_mpm-4.5.6.dist-info → claude_mpm-4.5.11.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,601 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Project Port Allocator Service
4
+ ==============================
5
+
6
+ Provides deterministic, hash-based port allocation for local development projects.
7
+ Ensures each project gets a consistent port across sessions while avoiding conflicts.
8
+
9
+ Part of local-ops agent improvements for single port per project allocation.
10
+
11
+ WHY: Manual port assignment is error-prone and leads to conflicts. Hash-based
12
+ allocation provides predictable, consistent port assignments while avoiding
13
+ collisions through linear probing.
14
+
15
+ DESIGN DECISIONS:
16
+ - Hash-based allocation: Projects get same port consistently (SHA-256 of path)
17
+ - Port range: 3000-3999 (1000 ports for user projects)
18
+ - Linear probing: Handles hash collisions gracefully
19
+ - Global registry: Prevents conflicts across multiple projects
20
+ - Persistent state: Survives restarts and maintains history
21
+ - Atomic operations: Prevents race conditions in port allocation
22
+ """
23
+
24
+ import hashlib
25
+ import json
26
+ import os
27
+ from datetime import datetime, timezone
28
+ from pathlib import Path
29
+ from typing import Any, Dict, Optional
30
+
31
+ import psutil
32
+
33
+ from .core.base import SyncBaseService
34
+
35
+
36
+ class ProjectPortAllocator(SyncBaseService):
37
+ """
38
+ Manages port allocation for local development projects.
39
+
40
+ Features:
41
+ - Deterministic port allocation based on project path hash
42
+ - Persistent state tracking across sessions
43
+ - Global registry to prevent cross-project conflicts
44
+ - Orphan detection and cleanup
45
+ - Linear probing for conflict resolution
46
+ """
47
+
48
+ # Port range for user projects (avoiding system ports and Claude MPM services)
49
+ DEFAULT_PORT_RANGE_START = 3000
50
+ DEFAULT_PORT_RANGE_END = 3999
51
+
52
+ # Claude MPM services use 8765-8785, keep these protected
53
+ PROTECTED_PORT_RANGES = [(8765, 8785)]
54
+
55
+ # State file names
56
+ STATE_FILE_NAME = "deployment-state.json"
57
+ GLOBAL_REGISTRY_FILE = "global-port-registry.json"
58
+
59
+ def __init__(
60
+ self,
61
+ project_root: Optional[Path] = None,
62
+ port_range_start: Optional[int] = None,
63
+ port_range_end: Optional[int] = None,
64
+ ):
65
+ """
66
+ Initialize the port allocator.
67
+
68
+ Args:
69
+ project_root: Project directory (default: current working directory)
70
+ port_range_start: Start of port range (default: 3000)
71
+ port_range_end: End of port range (default: 3999)
72
+ """
73
+ super().__init__(service_name="ProjectPortAllocator")
74
+
75
+ self.project_root = (project_root or Path.cwd()).resolve()
76
+ self.port_range_start = port_range_start or self.DEFAULT_PORT_RANGE_START
77
+ self.port_range_end = port_range_end or self.DEFAULT_PORT_RANGE_END
78
+
79
+ # Project-local state directory
80
+ self.state_dir = self.project_root / ".claude-mpm"
81
+ self.state_file = self.state_dir / self.STATE_FILE_NAME
82
+
83
+ # Global registry in user home directory
84
+ self.global_registry_dir = Path.home() / ".claude-mpm"
85
+ self.global_registry_file = self.global_registry_dir / self.GLOBAL_REGISTRY_FILE
86
+
87
+ # Ensure directories exist
88
+ self.state_dir.mkdir(parents=True, exist_ok=True)
89
+ self.global_registry_dir.mkdir(parents=True, exist_ok=True)
90
+
91
+ def initialize(self) -> bool:
92
+ """
93
+ Initialize the service.
94
+
95
+ Returns:
96
+ True if initialization successful
97
+ """
98
+ try:
99
+ # Cleanup any dead registrations on startup
100
+ self.cleanup_dead_registrations()
101
+ self._initialized = True
102
+ self.log_info("ProjectPortAllocator initialized successfully")
103
+ return True
104
+ except Exception as e:
105
+ self.log_error(f"Failed to initialize: {e}")
106
+ return False
107
+
108
+ def shutdown(self) -> None:
109
+ """Shutdown the service gracefully."""
110
+ self._shutdown = True
111
+ self.log_info("ProjectPortAllocator shutdown")
112
+
113
+ def _compute_project_hash(self, project_path: Path) -> str:
114
+ """
115
+ Compute deterministic hash for a project path.
116
+
117
+ Args:
118
+ project_path: Absolute path to project
119
+
120
+ Returns:
121
+ SHA-256 hash of the project path
122
+ """
123
+ # Use absolute path for consistency
124
+ absolute_path = project_path.resolve()
125
+ path_str = str(absolute_path)
126
+
127
+ # Compute SHA-256 hash
128
+ hash_obj = hashlib.sha256(path_str.encode("utf-8"))
129
+ return hash_obj.hexdigest()
130
+
131
+ def _hash_to_port(self, project_hash: str) -> int:
132
+ """
133
+ Convert project hash to a port number in the allowed range.
134
+
135
+ Args:
136
+ project_hash: SHA-256 hash of project path
137
+
138
+ Returns:
139
+ Port number in the configured range
140
+ """
141
+ # Use first 8 hex chars as integer
142
+ hash_int = int(project_hash[:8], 16)
143
+
144
+ # Map to port range
145
+ port_range = self.port_range_end - self.port_range_start + 1
146
+ port = self.port_range_start + (hash_int % port_range)
147
+
148
+ return port
149
+
150
+ def _is_port_available(self, port: int) -> bool:
151
+ """
152
+ Check if a port is available for binding.
153
+
154
+ Args:
155
+ port: Port number to check
156
+
157
+ Returns:
158
+ True if port is available
159
+ """
160
+ try:
161
+ import socket
162
+
163
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
164
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
165
+ sock.bind(("localhost", port))
166
+ return True
167
+ except OSError:
168
+ return False
169
+
170
+ def _is_protected_port(self, port: int) -> bool:
171
+ """
172
+ Check if port is in a protected range.
173
+
174
+ Args:
175
+ port: Port number to check
176
+
177
+ Returns:
178
+ True if port is protected
179
+ """
180
+ for start, end in self.PROTECTED_PORT_RANGES:
181
+ if start <= port <= end:
182
+ return True
183
+ return False
184
+
185
+ def _load_project_state(self) -> Dict[str, Any]:
186
+ """
187
+ Load project deployment state.
188
+
189
+ Returns:
190
+ State dictionary or empty dict if not found
191
+ """
192
+ try:
193
+ if self.state_file.exists():
194
+ with open(self.state_file) as f:
195
+ return json.load(f)
196
+ except Exception as e:
197
+ self.log_warning(f"Failed to load project state: {e}")
198
+
199
+ return {}
200
+
201
+ def _save_project_state(self, state: Dict[str, Any]) -> None:
202
+ """
203
+ Save project deployment state atomically.
204
+
205
+ Args:
206
+ state: State dictionary to save
207
+ """
208
+ try:
209
+ # Write to temporary file first
210
+ temp_file = self.state_file.with_suffix(".tmp")
211
+ with open(temp_file, "w") as f:
212
+ json.dump(state, f, indent=2)
213
+
214
+ # Atomic rename
215
+ temp_file.replace(self.state_file)
216
+
217
+ except Exception as e:
218
+ self.log_error(f"Failed to save project state: {e}")
219
+ raise
220
+
221
+ def _load_global_registry(self) -> Dict[str, Any]:
222
+ """
223
+ Load global port registry.
224
+
225
+ Returns:
226
+ Registry dictionary or empty dict if not found
227
+ """
228
+ try:
229
+ if self.global_registry_file.exists():
230
+ with open(self.global_registry_file) as f:
231
+ return json.load(f)
232
+ except Exception as e:
233
+ self.log_warning(f"Failed to load global registry: {e}")
234
+
235
+ return {"allocations": {}, "last_updated": None}
236
+
237
+ def _save_global_registry(self, registry: Dict[str, Any]) -> None:
238
+ """
239
+ Save global port registry atomically.
240
+
241
+ Args:
242
+ registry: Registry dictionary to save
243
+ """
244
+ try:
245
+ # Update timestamp
246
+ registry["last_updated"] = datetime.now(timezone.utc).isoformat()
247
+
248
+ # Write to temporary file first
249
+ temp_file = self.global_registry_file.with_suffix(".tmp")
250
+ with open(temp_file, "w") as f:
251
+ json.dump(registry, f, indent=2)
252
+
253
+ # Atomic rename
254
+ temp_file.replace(self.global_registry_file)
255
+
256
+ except Exception as e:
257
+ self.log_error(f"Failed to save global registry: {e}")
258
+ raise
259
+
260
+ def get_project_port(
261
+ self,
262
+ project_path: Optional[Path] = None,
263
+ service_name: str = "main",
264
+ respect_env_override: bool = True,
265
+ ) -> int:
266
+ """
267
+ Get the allocated port for a project service.
268
+
269
+ This is the main entry point for port allocation. It:
270
+ 1. Checks for environment variable override (PROJECT_PORT)
271
+ 2. Checks existing allocation in state files
272
+ 3. Computes hash-based port with linear probing for conflicts
273
+
274
+ Args:
275
+ project_path: Path to project (default: self.project_root)
276
+ service_name: Name of the service (default: "main")
277
+ respect_env_override: Whether to respect PROJECT_PORT env var
278
+
279
+ Returns:
280
+ Allocated port number
281
+
282
+ Raises:
283
+ RuntimeError: If no available port found
284
+ """
285
+ project_path = (project_path or self.project_root).resolve()
286
+
287
+ # Check environment variable override
288
+ if respect_env_override:
289
+ env_port = os.environ.get("PROJECT_PORT")
290
+ if env_port:
291
+ try:
292
+ port = int(env_port)
293
+ self.log_info(
294
+ f"Using port {port} from PROJECT_PORT environment variable"
295
+ )
296
+ return port
297
+ except ValueError:
298
+ self.log_warning(f"Invalid PROJECT_PORT value: {env_port}")
299
+
300
+ # Check existing allocation
301
+ state = self._load_project_state()
302
+ deployments = state.get("deployments", {})
303
+
304
+ if service_name in deployments:
305
+ existing_port = deployments[service_name].get("port")
306
+ if existing_port and self._is_port_available(existing_port):
307
+ self.log_info(
308
+ f"Reusing existing port {existing_port} for {service_name}"
309
+ )
310
+ return existing_port
311
+
312
+ # Compute hash-based port with linear probing
313
+ project_hash = self._compute_project_hash(project_path)
314
+ base_port = self._hash_to_port(project_hash)
315
+
316
+ # Try base port first
317
+ port = self._find_available_port(base_port, project_path, service_name)
318
+
319
+ self.log_info(
320
+ f"Allocated port {port} for {service_name} "
321
+ f"(hash: {project_hash[:8]}, base: {base_port})"
322
+ )
323
+
324
+ return port
325
+
326
+ def _find_available_port(
327
+ self,
328
+ start_port: int,
329
+ project_path: Path,
330
+ service_name: str,
331
+ ) -> int:
332
+ """
333
+ Find available port using linear probing.
334
+
335
+ Args:
336
+ start_port: Starting port from hash
337
+ project_path: Project path
338
+ service_name: Service name
339
+
340
+ Returns:
341
+ Available port number
342
+
343
+ Raises:
344
+ RuntimeError: If no available port found
345
+ """
346
+ max_probes = self.port_range_end - self.port_range_start + 1
347
+
348
+ for offset in range(max_probes):
349
+ port = start_port + offset
350
+
351
+ # Wrap around if we exceed range
352
+ if port > self.port_range_end:
353
+ port = self.port_range_start + (port - self.port_range_end - 1)
354
+
355
+ # Skip protected ports
356
+ if self._is_protected_port(port):
357
+ continue
358
+
359
+ # Check if port is available
360
+ if self._is_port_available(port):
361
+ return port
362
+
363
+ raise RuntimeError(
364
+ f"No available ports in range {self.port_range_start}-{self.port_range_end}"
365
+ )
366
+
367
+ def register_port(
368
+ self,
369
+ port: int,
370
+ service_name: str = "main",
371
+ deployment_info: Optional[Dict[str, Any]] = None,
372
+ project_path: Optional[Path] = None,
373
+ ) -> None:
374
+ """
375
+ Register a port allocation for a project service.
376
+
377
+ Args:
378
+ port: Port number
379
+ service_name: Service name
380
+ deployment_info: Additional deployment information
381
+ project_path: Project path (default: self.project_root)
382
+ """
383
+ project_path = (project_path or self.project_root).resolve()
384
+ project_hash = self._compute_project_hash(project_path)
385
+
386
+ # Update project state
387
+ state = self._load_project_state()
388
+
389
+ if "project_path" not in state:
390
+ state["project_path"] = str(project_path)
391
+ state["project_hash"] = project_hash
392
+ state["deployments"] = {}
393
+ state["port_history"] = []
394
+
395
+ # Merge deployment info
396
+ deployment_data = deployment_info or {}
397
+ deployment_data.update(
398
+ {
399
+ "port": port,
400
+ "service_name": service_name,
401
+ "registered_at": datetime.now(timezone.utc).isoformat(),
402
+ }
403
+ )
404
+
405
+ state["deployments"][service_name] = deployment_data
406
+
407
+ # Track port history
408
+ if port not in state.get("port_history", []):
409
+ state.setdefault("port_history", []).append(port)
410
+
411
+ state["last_updated"] = datetime.now(timezone.utc).isoformat()
412
+
413
+ self._save_project_state(state)
414
+
415
+ # Update global registry
416
+ registry = self._load_global_registry()
417
+
418
+ registry.setdefault("allocations", {})[str(port)] = {
419
+ "project_path": str(project_path),
420
+ "project_hash": project_hash,
421
+ "service_name": service_name,
422
+ "registered_at": datetime.now(timezone.utc).isoformat(),
423
+ }
424
+
425
+ self._save_global_registry(registry)
426
+
427
+ self.log_info(f"Registered port {port} for {service_name}")
428
+
429
+ def release_port(
430
+ self,
431
+ port: int,
432
+ service_name: str = "main",
433
+ project_path: Optional[Path] = None,
434
+ ) -> None:
435
+ """
436
+ Release a port allocation.
437
+
438
+ Args:
439
+ port: Port number to release
440
+ service_name: Service name
441
+ project_path: Project path (default: self.project_root)
442
+ """
443
+ project_path = (project_path or self.project_root).resolve()
444
+
445
+ # Update project state
446
+ state = self._load_project_state()
447
+ deployments = state.get("deployments", {})
448
+
449
+ if service_name in deployments:
450
+ del deployments[service_name]
451
+ state["last_updated"] = datetime.now(timezone.utc).isoformat()
452
+ self._save_project_state(state)
453
+
454
+ # Update global registry
455
+ registry = self._load_global_registry()
456
+ allocations = registry.get("allocations", {})
457
+
458
+ if str(port) in allocations:
459
+ del allocations[str(port)]
460
+ self._save_global_registry(registry)
461
+
462
+ self.log_info(f"Released port {port} for {service_name}")
463
+
464
+ def cleanup_dead_registrations(self) -> int:
465
+ """
466
+ Clean up registrations for dead processes.
467
+
468
+ Returns:
469
+ Number of registrations cleaned up
470
+ """
471
+ cleaned = 0
472
+
473
+ # Clean project state
474
+ state = self._load_project_state()
475
+ deployments = state.get("deployments", {})
476
+ dead_services = []
477
+
478
+ for service_name, deployment in deployments.items():
479
+ pid = deployment.get("pid")
480
+ if pid and not self._is_process_alive(pid):
481
+ dead_services.append(service_name)
482
+ cleaned += 1
483
+
484
+ for service_name in dead_services:
485
+ self.log_info(f"Cleaning up dead deployment: {service_name}")
486
+ del deployments[service_name]
487
+
488
+ if dead_services:
489
+ state["last_updated"] = datetime.now(timezone.utc).isoformat()
490
+ self._save_project_state(state)
491
+
492
+ # Clean global registry
493
+ registry = self._load_global_registry()
494
+ allocations = registry.get("allocations", {})
495
+ dead_ports = []
496
+
497
+ for port_str, allocation in allocations.items():
498
+ project_path = Path(allocation.get("project_path", ""))
499
+
500
+ # Check if project still exists
501
+ if not project_path.exists():
502
+ dead_ports.append(port_str)
503
+ cleaned += 1
504
+ continue
505
+
506
+ # Check project state file
507
+ state_file = project_path / ".claude-mpm" / self.STATE_FILE_NAME
508
+ if state_file.exists():
509
+ try:
510
+ with open(state_file) as f:
511
+ project_state = json.load(f)
512
+
513
+ # Check if service still registered
514
+ service_name = allocation.get("service_name")
515
+ if service_name not in project_state.get("deployments", {}):
516
+ dead_ports.append(port_str)
517
+ cleaned += 1
518
+
519
+ except Exception as e:
520
+ self.log_warning(f"Error checking state for port {port_str}: {e}")
521
+
522
+ for port_str in dead_ports:
523
+ self.log_info(f"Cleaning up dead global allocation: port {port_str}")
524
+ del allocations[port_str]
525
+
526
+ if dead_ports:
527
+ self._save_global_registry(registry)
528
+
529
+ if cleaned > 0:
530
+ self.log_info(f"Cleaned up {cleaned} dead registrations")
531
+
532
+ return cleaned
533
+
534
+ def _is_process_alive(self, pid: int) -> bool:
535
+ """
536
+ Check if a process is alive.
537
+
538
+ Args:
539
+ pid: Process ID
540
+
541
+ Returns:
542
+ True if process is alive
543
+ """
544
+ try:
545
+ return psutil.pid_exists(pid)
546
+ except Exception:
547
+ return False
548
+
549
+ def get_allocation_info(
550
+ self,
551
+ service_name: str = "main",
552
+ project_path: Optional[Path] = None,
553
+ ) -> Optional[Dict[str, Any]]:
554
+ """
555
+ Get allocation information for a service.
556
+
557
+ Args:
558
+ service_name: Service name
559
+ project_path: Project path (default: self.project_root)
560
+
561
+ Returns:
562
+ Allocation info dict or None if not found
563
+ """
564
+ project_path = (project_path or self.project_root).resolve()
565
+ state = self._load_project_state()
566
+ deployments = state.get("deployments", {})
567
+
568
+ return deployments.get(service_name)
569
+
570
+ def list_project_allocations(
571
+ self,
572
+ project_path: Optional[Path] = None,
573
+ ) -> Dict[str, Any]:
574
+ """
575
+ List all port allocations for a project.
576
+
577
+ Args:
578
+ project_path: Project path (default: self.project_root)
579
+
580
+ Returns:
581
+ Dictionary of service allocations
582
+ """
583
+ project_path = (project_path or self.project_root).resolve()
584
+ state = self._load_project_state()
585
+
586
+ return {
587
+ "project_path": str(project_path),
588
+ "project_hash": state.get("project_hash"),
589
+ "deployments": state.get("deployments", {}),
590
+ "port_history": state.get("port_history", []),
591
+ "last_updated": state.get("last_updated"),
592
+ }
593
+
594
+ def list_global_allocations(self) -> Dict[str, Any]:
595
+ """
596
+ List all global port allocations.
597
+
598
+ Returns:
599
+ Global registry data
600
+ """
601
+ return self._load_global_registry()
@@ -19,6 +19,7 @@ DESIGN DECISIONS:
19
19
  """
20
20
 
21
21
  from datetime import datetime, timezone
22
+ from threading import Lock
22
23
  from typing import Any, Dict, Optional
23
24
 
24
25
  from claude_mpm.core.config import Config
@@ -79,7 +80,7 @@ class ResponseTracker:
79
80
  from claude_mpm.services.claude_session_logger import get_session_logger
80
81
 
81
82
  self.session_logger = get_session_logger(config)
82
- logger.info(
83
+ logger.debug(
83
84
  f"Response tracker initialized with base directory: {base_dir}"
84
85
  )
85
86
  except Exception as e:
@@ -214,12 +215,15 @@ class ResponseTracker:
214
215
  logger.info(f"Response tracker session ID set to: {session_id}")
215
216
 
216
217
 
217
- # Singleton instance for consistency
218
+ # Singleton instance with thread-safe initialization
218
219
  _tracker_instance = None
220
+ _tracker_lock = Lock()
219
221
 
220
222
 
221
223
  def get_response_tracker(config: Optional[Config] = None) -> ResponseTracker:
222
- """Get the singleton response tracker instance.
224
+ """Get the singleton response tracker instance with thread-safe initialization.
225
+
226
+ Uses double-checked locking pattern to ensure thread safety.
223
227
 
224
228
  Args:
225
229
  config: Optional configuration instance
@@ -228,9 +232,16 @@ def get_response_tracker(config: Optional[Config] = None) -> ResponseTracker:
228
232
  The shared ResponseTracker instance
229
233
  """
230
234
  global _tracker_instance
231
- if _tracker_instance is None:
232
- _tracker_instance = ResponseTracker(config=config)
233
- return _tracker_instance
235
+
236
+ # Fast path - check without lock
237
+ if _tracker_instance is not None:
238
+ return _tracker_instance
239
+
240
+ # Slow path - acquire lock and double-check
241
+ with _tracker_lock:
242
+ if _tracker_instance is None:
243
+ _tracker_instance = ResponseTracker(config=config)
244
+ return _tracker_instance
234
245
 
235
246
 
236
247
  def track_response(