claude-mpm 4.0.32__py3-none-any.whl → 4.0.34__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 (67) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/templates/documentation.json +51 -34
  3. claude_mpm/agents/templates/research.json +0 -11
  4. claude_mpm/cli/__init__.py +63 -26
  5. claude_mpm/cli/commands/agent_manager.py +10 -8
  6. claude_mpm/core/framework_loader.py +173 -84
  7. claude_mpm/dashboard/static/css/dashboard.css +449 -0
  8. claude_mpm/dashboard/static/dist/components/agent-inference.js +1 -1
  9. claude_mpm/dashboard/static/dist/components/event-viewer.js +1 -1
  10. claude_mpm/dashboard/static/dist/components/file-tool-tracker.js +1 -1
  11. claude_mpm/dashboard/static/dist/components/module-viewer.js +1 -1
  12. claude_mpm/dashboard/static/dist/components/session-manager.js +1 -1
  13. claude_mpm/dashboard/static/dist/dashboard.js +1 -1
  14. claude_mpm/dashboard/static/dist/socket-client.js +1 -1
  15. claude_mpm/dashboard/static/js/components/agent-hierarchy.js +774 -0
  16. claude_mpm/dashboard/static/js/components/agent-inference.js +257 -3
  17. claude_mpm/dashboard/static/js/components/build-tracker.js +289 -0
  18. claude_mpm/dashboard/static/js/components/event-viewer.js +168 -39
  19. claude_mpm/dashboard/static/js/components/file-tool-tracker.js +17 -0
  20. claude_mpm/dashboard/static/js/components/session-manager.js +23 -3
  21. claude_mpm/dashboard/static/js/components/socket-manager.js +2 -0
  22. claude_mpm/dashboard/static/js/dashboard.js +207 -31
  23. claude_mpm/dashboard/static/js/socket-client.js +85 -6
  24. claude_mpm/dashboard/templates/index.html +1 -0
  25. claude_mpm/hooks/claude_hooks/connection_pool.py +12 -2
  26. claude_mpm/hooks/claude_hooks/event_handlers.py +81 -19
  27. claude_mpm/hooks/claude_hooks/hook_handler.py +72 -10
  28. claude_mpm/hooks/claude_hooks/hook_handler_eventbus.py +398 -0
  29. claude_mpm/hooks/claude_hooks/response_tracking.py +10 -0
  30. claude_mpm/services/agents/deployment/agent_deployment.py +34 -48
  31. claude_mpm/services/agents/deployment/agent_template_builder.py +18 -10
  32. claude_mpm/services/agents/deployment/agents_directory_resolver.py +10 -25
  33. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +189 -3
  34. claude_mpm/services/agents/deployment/pipeline/steps/target_directory_step.py +3 -2
  35. claude_mpm/services/agents/deployment/strategies/system_strategy.py +10 -3
  36. claude_mpm/services/agents/deployment/strategies/user_strategy.py +10 -14
  37. claude_mpm/services/agents/deployment/system_instructions_deployer.py +8 -85
  38. claude_mpm/services/agents/memory/content_manager.py +98 -105
  39. claude_mpm/services/event_bus/__init__.py +18 -0
  40. claude_mpm/services/event_bus/event_bus.py +334 -0
  41. claude_mpm/services/event_bus/relay.py +301 -0
  42. claude_mpm/services/events/__init__.py +44 -0
  43. claude_mpm/services/events/consumers/__init__.py +18 -0
  44. claude_mpm/services/events/consumers/dead_letter.py +296 -0
  45. claude_mpm/services/events/consumers/logging.py +183 -0
  46. claude_mpm/services/events/consumers/metrics.py +242 -0
  47. claude_mpm/services/events/consumers/socketio.py +376 -0
  48. claude_mpm/services/events/core.py +470 -0
  49. claude_mpm/services/events/interfaces.py +230 -0
  50. claude_mpm/services/events/producers/__init__.py +14 -0
  51. claude_mpm/services/events/producers/hook.py +269 -0
  52. claude_mpm/services/events/producers/system.py +327 -0
  53. claude_mpm/services/mcp_gateway/core/process_pool.py +411 -0
  54. claude_mpm/services/mcp_gateway/server/stdio_server.py +13 -0
  55. claude_mpm/services/monitor_build_service.py +345 -0
  56. claude_mpm/services/socketio/event_normalizer.py +667 -0
  57. claude_mpm/services/socketio/handlers/connection.py +78 -20
  58. claude_mpm/services/socketio/handlers/hook.py +14 -5
  59. claude_mpm/services/socketio/migration_utils.py +329 -0
  60. claude_mpm/services/socketio/server/broadcaster.py +26 -33
  61. claude_mpm/services/socketio/server/core.py +4 -3
  62. {claude_mpm-4.0.32.dist-info → claude_mpm-4.0.34.dist-info}/METADATA +4 -3
  63. {claude_mpm-4.0.32.dist-info → claude_mpm-4.0.34.dist-info}/RECORD +67 -46
  64. {claude_mpm-4.0.32.dist-info → claude_mpm-4.0.34.dist-info}/WHEEL +0 -0
  65. {claude_mpm-4.0.32.dist-info → claude_mpm-4.0.34.dist-info}/entry_points.txt +0 -0
  66. {claude_mpm-4.0.32.dist-info → claude_mpm-4.0.34.dist-info}/licenses/LICENSE +0 -0
  67. {claude_mpm-4.0.32.dist-info → claude_mpm-4.0.34.dist-info}/top_level.txt +0 -0
