claude-mpm 3.9.0__py3-none-any.whl → 3.9.4__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/BASE_AGENT_TEMPLATE.md +59 -135
- claude_mpm/agents/MEMORY.md +5 -2
- claude_mpm/agents/WORKFLOW.md +54 -4
- claude_mpm/agents/agents_metadata.py +25 -1
- claude_mpm/agents/schema/agent_schema.json +1 -1
- claude_mpm/agents/templates/backup/research_agent_2025011_234551.json +88 -0
- claude_mpm/agents/templates/project_organizer.json +178 -0
- claude_mpm/agents/templates/research.json +33 -30
- claude_mpm/config/paths.py +56 -6
- claude_mpm/core/claude_runner.py +31 -10
- claude_mpm/hooks/claude_hooks/hook_handler.py +167 -105
- claude_mpm/hooks/claude_hooks/hook_handler_fixed.py +454 -0
- claude_mpm/utils/paths.py +112 -8
- {claude_mpm-3.9.0.dist-info → claude_mpm-3.9.4.dist-info}/METADATA +1 -1
- {claude_mpm-3.9.0.dist-info → claude_mpm-3.9.4.dist-info}/RECORD +20 -17
- {claude_mpm-3.9.0.dist-info → claude_mpm-3.9.4.dist-info}/WHEEL +0 -0
- {claude_mpm-3.9.0.dist-info → claude_mpm-3.9.4.dist-info}/entry_points.txt +0 -0
- {claude_mpm-3.9.0.dist-info → claude_mpm-3.9.4.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-3.9.0.dist-info → claude_mpm-3.9.4.dist-info}/top_level.txt +0 -0
| @@ -21,6 +21,7 @@ import time | |
| 21 21 | 
             
            import asyncio
         | 
| 22 22 | 
             
            from pathlib import Path
         | 
| 23 23 | 
             
            from collections import deque
         | 
| 24 | 
            +
            import threading
         | 
| 24 25 |  | 
| 25 26 | 
             
            # Import constants for configuration
         | 
| 26 27 | 
             
            try:
         | 
| @@ -87,6 +88,102 @@ except ImportError: | |
| 87 88 | 
             
            # No fallback needed - we only use Socket.IO now
         | 
| 88 89 |  | 
| 89 90 |  | 
| 91 | 
            +
             | 
| 92 | 
            +
            class SocketIOConnectionPool:
         | 
| 93 | 
            +
                """Connection pool for Socket.IO clients to prevent connection leaks."""
         | 
| 94 | 
            +
                
         | 
| 95 | 
            +
                def __init__(self, max_connections=3):
         | 
| 96 | 
            +
                    self.max_connections = max_connections
         | 
| 97 | 
            +
                    self.connections = []
         | 
| 98 | 
            +
                    self.last_cleanup = time.time()
         | 
| 99 | 
            +
                    
         | 
| 100 | 
            +
                def get_connection(self, port):
         | 
| 101 | 
            +
                    """Get or create a connection to the specified port."""
         | 
| 102 | 
            +
                    if time.time() - self.last_cleanup > 60:
         | 
| 103 | 
            +
                        self._cleanup_dead_connections()
         | 
| 104 | 
            +
                        self.last_cleanup = time.time()
         | 
| 105 | 
            +
                    
         | 
| 106 | 
            +
                    for conn in self.connections:
         | 
| 107 | 
            +
                        if conn.get('port') == port and conn.get('client'):
         | 
| 108 | 
            +
                            client = conn['client']
         | 
| 109 | 
            +
                            if self._is_connection_alive(client):
         | 
| 110 | 
            +
                                return client
         | 
| 111 | 
            +
                            else:
         | 
| 112 | 
            +
                                self.connections.remove(conn)
         | 
| 113 | 
            +
                    
         | 
| 114 | 
            +
                    if len(self.connections) < self.max_connections:
         | 
| 115 | 
            +
                        client = self._create_connection(port)
         | 
| 116 | 
            +
                        if client:
         | 
| 117 | 
            +
                            self.connections.append({
         | 
| 118 | 
            +
                                'port': port,
         | 
| 119 | 
            +
                                'client': client,
         | 
| 120 | 
            +
                                'created': time.time()
         | 
| 121 | 
            +
                            })
         | 
| 122 | 
            +
                            return client
         | 
| 123 | 
            +
                    
         | 