@@ -24,11 +24,41 @@ import time
24
24
  from collections import deque
25
25
  from datetime import datetime
26
26
 
27
- # Import extracted modules
28
- from .connection_pool import SocketIOConnectionPool
29
- from .event_handlers import EventHandlers
30
- from .memory_integration import MemoryHookManager
31
- from .response_tracking import ResponseTrackingManager
27
+ # Import extracted modules with fallback for direct execution
28
+ try:
29
+ # Try relative imports first (when imported as module)
30
+ from .connection_pool import SocketIOConnectionPool
31
+ from .event_handlers import EventHandlers
32
+ from .memory_integration import MemoryHookManager
33
+ from .response_tracking import ResponseTrackingManager
34
+ except ImportError:
35
+ # Fall back to absolute imports (when run directly)
36
+ import sys
37
+ from pathlib import Path
38
+ # Add parent directory to path
39
+ sys.path.insert(0, str(Path(__file__).parent))
40
+ from connection_pool import SocketIOConnectionPool
41
+ from event_handlers import EventHandlers
42
+ from memory_integration import MemoryHookManager
43
+ from response_tracking import ResponseTrackingManager
44
+
45
+ # Import EventNormalizer for consistent event formatting
46
+ try:
47
+ from claude_mpm.services.socketio.event_normalizer import EventNormalizer
48
+ except ImportError:
49
+ # Create a simple fallback EventNormalizer if import fails
50
+ class EventNormalizer:
51
+ def normalize(self, event_data):
52
+ """Simple fallback normalizer that returns event as-is."""
53
+ return type('NormalizedEvent', (), {
54
+ 'to_dict': lambda: {
55
+ 'event': 'claude_event',
56
+ 'type': event_data.get('type', 'unknown'),
57
+ 'subtype': event_data.get('subtype', 'generic'),
58
+ 'timestamp': event_data.get('timestamp', datetime.now().isoformat()),
59
+ 'data': event_data.get('data', event_data)
60
+ }
61
+ })
32
62
 
33
63
  # Import constants for configuration
34
64
  try:
@@ -86,6 +116,8 @@ class ClaudeHookHandler:
86
116
  # Track events for periodic cleanup
87
117
  self.events_processed = 0
88
118
  self.last_cleanup = time.time()
119
+ # Event normalizer for consistent event schema
120
+ self.event_normalizer = EventNormalizer()
89
121
 
90
122
  # Maximum sizes for tracking
91
123
  self.MAX_DELEGATION_TRACKING = 200