| 124 | 
            +
                    if self.connections:
         | 
| 125 | 
            +
                        oldest = min(self.connections, key=lambda x: x['created'])
         | 
| 126 | 
            +
                        self._close_connection(oldest['client'])
         | 
| 127 | 
            +
                        oldest['client'] = self._create_connection(port)
         | 
| 128 | 
            +
                        oldest['port'] = port
         | 
| 129 | 
            +
                        oldest['created'] = time.time()
         | 
| 130 | 
            +
                        return oldest['client']
         | 
| 131 | 
            +
                    
         | 
| 132 | 
            +
                    return None
         | 
| 133 | 
            +
                
         | 
| 134 | 
            +
                def _create_connection(self, port):
         | 
| 135 | 
            +
                    """Create a new Socket.IO connection."""
         | 
| 136 | 
            +
                    if not SOCKETIO_AVAILABLE:
         | 
| 137 | 
            +
                        return None
         | 
| 138 | 
            +
                    try:
         | 
| 139 | 
            +
                        client = socketio.Client(
         | 
| 140 | 
            +
                            reconnection=False,  # Disable auto-reconnect
         | 
| 141 | 
            +
                            logger=False,
         | 
| 142 | 
            +
                            engineio_logger=False
         | 
| 143 | 
            +
                        )
         | 
| 144 | 
            +
                        client.connect(f'http://localhost:{port}', 
         | 
| 145 | 
            +
                                      wait=True, 
         | 
| 146 | 
            +
                                      wait_timeout=NetworkConfig.SOCKET_WAIT_TIMEOUT)
         | 
| 147 | 
            +
                        if client.connected:
         | 
| 148 | 
            +
                            return client
         | 
| 149 | 
            +
                    except Exception:
         | 
| 150 | 
            +
                        pass
         | 
| 151 | 
            +
                    return None
         | 
| 152 | 
            +
                
         | 
| 153 | 
            +
                def _is_connection_alive(self, client):
         | 
| 154 | 
            +
                    """Check if a connection is still alive."""
         | 
| 155 | 
            +
                    try:
         | 
| 156 | 
            +
                        return client and client.connected
         | 
| 157 | 
            +
                    except:
         | 
| 158 | 
            +
                        return False
         | 
| 159 | 
            +
                
         | 
| 160 | 
            +
                def _close_connection(self, client):
         | 
| 161 | 
            +
                    """Safely close a connection."""
         | 
| 162 | 
            +
                    try:
         | 
| 163 | 
            +
                        if client:
         | 
| 164 | 
            +
                            client.disconnect()
         | 
| 165 | 
            +
                    except:
         | 
| 166 | 
            +
                        pass
         | 
| 167 | 
            +
                
         | 
| 168 | 
            +
                def _cleanup_dead_connections(self):
         | 
| 169 | 
            +
                    """Remove dead connections from the pool."""
         | 
| 170 | 
            +
                    self.connections = [
         | 
| 171 | 
            +
                        conn for conn in self.connections 
         | 
| 172 | 
            +
                        if self._is_connection_alive(conn.get('client'))
         | 
| 173 | 
            +
                    ]
         | 
| 174 | 
            +
                
         | 
| 175 | 
            +
                def close_all(self):
         | 
| 176 | 
            +
                    """Close all connections in the pool."""
         | 
| 177 | 
            +
                    for conn in self.connections:
         | 
| 178 | 
            +
                        self._close_connection(conn.get('client'))
         | 
| 179 | 
            +
                    self.connections.clear()
         | 
| 180 | 
            +
             | 
| 181 | 
            +
             | 
| 182 | 
            +
            # Global singleton handler instance
         | 
| 183 | 
            +
            _global_handler = None
         | 
| 184 | 
            +
            _handler_lock = threading.Lock()
         | 
| 185 | 
            +
             | 
| 186 | 
            +
             | 
| 90 187 | 
             
            class ClaudeHookHandler:
         | 