@@ -503,14 +535,15 @@ class ClaudeHookHandler:
503
535
  return int(os.environ.get("CLAUDE_MPM_SOCKETIO_PORT", "8765"))
504
536
 
505
537
  def _emit_socketio_event(self, namespace: str, event: str, data: dict):
506
- """Emit Socket.IO event with improved reliability and persistent connections.
538
+ """Emit Socket.IO event with improved reliability and event normalization.
507
539
 
508
540
  WHY improved approach:
541
+ - Uses EventNormalizer for consistent event schema
509
542
  - Maintains persistent connections throughout handler lifecycle
510
543
  - Better error handling and automatic recovery
511
544
  - Connection health monitoring before emission
512
545
  - Automatic reconnection for critical events
513
- - Validates data before emission
546
+ - All events normalized to standard schema before emission
514
547
  """
515
548
  # Always try to emit Socket.IO events if available
516
549
  # The daemon should be running when manager is active
@@ -568,12 +601,20 @@ class ClaudeHookHandler:
568
601
  )
569
602
  return
570
603
 
571
- # Format event for Socket.IO server
572
- claude_event_data = {
573
- "type": f"hook.{event}", # Dashboard expects 'hook.' prefix
604
+ # Create event data for normalization
605
+ raw_event = {
606
+ "type": "hook",
607
+ "subtype": event, # e.g., "user_prompt", "pre_tool", "subagent_stop"
574
608
  "timestamp": datetime.now().isoformat(),
575
609
  "data": data,
610
+ "source": "claude_hooks", # Identify the source
611
+ "session_id": data.get("sessionId"), # Include session if available
576
612
  }
613
+
614
+ # Normalize the event using EventNormalizer for consistent schema
615
+ # Pass source explicitly to ensure it's set correctly
616
+ normalized_event = self.event_normalizer.normalize(raw_event, source="hook")
617
+ claude_event_data = normalized_event.to_dict()
577
618
 
578
619
  # Log important events for debugging
579
620
  if DEBUG and event in ["subagent_stop", "pre_tool"]:
@@ -876,6 +917,17 @@ class ClaudeHookHandler:
876
917
  metadata["task_completed"] = structured_response.get(
877
918
  "task_completed", False
878
919
  )
920
+
921
+ # Check for MEMORIES field and process if present
922
+ if "MEMORIES" in structured_response and structured_response["MEMORIES"]:
923
+ memories = structured_response["MEMORIES"]
924
+ if DEBUG:
925
+ print(
926
+ f"Found MEMORIES field in {agent_type} response with {len(memories)} items",
927
+ file=sys.stderr,
928
+ )
929
+ # The memory will be processed by extract_and_update_memory
930
+ # which is called by the memory hook service
879
931
 
880
932
  # Track the response
881
933
  file_path = (
@@ -937,7 +989,17 @@ class ClaudeHookHandler:
937
989
  "files_modified": structured_response.get("files_modified", []),
938
990
  "tools_used": structured_response.get("tools_used", []),
939
991
  "remember": structured_response.get("remember"),
992
+ "MEMORIES": structured_response.get("MEMORIES"), # Complete memory replacement
940
993
  }
994
+
995
+ # Log if MEMORIES field is present
996
+ if "MEMORIES" in structured_response and structured_response["MEMORIES"]:
997
+ if DEBUG:
998
+ memories_count = len(structured_response["MEMORIES"])
999
+ print(
1000
+ f"Agent {agent_type} returned MEMORIES field with {memories_count} items",
1001
+ file=sys.stderr,
1002
+ )
941
1003
 
942
1004
  # Debug log the processed data
943
1005
  if DEBUG:
@@ -0,0 +1,398 @@
1
+ #!/usr/bin/env python3
2
+ """Optimized Claude Code hook handler with EventBus architecture.
3
+
4
+ This handler uses the EventBus for decoupled event emission instead of
5
+ direct Socket.IO connections. This provides better separation of concerns
6
+ and improved testability.
7
+
8
+ WHY EventBus approach:
9
+ - Decouples hook processing from Socket.IO implementation
10
+ - Enables multiple event consumers without code changes
11
+ - Simplifies testing by removing Socket.IO dependencies
12
+ - Provides centralized event routing and filtering
13
+ - Maintains backward compatibility with existing hooks
14
+ """
15
+
16
+ import json
17
+ import os
18
+ import select
19
+ import signal
20
+ import subprocess
21
+ import sys
22
+ import threading
23
+ import time
24
+ from collections import deque
25
+ from datetime import datetime
26
+ from pathlib import Path
27
+
28
+ # Add parent path for imports
29
+ sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent))
30
+
31
+ # Import EventBus
32
+ try:
33
+ from claude_mpm.services.event_bus import EventBus
34
+ EVENTBUS_AVAILABLE = True
35
+ except ImportError:
36
+ EVENTBUS_AVAILABLE = False
37
+ EventBus = None
38
+
39
+ # Import EventNormalizer for consistent event formatting
40
+ try:
41
+ from claude_mpm.services.socketio.event_normalizer import EventNormalizer
42
+ except ImportError:
43
+ # Create a simple fallback EventNormalizer if import fails
44
+ class EventNormalizer:
45
+ def normalize(self, event_data, source="hook"):
46
+ """Simple fallback normalizer that returns event as-is."""
47
+ return type('NormalizedEvent', (), {
48
+ 'to_dict': lambda: {
49
+ 'event': 'claude_event',
50
+ 'type': event_data.get('type', 'unknown'),
51
+ 'subtype': event_data.get('subtype', 'generic'),
52
+ 'timestamp': event_data.get('timestamp', datetime.now().isoformat()),
53
+ 'data': event_data.get('data', event_data),
54
+ 'source': source
55
+ }
56
+ })
57
+
58
+ # Import constants for configuration
59
+ try:
60
+ from claude_mpm.core.constants import TimeoutConfig
61
+ except ImportError:
62
+ # Fallback values if constants module not available
63
+ class TimeoutConfig:
64
+ QUICK_TIMEOUT = 2.0
65
+
66
+ # Import other handler modules
67
+ try:
68
+ from .memory_integration import MemoryHookManager
69
+ from .response_tracking import ResponseTrackingManager
70
+ from .event_handlers import EventHandlers
71
+ except ImportError:
72
+ # Fallback for direct execution
73
+ from memory_integration import MemoryHookManager
74
+ from response_tracking import ResponseTrackingManager
75
+ from event_handlers import EventHandlers
76
+
77
+ # Debug mode is enabled by default for better visibility into hook processing
78
+ DEBUG = os.environ.get("CLAUDE_MPM_HOOK_DEBUG", "true").lower() != "false"
79
+
80
+ # Global singleton handler instance
81
+ _global_handler = None
82
+ _handler_lock = threading.Lock()
83
+
84
+ # Track recent events to detect duplicates
85
+ _recent_events = deque(maxlen=10)
86
+ _events_lock = threading.Lock()
87
+
88
+
89
+ class HookHandler:
90
+ """Main hook handler class using EventBus for event emission.
91
+
92
+ WHY EventBus integration:
93
+ - Replaces direct Socket.IO connections with EventBus publishing
94
+ - Events are published once and consumed by multiple listeners
95
+ - Failures in one consumer don't affect others
96
+ - Simplified testing without Socket.IO dependencies
97
+ """
98
+
99
+ # Tracking dictionaries with size limits
100
+ MAX_DELEGATION_TRACKING = 100
101
+ MAX_PROMPT_TRACKING = 50
102
+ MAX_CACHE_AGE_SECONDS = 1800 # 30 minutes
103
+
104
+ def __init__(self):
105
+ """Initialize the hook handler with EventBus."""
106
+ # Initialize EventBus if available
107
+ self.event_bus = EventBus.get_instance() if EVENTBUS_AVAILABLE else None
108
+ self.event_normalizer = EventNormalizer()
109
+
110
+ # Initialize tracking managers
111
+ self.memory_manager = MemoryHookManager()
112
+ self.response_tracker = ResponseTrackingManager()
113
+ self.event_handlers = EventHandlers(self)
114
+
115
+ # Delegation tracking
116
+ self.active_delegations = {}
117
+ self.delegation_requests = {}
118
+ self.delegation_history = deque(maxlen=20)
119
+
120
+ # Prompt tracking
121
+ self.pending_prompts = {}
122
+
123
+ # Git branch caching
124
+ self._git_branch_cache = {}
125
+ self._git_branch_cache_time = {}
126
+
127
+ # Session tracking
128
+ self.current_session_id = None
129
+
130
+ # Cleanup old entries periodically
131
+ self._last_cleanup = time.time()
132
+
133
+ if self.event_bus:
134
+ logger_msg = "HookHandler initialized with EventBus"
135
+ else:
136
+ logger_msg = "HookHandler initialized (EventBus not available)"
137
+
138
+ if DEBUG:
139
+ print(f"🚀 {logger_msg}", file=sys.stderr)
140
+
141
+ def _emit_event(self, event_type: str, data: dict):
142
+ """Emit an event through the EventBus.
143
+
144
+ WHY this approach:
145
+ - Single point of event emission
146
+ - Consistent event normalization
147
+ - Graceful fallback if EventBus unavailable
148
+ - Easy to add metrics and monitoring
149
+
150
+ Args:
151
+ event_type: The event type (e.g., 'pre_tool', 'subagent_stop')
152
+ data: The event data
153
+ """
154
+ if not self.event_bus:
155
+ if DEBUG:
156
+ print(f"EventBus not available, cannot emit: hook.{event_type}", file=sys.stderr)
157
+ return
158
+
159
+ try:
160
+ # Create event data for normalization
161
+ raw_event = {
162
+ "type": "hook",
163
+ "subtype": event_type,
164
+ "timestamp": datetime.now().isoformat(),
165
+ "data": data,
166
+ "source": "claude_hooks",
167
+ "session_id": data.get("sessionId", self.current_session_id)
168
+ }
169
+
170
+ # Normalize the event
171
+ normalized_event = self.event_normalizer.normalize(raw_event, source="hook")
172
+ event_data = normalized_event.to_dict()
173
+
174
+ # Publish to EventBus
175
+ success = self.event_bus.publish(f"hook.{event_type}", event_data)
176
+
177
+ if DEBUG:
178
+ if success:
179
+ print(f"✅ Published to EventBus: hook.{event_type}", file=sys.stderr)
180
+ else:
181
+ print(f"⚠️ EventBus rejected event: hook.{event_type}", file=sys.stderr)
182
+
183
+ # Log important events
184
+ if DEBUG and event_type in ["subagent_stop", "pre_tool"]:
185
+ if event_type == "subagent_stop":
186
+ agent_type = data.get("agent_type", "unknown")
187
+ print(f"📤 Published SubagentStop for agent '{agent_type}'", file=sys.stderr)
188
+ elif event_type == "pre_tool" and data.get("tool_name") == "Task":
189
+ delegation = data.get("delegation_details", {})
190
+ agent_type = delegation.get("agent_type", "unknown")
191
+ print(f"📤 Published Task delegation to agent '{agent_type}'", file=sys.stderr)
192
+
193
+ except Exception as e:
194
+ if DEBUG:
195
+ print(f"❌ Failed to publish event hook.{event_type}: {e}", file=sys.stderr)
196
+
197
+ def _get_git_branch(self, working_dir: str = None) -> str:
198
+ """Get git branch for the given directory with caching."""
199
+ # Use current working directory if not specified
200
+ if not working_dir:
201
+ working_dir = os.getcwd()
202
+
203
+ # Check cache first (cache for 30 seconds)
204
+ current_time = time.time()
205
+ cache_key = working_dir
206
+
207
+ if (
208
+ cache_key in self._git_branch_cache
209
+ and cache_key in self._git_branch_cache_time
210
+ and current_time - self._git_branch_cache_time[cache_key] < 30
211
+ ):
212
+ return self._git_branch_cache[cache_key]
213
+
214
+ # Try to get git branch
215
+ try:
216
+ # Change to the working directory temporarily
217
+ original_cwd = os.getcwd()
218
+ os.chdir(working_dir)
219
+
220
+ # Run git command to get current branch
221
+ result = subprocess.run(
222
+ ["git", "branch", "--show-current"],
223
+ capture_output=True,
224
+ text=True,
225
+ timeout=TimeoutConfig.QUICK_TIMEOUT
226
+ )
227
+
228
+ # Restore original directory
229
+ os.chdir(original_cwd)
230
+
231
+ if result.returncode == 0 and result.stdout.strip():
232
+ branch = result.stdout.strip()
233
+ # Cache the result
234
+ self._git_branch_cache[cache_key] = branch
235
+ self._git_branch_cache_time[cache_key] = current_time
236
+ return branch
237
+ else:
238
+ return "unknown"
239
+
240
+ except Exception:
241
+ return "unknown"
242
+
243
+ def _cleanup_old_entries(self):
244
+ """Clean up old entries to prevent memory growth."""
245
+ cutoff_time = time.time() - self.MAX_CACHE_AGE_SECONDS
246
+
247
+ # Clean up delegation tracking dictionaries
248
+ for storage in [self.active_delegations, self.delegation_requests]:
249
+ if len(storage) > self.MAX_DELEGATION_TRACKING:
250
+ # Keep only the most recent entries
251
+ sorted_keys = sorted(storage.keys())
252
+ excess = len(storage) - self.MAX_DELEGATION_TRACKING
253
+ for key in sorted_keys[:excess]:
254
+ del storage[key]
255
+
256
+ # Clean up pending prompts
257
+ if len(self.pending_prompts) > self.MAX_PROMPT_TRACKING:
258
+ sorted_keys = sorted(self.pending_prompts.keys())
259
+ excess = len(self.pending_prompts) - self.MAX_PROMPT_TRACKING
260
+ for key in sorted_keys[:excess]:
261
+ del self.pending_prompts[key]
262
+
263
+ # Clean up git branch cache
264
+ expired_keys = [
265
+ key
266
+ for key, cache_time in self._git_branch_cache_time.items()
267
+ if time.time() - cache_time > self.MAX_CACHE_AGE_SECONDS
268
+ ]
269
+ for key in expired_keys:
270
+ self._git_branch_cache.pop(key, None)
271
+ self._git_branch_cache_time.pop(key, None)
272
+
273
+ def handle_event(self, event: dict):
274
+ """Process an event from Claude Code.
275
+
276
+ Args:
277
+ event: The event dictionary from Claude
278
+ """
279
+ # Periodic cleanup
280
+ current_time = time.time()
281
+ if current_time - self._last_cleanup > 300: # Every 5 minutes
282
+ self._cleanup_old_entries()
283
+ self._last_cleanup = current_time
284
+
285
+ # Extract event details
286
+ event_type = event.get("type", "")
287
+ event_name = event.get("name", "")
288
+
289
+ # Update session ID if present
290
+ if "sessionId" in event:
291
+ self.current_session_id = event["sessionId"]
292
+
293
+ # Detect duplicate events
294
+ event_signature = f"{event_type}:{event_name}:{json.dumps(event.get('data', ''))[:100]}"
295
+ with _events_lock:
296
+ if event_signature in _recent_events:
297
+ if DEBUG:
298
+ print(f"Skipping duplicate event: {event_type}", file=sys.stderr)
299
+ return
300
+ _recent_events.append(event_signature)
301
+
302
+ # Route to appropriate handler
303
+ if event_type == "Start":
304
+ self.event_handlers.handle_start(event)
305
+ elif event_type == "Stop":
306
+ self.event_handlers.handle_stop(event)
307
+ elif event_type == "UserPrompt":
308
+ self.event_handlers.handle_user_prompt(event)
309
+ elif event_type == "AssistantResponse":
310
+ self.event_handlers.handle_assistant_response(event)
311
+ elif event_type == "SubagentStart":
312
+ self.event_handlers.handle_subagent_start(event)
313
+ elif event_type == "SubagentStop":
314
+ self.event_handlers.handle_subagent_stop(event)
315
+ elif event_type == "PreToolExecution" and event_name == "Task":
316
+ self.event_handlers.handle_task_delegation(event)
317
+ elif event_type == "PreToolExecution":
318
+ self.event_handlers.handle_pre_tool(event)
319
+ elif event_type == "PostToolExecution":
320
+ self.event_handlers.handle_post_tool(event)
321
+ elif event_type == "PromptCachingBetaStats":
322
+ # Ignore caching stats events
323
+ pass
324
+ else:
325
+ # Log unhandled events in debug mode
326
+ if DEBUG:
327
+ print(f"Unhandled event type: {event_type}", file=sys.stderr)
328
+
329
+
330
+ def get_handler() -> HookHandler:
331
+ """Get or create the global hook handler instance.
332
+
333
+ Returns:
334
+ HookHandler: The singleton handler instance
335
+ """
336
+ global _global_handler
337
+ if _global_handler is None:
338
+ with _handler_lock:
339
+ if _global_handler is None:
340
+ _global_handler = HookHandler()
341
+ return _global_handler
342
+
343
+
344
+ def main():
345
+ """Main entry point for the hook handler."""
346
+ if DEBUG:
347
+ print("🎯 EventBus Hook Handler starting...", file=sys.stderr)
348
+
349
+ handler = get_handler()
350
+
351
+ # Set up signal handling for clean shutdown
352
+ def signal_handler(signum, frame):
353
+ if DEBUG:
354
+ print("\n👋 Hook handler shutting down...", file=sys.stderr)
355
+ sys.exit(0)
356
+
357
+ signal.signal(signal.SIGINT, signal_handler)
358
+ signal.signal(signal.SIGTERM, signal_handler)
359
+
360
+ # Process events from stdin
361
+ try:
362
+ while True:
363
+ # Check if data is available with timeout
364
+ readable, _, _ = select.select([sys.stdin], [], [], 0.1)
365
+ if readable:
366
+ line = sys.stdin.readline()
367
+ if not line:
368
+ break
369
+
370
+ try:
371
+ event = json.loads(line.strip())
372
+ handler.handle_event(event)
373
+
374
+ # Acknowledge event
375
+ print(json.dumps({"status": "ok"}))
376
+ sys.stdout.flush()
377
+
378
+ except json.JSONDecodeError as e:
379
+ if DEBUG:
380
+ print(f"Invalid JSON: {e}", file=sys.stderr)
381
+ print(json.dumps({"status": "error", "message": str(e)}))
382
+ sys.stdout.flush()
383
+ except Exception as e:
384
+ if DEBUG:
385
+ print(f"Error processing event: {e}", file=sys.stderr)
386
+ print(json.dumps({"status": "error", "message": str(e)}))
387
+ sys.stdout.flush()
388
+
389
+ except KeyboardInterrupt:
390
+ if DEBUG:
391
+ print("\n👋 Hook handler interrupted", file=sys.stderr)
392
+ finally:
393
+ if DEBUG:
394
+ print("Hook handler exiting", file=sys.stderr)
395
+
396
+
397
+ if __name__ == "__main__":
398
+ main()
@@ -203,7 +203,17 @@ class ResponseTrackingManager:
203
203
  "files_modified": structured_response.get("files_modified", []),
204
204
  "tools_used": structured_response.get("tools_used", []),
205
205
  "remember": structured_response.get("remember"),
206
+ "MEMORIES": structured_response.get("MEMORIES"), # Complete memory replacement
206
207
  }
208
+
209
+ # Log if MEMORIES field is present
210
+ if "MEMORIES" in structured_response and structured_response["MEMORIES"]:
211
+ if DEBUG:
212
+ memories_count = len(structured_response["MEMORIES"])
213
+ print(
214
+ f"Agent {agent_type} returned MEMORIES field with {memories_count} items",
215
+ file=sys.stderr,
216
+ )
207
217
 
208
218
  # Check if task was completed for logging purposes
209
219
  if structured_response.get("task_completed"):