| 91 188 | 
             
                """Optimized hook handler with direct Socket.IO client.
         | 
| 92 189 |  | 
| @@ -99,8 +196,17 @@ class ClaudeHookHandler: | |
| 99 196 |  | 
| 100 197 | 
             
                def __init__(self):
         | 
| 101 198 | 
             
                    # Socket.IO client (persistent if possible)
         | 
| 102 | 
            -
                    self. | 
| 103 | 
            -
                     | 
| 199 | 
            +
                    self.connection_pool = SocketIOConnectionPool(max_connections=3)
         | 
| 200 | 
            +
                    # Track events for periodic cleanup
         | 
| 201 | 
            +
                    self.events_processed = 0
         | 
| 202 | 
            +
                    self.last_cleanup = time.time()
         | 
| 203 | 
            +
                    
         | 
| 204 | 
            +
                    # Maximum sizes for tracking
         | 
| 205 | 
            +
                    self.MAX_DELEGATION_TRACKING = 200
         | 
| 206 | 
            +
                    self.MAX_PROMPT_TRACKING = 100
         | 
| 207 | 
            +
                    self.MAX_CACHE_AGE_SECONDS = 300
         | 
| 208 | 
            +
                    self.CLEANUP_INTERVAL_EVENTS = 100
         | 
| 209 | 
            +
             | 
| 104 210 |  | 
| 105 211 | 
             
                    # Agent delegation tracking
         | 
| 106 212 | 
             
                    # Store recent Task delegations: session_id -> agent_type
         | 
| @@ -179,6 +285,36 @@ class ClaudeHookHandler: | |
| 179 285 | 
             
                            if key in self.delegation_requests:
         | 
| 180 286 | 
             
                                del self.delegation_requests[key]
         | 
| 181 287 |  | 
| 288 | 
            +
                
         | 
| 289 | 
            +
                def _cleanup_old_entries(self):
         | 
| 290 | 
            +
                    """Clean up old entries to prevent memory growth."""
         | 
| 291 | 
            +
                    cutoff_time = datetime.now().timestamp() - self.MAX_CACHE_AGE_SECONDS
         | 
| 292 | 
            +
                    
         | 
| 293 | 
            +
                    # Clean up delegation tracking dictionaries
         | 
| 294 | 
            +
                    for storage in [self.active_delegations, self.delegation_requests]:
         | 
| 295 | 
            +
                        if len(storage) > self.MAX_DELEGATION_TRACKING:
         | 
| 296 | 
            +
                            # Keep only the most recent entries
         | 
| 297 | 
            +
                            sorted_keys = sorted(storage.keys())
         | 
| 298 | 
            +
                            excess = len(storage) - self.MAX_DELEGATION_TRACKING
         | 
| 299 | 
            +
                            for key in sorted_keys[:excess]:
         | 
| 300 | 
            +
                                del storage[key]
         | 
| 301 | 
            +
                    
         | 
| 302 | 
            +
                    # Clean up pending prompts
         | 
| 303 | 
            +
                    if len(self.pending_prompts) > self.MAX_PROMPT_TRACKING:
         | 
| 304 | 
            +
                        sorted_keys = sorted(self.pending_prompts.keys())
         | 
| 305 | 
            +
                        excess = len(self.pending_prompts) - self.MAX_PROMPT_TRACKING
         | 
| 306 | 
            +
                        for key in sorted_keys[:excess]:
         | 
| 307 | 
            +
                            del self.pending_prompts[key]
         | 
| 308 | 
            +
                    
         | 
| 309 | 
            +
                    # Clean up git branch cache
         | 
| 310 | 
            +
                    expired_keys = [
         | 
| 311 | 
            +
                        key for key, cache_time in self._git_branch_cache_time.items()
         | 
| 312 | 
            +
                        if datetime.now().timestamp() - cache_time > self.MAX_CACHE_AGE_SECONDS
         | 
| 313 | 
            +
                    ]
         | 
| 314 | 
            +
                    for key in expired_keys:
         | 
| 315 | 
            +
                        self._git_branch_cache.pop(key, None)
         | 
| 316 | 
            +
                        self._git_branch_cache_time.pop(key, None)
         | 
| 317 | 
            +
             | 
| 182 318 | 
             
                def _get_delegation_agent_type(self, session_id: str) -> str:
         | 
| 183 319 | 
             
                    """Get the agent type for a session's active delegation."""
         | 
| 184 320 | 
             
                    # First try exact session match
         | 
| @@ -455,97 +591,6 @@ class ClaudeHookHandler: | |
| 455 591 | 
             
                        self._git_branch_cache_time[cache_key] = current_time
         | 
| 456 592 | 
             
                        return 'Unknown'
         | 
| 457 593 |  | 
| 458 | 
            -
                def _get_socketio_client(self):
         | 
| 459 | 
            -
                    """Get or create Socket.IO client with improved reliability.
         | 
| 460 | 
            -
                    
         | 
| 461 | 
            -
                    WHY improved approach:
         | 
| 462 | 
            -
                    - Implements retry logic with exponential backoff
         | 
| 463 | 
            -
                    - Properly tests connection before returning
         | 
| 464 | 
            -
                    - Ensures connection persists across events
         | 
| 465 | 
            -
                    - Better error handling and recovery
         | 
| 466 | 
            -
                    """
         | 
| 467 | 
            -
                    if not SOCKETIO_AVAILABLE:
         | 
| 468 | 
            -
                        return None
         | 
| 469 | 
            -
                        
         | 
| 470 | 
            -
                    # Check if we have a connected client
         | 
| 471 | 
            -
                    if self.sio_client and self.sio_connected:
         | 
| 472 | 
            -
                        try:
         | 
| 473 | 
            -
                            # Test if still connected
         | 
| 474 | 
            -
                            if self.sio_client.connected:
         | 
| 475 | 
            -
                                return self.sio_client
         | 
| 476 | 
            -
                            else:
         | 
| 477 | 
            -
                                # Connection lost, clear it
         | 
| 478 | 
            -
                                if DEBUG:
         | 
| 479 | 
            -
                                    print("Hook handler: Socket.IO connection lost, reconnecting...", file=sys.stderr)
         | 
| 480 | 
            -
                                self.sio_connected = False
         | 
| 481 | 
            -
                        except:
         | 
| 482 | 
            -
                            self.sio_connected = False
         | 
| 483 | 
            -
                    
         | 
| 484 | 
            -
                    # Need to create or reconnect client
         | 
| 485 | 
            -
                    port = int(os.environ.get('CLAUDE_MPM_SOCKETIO_PORT', '8765'))
         | 
| 486 | 
            -
                    max_retries = RetryConfig.MAX_RETRIES
         | 
| 487 | 
            -
                    retry_delay = RetryConfig.INITIAL_RETRY_DELAY
         | 
| 488 | 
            -
                    
         | 
| 489 | 
            -
                    for attempt in range(max_retries):
         | 
| 490 | 
            -
                        try:
         | 
| 491 | 
            -
                            # Clean up old client if exists
         | 
| 492 | 
            -
                            if self.sio_client and not self.sio_connected:
         | 
| 493 | 
            -
                                try:
         | 
| 494 | 
            -
                                    self.sio_client.disconnect()
         | 
| 495 | 
            -
                                except:
         | 
| 496 | 
            -
                                    pass
         | 
| 497 | 
            -
                                self.sio_client = None
         | 
| 498 | 
            -
                            
         | 
| 499 | 
            -
                            # Create new client
         | 
| 500 | 
            -
                            self.sio_client = socketio.Client(
         | 
| 501 | 
            -
                                reconnection=True,  # Enable auto-reconnection
         | 
| 502 | 
            -
                                reconnection_attempts=3,
         | 
| 503 | 
            -
                                reconnection_delay=NetworkConfig.RECONNECTION_DELAY,
         | 
| 504 | 
            -
                                reconnection_delay_max=2,
         | 
| 505 | 
            -
                                logger=False,
         | 
| 506 | 
            -
                                engineio_logger=False
         | 
| 507 | 
            -
                            )
         | 
| 508 | 
            -
                            
         | 
| 509 | 
            -
                            # Try to connect with proper wait
         | 
| 510 | 
            -
                            self.sio_client.connect(
         | 
| 511 | 
            -
                                f'http://localhost:{port}', 
         | 
| 512 | 
            -
                                wait=True, 
         | 
| 513 | 
            -
                                wait_timeout=NetworkConfig.SOCKET_WAIT_TIMEOUT
         | 
| 514 | 
            -
                            )
         | 
| 515 | 
            -
                            
         | 
| 516 | 
            -
                            # Verify connection
         | 
| 517 | 
            -
                            if self.sio_client.connected:
         | 
| 518 | 
            -
                                self.sio_connected = True
         | 
| 519 | 
            -
                                if DEBUG:
         | 
| 520 | 
            -
                                    print(f"Hook handler: Successfully connected to Socket.IO server on port {port} (attempt {attempt + 1})", file=sys.stderr)
         | 
| 521 | 
            -
                                return self.sio_client
         | 
| 522 | 
            -
                                
         | 
| 523 | 
            -
                        except Exception as e:
         | 
| 524 | 
            -
                            if DEBUG and attempt == max_retries - 1:
         | 
| 525 | 
            -
                                print(f"Hook handler: Failed to connect to Socket.IO after {max_retries} attempts: {e}", file=sys.stderr)
         | 
| 526 | 
            -
                            elif DEBUG:
         | 
| 527 | 
            -
                                print(f"Hook handler: Connection attempt {attempt + 1} failed, retrying...", file=sys.stderr)
         | 
| 528 | 
            -
                            
         | 
| 529 | 
            -
                            # Exponential backoff with async delay
         | 
| 530 | 
            -
                            if attempt < max_retries - 1:
         | 
| 531 | 
            -
                                # Use asyncio.sleep if in async context, otherwise fall back to time.sleep
         | 
| 532 | 
            -
                                try:
         | 
| 533 | 
            -
                                    loop = asyncio.get_event_loop()
         | 
| 534 | 
            -
                                    if loop.is_running():
         | 
| 535 | 
            -
                                        # We're in an async context, use async sleep
         | 
| 536 | 
            -
                                        asyncio.create_task(asyncio.sleep(retry_delay))
         | 
| 537 | 
            -
                                    else:
         | 
| 538 | 
            -
                                        # Sync context, use regular sleep
         | 
| 539 | 
            -
                                        time.sleep(retry_delay)
         | 
| 540 | 
            -
                                except:
         | 
| 541 | 
            -
                                    # Fallback to sync sleep if asyncio not available
         | 
| 542 | 
            -
                                    time.sleep(retry_delay)
         | 
| 543 | 
            -
                                retry_delay *= 2  # Double the delay for next attempt
         | 
| 544 | 
            -
                    
         | 
| 545 | 
            -
                    # All attempts failed
         | 
| 546 | 
            -
                    self.sio_client = None
         | 
| 547 | 
            -
                    self.sio_connected = False
         | 
| 548 | 
            -
                    return None
         | 
| 549 594 |  | 
| 550 595 | 
             
                def handle(self):
         | 
| 551 596 | 
             
                    """Process hook event with minimal overhead and zero blocking delays.
         | 
| @@ -565,6 +610,13 @@ class ClaudeHookHandler: | |
| 565 610 | 
             
                            self._continue_execution()
         | 
| 566 611 | 
             
                            return
         | 
| 567 612 |  | 
| 613 | 
            +
                        # Increment event counter and perform periodic cleanup
         | 
| 614 | 
            +
                        self.events_processed += 1
         | 
| 615 | 
            +
                        if self.events_processed % self.CLEANUP_INTERVAL_EVENTS == 0:
         | 
| 616 | 
            +
                            self._cleanup_old_entries()
         | 
| 617 | 
            +
                            if DEBUG:
         | 
| 618 | 
            +
                                print(f"🧹 Performed cleanup after {self.events_processed} events", file=sys.stderr)
         | 
| 619 | 
            +
                        
         | 
| 568 620 | 
             
                        # Route event to appropriate handler
         | 
| 569 621 | 
             
                        self._route_event(event)
         | 
| 570 622 |  | 
| @@ -647,7 +699,8 @@ class ClaudeHookHandler: | |
| 647 699 | 
             
                    # The daemon should be running when manager is active
         | 
| 648 700 |  | 
| 649 701 | 
             
                    # Get Socket.IO client
         | 
| 650 | 
            -
                     | 
| 702 | 
            +
                    port = int(os.environ.get('CLAUDE_MPM_SOCKETIO_PORT', '8765'))
         | 
| 703 | 
            +
                    client = self.connection_pool.get_connection(port)
         | 
| 651 704 | 
             
                    if not client:
         | 
| 652 705 | 
             
                        if DEBUG:
         | 
| 653 706 | 
             
                            print(f"Hook handler: No Socket.IO client available for event: hook.{event}", file=sys.stderr)
         | 
| @@ -680,22 +733,18 @@ class ClaudeHookHandler: | |
| 680 733 | 
             
                                print(f"✅ Successfully emitted Socket.IO event: hook.{event}", file=sys.stderr)
         | 
| 681 734 | 
             
                            else:
         | 
| 682 735 | 
             
                                print(f"⚠️ Event emitted but connection status uncertain: hook.{event}", file=sys.stderr)
         | 
| 683 | 
            -
                                self.sio_connected = False  # Force reconnection next time
         | 
| 684 736 |  | 
| 685 737 | 
             
                    except Exception as e:
         | 
| 686 738 | 
             
                        if DEBUG:
         | 
| 687 739 | 
             
                            print(f"❌ Socket.IO emit failed for hook.{event}: {e}", file=sys.stderr)
         | 
| 688 | 
            -
                        # Mark as disconnected so next call will reconnect
         | 
| 689 | 
            -
                        self.sio_connected = False
         | 
| 690 740 |  | 
| 691 741 | 
             
                        # Try to reconnect immediately for critical events
         | 
| 692 742 | 
             
                        if event in ['subagent_stop', 'pre_tool']:
         | 
| 693 743 | 
             
                            if DEBUG:
         | 
| 694 744 | 
             
                                print(f"Hook handler: Attempting immediate reconnection for critical event: hook.{event}", file=sys.stderr)
         | 
| 695 | 
            -
                            # Clear the client to force reconnection
         | 
| 696 | 
            -
                            self.sio_client = None
         | 
| 697 745 | 
             
                            # Try to get a new client and emit again
         | 
| 698 | 
            -
                             | 
| 746 | 
            +
                            retry_port = int(os.environ.get('CLAUDE_MPM_SOCKETIO_PORT', '8765'))
         | 
| 747 | 
            +
                            retry_client = self.connection_pool.get_connection(retry_port)
         | 
| 699 748 | 
             
                            if retry_client:
         | 
| 700 749 | 
             
                                try:
         | 
| 701 750 | 
             
                                    retry_client.emit('claude_event', claude_event_data)
         | 
| @@ -1667,18 +1716,31 @@ class ClaudeHookHandler: | |
| 1667 1716 | 
             
                        # Don't fail the delegation result - memory is optional
         | 
| 1668 1717 |  | 
| 1669 1718 | 
             
                def __del__(self):
         | 
| 1670 | 
            -
                    """Cleanup Socket.IO  | 
| 1671 | 
            -
                    if self | 
| 1719 | 
            +
                    """Cleanup Socket.IO connections on handler destruction."""
         | 
| 1720 | 
            +
                    if hasattr(self, 'connection_pool') and self.connection_pool:
         | 
| 1672 1721 | 
             
                        try:
         | 
| 1673 | 
            -
                            self. | 
| 1722 | 
            +
                            self.connection_pool.close_all()
         | 
| 1674 1723 | 
             
                        except:
         | 
| 1675 1724 | 
             
                            pass
         | 
| 1676 1725 |  | 
| 1677 1726 |  | 
| 1678 1727 | 
             
            def main():
         | 
| 1679 | 
            -
                """Entry point with  | 
| 1728 | 
            +
                """Entry point with singleton pattern to prevent multiple instances."""
         | 
| 1729 | 
            +
                global _global_handler
         | 
| 1730 | 
            +
                
         | 
| 1680 1731 | 
             
                try:
         | 
| 1681 | 
            -
                     | 
| 1732 | 
            +
                    # Use singleton pattern to prevent creating multiple instances
         | 
| 1733 | 
            +
                    with _handler_lock:
         | 
| 1734 | 
            +
                        if _global_handler is None:
         | 
| 1735 | 
            +
                            _global_handler = ClaudeHookHandler()
         | 
| 1736 | 
            +
                            if DEBUG:
         | 
| 1737 | 
            +
                                print(f"✅ Created new ClaudeHookHandler singleton (pid: {os.getpid()})", file=sys.stderr)
         | 
| 1738 | 
            +
                        else:
         | 
| 1739 | 
            +
                            if DEBUG:
         | 
| 1740 | 
            +
                                print(f"♻️ Reusing existing ClaudeHookHandler singleton (pid: {os.getpid()})", file=sys.stderr)
         | 
| 1741 | 
            +
                        
         | 
| 1742 | 
            +
                        handler = _global_handler
         | 
| 1743 | 
            +
                    
         | 
| 1682 1744 | 
             
                    handler.handle()
         | 
| 1683 1745 | 
             
                except Exception as e:
         | 
| 1684 1746 | 
             
                    # Always output continue action to not block Claude
         |