claude-mpm 3.4.26__py3-none-any.whl → 3.5.0__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/INSTRUCTIONS.md +182 -299
- claude_mpm/agents/agent_loader.py +283 -57
- claude_mpm/agents/agent_loader_integration.py +6 -9
- claude_mpm/agents/base_agent.json +2 -1
- claude_mpm/agents/base_agent_loader.py +1 -1
- claude_mpm/cli/__init__.py +6 -10
- claude_mpm/cli/commands/__init__.py +0 -2
- claude_mpm/cli/commands/agents.py +1 -1
- claude_mpm/cli/commands/memory.py +1 -1
- claude_mpm/cli/commands/run.py +12 -0
- claude_mpm/cli/parser.py +0 -13
- claude_mpm/cli/utils.py +1 -1
- claude_mpm/config/__init__.py +44 -2
- claude_mpm/config/agent_config.py +348 -0
- claude_mpm/config/paths.py +322 -0
- claude_mpm/constants.py +0 -1
- claude_mpm/core/__init__.py +2 -5
- claude_mpm/core/agent_registry.py +63 -17
- claude_mpm/core/claude_runner.py +354 -43
- claude_mpm/core/config.py +7 -1
- claude_mpm/core/config_aliases.py +4 -3
- claude_mpm/core/config_paths.py +151 -0
- claude_mpm/core/factories.py +4 -50
- claude_mpm/core/logger.py +11 -13
- claude_mpm/core/service_registry.py +2 -2
- claude_mpm/dashboard/static/js/components/agent-inference.js +101 -25
- claude_mpm/dashboard/static/js/components/event-processor.js +3 -2
- claude_mpm/hooks/claude_hooks/hook_handler.py +343 -83
- claude_mpm/hooks/memory_integration_hook.py +1 -1
- claude_mpm/init.py +37 -6
- claude_mpm/scripts/socketio_daemon.py +6 -2
- claude_mpm/services/__init__.py +71 -3
- claude_mpm/services/agents/__init__.py +85 -0
- claude_mpm/services/agents/deployment/__init__.py +21 -0
- claude_mpm/services/{agent_deployment.py → agents/deployment/agent_deployment.py} +192 -41
- claude_mpm/services/{agent_lifecycle_manager.py → agents/deployment/agent_lifecycle_manager.py} +11 -10
- claude_mpm/services/agents/loading/__init__.py +11 -0
- claude_mpm/services/{agent_profile_loader.py → agents/loading/agent_profile_loader.py} +9 -8
- claude_mpm/services/{base_agent_manager.py → agents/loading/base_agent_manager.py} +2 -2
- claude_mpm/services/{framework_agent_loader.py → agents/loading/framework_agent_loader.py} +116 -40
- claude_mpm/services/agents/management/__init__.py +9 -0
- claude_mpm/services/{agent_management_service.py → agents/management/agent_management_service.py} +6 -5
- claude_mpm/services/agents/memory/__init__.py +21 -0
- claude_mpm/services/{agent_memory_manager.py → agents/memory/agent_memory_manager.py} +3 -3
- claude_mpm/services/agents/registry/__init__.py +29 -0
- claude_mpm/services/{agent_registry.py → agents/registry/agent_registry.py} +101 -16
- claude_mpm/services/{deployed_agent_discovery.py → agents/registry/deployed_agent_discovery.py} +12 -2
- claude_mpm/services/{agent_modification_tracker.py → agents/registry/modification_tracker.py} +6 -5
- claude_mpm/services/async_session_logger.py +584 -0
- claude_mpm/services/claude_session_logger.py +299 -0
- claude_mpm/services/framework_claude_md_generator/content_assembler.py +2 -2
- claude_mpm/services/framework_claude_md_generator/section_generators/agents.py +17 -17
- claude_mpm/services/framework_claude_md_generator/section_generators/claude_pm_init.py +3 -3
- claude_mpm/services/framework_claude_md_generator/section_generators/core_responsibilities.py +1 -1
- claude_mpm/services/framework_claude_md_generator/section_generators/orchestration_principles.py +1 -1
- claude_mpm/services/framework_claude_md_generator/section_generators/todo_task_tools.py +19 -24
- claude_mpm/services/framework_claude_md_generator/section_generators/troubleshooting.py +1 -1
- claude_mpm/services/framework_claude_md_generator.py +4 -2
- claude_mpm/services/memory/__init__.py +17 -0
- claude_mpm/services/{memory_builder.py → memory/builder.py} +3 -3
- claude_mpm/services/memory/cache/__init__.py +14 -0
- claude_mpm/services/{shared_prompt_cache.py → memory/cache/shared_prompt_cache.py} +1 -1
- claude_mpm/services/memory/cache/simple_cache.py +317 -0
- claude_mpm/services/{memory_optimizer.py → memory/optimizer.py} +1 -1
- claude_mpm/services/{memory_router.py → memory/router.py} +1 -1
- claude_mpm/services/optimized_hook_service.py +542 -0
- claude_mpm/services/project_registry.py +14 -8
- claude_mpm/services/response_tracker.py +237 -0
- claude_mpm/services/ticketing_service_original.py +4 -2
- claude_mpm/services/version_control/branch_strategy.py +3 -1
- claude_mpm/utils/paths.py +12 -10
- claude_mpm/utils/session_logging.py +114 -0
- claude_mpm/validation/agent_validator.py +2 -1
- {claude_mpm-3.4.26.dist-info → claude_mpm-3.5.0.dist-info}/METADATA +26 -20
- {claude_mpm-3.4.26.dist-info → claude_mpm-3.5.0.dist-info}/RECORD +83 -106
- claude_mpm/cli/commands/ui.py +0 -57
- claude_mpm/core/simple_runner.py +0 -1046
- claude_mpm/hooks/builtin/__init__.py +0 -1
- claude_mpm/hooks/builtin/logging_hook_example.py +0 -165
- claude_mpm/hooks/builtin/memory_hooks_example.py +0 -67
- claude_mpm/hooks/builtin/mpm_command_hook.py +0 -125
- claude_mpm/hooks/builtin/post_delegation_hook_example.py +0 -124
- claude_mpm/hooks/builtin/pre_delegation_hook_example.py +0 -125
- claude_mpm/hooks/builtin/submit_hook_example.py +0 -100
- claude_mpm/hooks/builtin/ticket_extraction_hook_example.py +0 -237
- claude_mpm/hooks/builtin/todo_agent_prefix_hook.py +0 -240
- claude_mpm/hooks/builtin/workflow_start_hook.py +0 -181
- claude_mpm/orchestration/__init__.py +0 -6
- claude_mpm/orchestration/archive/direct_orchestrator.py +0 -195
- claude_mpm/orchestration/archive/factory.py +0 -215
- claude_mpm/orchestration/archive/hook_enabled_orchestrator.py +0 -188
- claude_mpm/orchestration/archive/hook_integration_example.py +0 -178
- claude_mpm/orchestration/archive/interactive_subprocess_orchestrator.py +0 -826
- claude_mpm/orchestration/archive/orchestrator.py +0 -501
- claude_mpm/orchestration/archive/pexpect_orchestrator.py +0 -252
- claude_mpm/orchestration/archive/pty_orchestrator.py +0 -270
- claude_mpm/orchestration/archive/simple_orchestrator.py +0 -82
- claude_mpm/orchestration/archive/subprocess_orchestrator.py +0 -801
- claude_mpm/orchestration/archive/system_prompt_orchestrator.py +0 -278
- claude_mpm/orchestration/archive/wrapper_orchestrator.py +0 -187
- claude_mpm/schemas/workflow_validator.py +0 -411
- claude_mpm/services/parent_directory_manager/__init__.py +0 -577
- claude_mpm/services/parent_directory_manager/backup_manager.py +0 -258
- claude_mpm/services/parent_directory_manager/config_manager.py +0 -210
- claude_mpm/services/parent_directory_manager/deduplication_manager.py +0 -279
- claude_mpm/services/parent_directory_manager/framework_protector.py +0 -143
- claude_mpm/services/parent_directory_manager/operations.py +0 -186
- claude_mpm/services/parent_directory_manager/state_manager.py +0 -624
- claude_mpm/services/parent_directory_manager/template_deployer.py +0 -579
- claude_mpm/services/parent_directory_manager/validation_manager.py +0 -378
- claude_mpm/services/parent_directory_manager/version_control_helper.py +0 -339
- claude_mpm/services/parent_directory_manager/version_manager.py +0 -222
- claude_mpm/ui/__init__.py +0 -1
- claude_mpm/ui/rich_terminal_ui.py +0 -295
- claude_mpm/ui/terminal_ui.py +0 -328
- /claude_mpm/services/{agent_versioning.py → agents/deployment/agent_versioning.py} +0 -0
- /claude_mpm/services/{agent_capabilities_generator.py → agents/management/agent_capabilities_generator.py} +0 -0
- /claude_mpm/services/{agent_persistence_service.py → agents/memory/agent_persistence_service.py} +0 -0
- {claude_mpm-3.4.26.dist-info → claude_mpm-3.5.0.dist-info}/WHEEL +0 -0
- {claude_mpm-3.4.26.dist-info → claude_mpm-3.5.0.dist-info}/entry_points.txt +0 -0
- {claude_mpm-3.4.26.dist-info → claude_mpm-3.5.0.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-3.4.26.dist-info → claude_mpm-3.5.0.dist-info}/top_level.txt +0 -0
|
@@ -17,6 +17,7 @@ import sys
|
|
|
17
17
|
import os
|
|
18
18
|
import subprocess
|
|
19
19
|
from datetime import datetime
|
|
20
|
+
import time
|
|
20
21
|
from pathlib import Path
|
|
21
22
|
from collections import deque
|
|
22
23
|
|
|
@@ -26,12 +27,9 @@ DEBUG = os.environ.get('CLAUDE_MPM_HOOK_DEBUG', '').lower() == 'true'
|
|
|
26
27
|
# Add imports for memory hook integration with comprehensive error handling
|
|
27
28
|
MEMORY_HOOKS_AVAILABLE = False
|
|
28
29
|
try:
|
|
29
|
-
#
|
|
30
|
-
import
|
|
31
|
-
|
|
32
|
-
src_path = Path(__file__).parent.parent.parent.parent
|
|
33
|
-
if src_path.exists() and str(src_path) not in sys.path:
|
|
34
|
-
sys.path.insert(0, str(src_path))
|
|
30
|
+
# Use centralized path management for adding src to path
|
|
31
|
+
from claude_mpm.config.paths import paths
|
|
32
|
+
paths.ensure_in_path()
|
|
35
33
|
|
|
36
34
|
from claude_mpm.services.hook_service import HookService
|
|
37
35
|
from claude_mpm.hooks.memory_integration_hook import (
|
|
@@ -47,6 +45,16 @@ except Exception as e:
|
|
|
47
45
|
print(f"Memory hooks not available: {e}", file=sys.stderr)
|
|
48
46
|
MEMORY_HOOKS_AVAILABLE = False
|
|
49
47
|
|
|
48
|
+
# Response tracking integration
|
|
49
|
+
RESPONSE_TRACKING_AVAILABLE = False
|
|
50
|
+
try:
|
|
51
|
+
from claude_mpm.services.response_tracker import ResponseTracker
|
|
52
|
+
RESPONSE_TRACKING_AVAILABLE = True
|
|
53
|
+
except Exception as e:
|
|
54
|
+
if DEBUG:
|
|
55
|
+
print(f"Response tracking not available: {e}", file=sys.stderr)
|
|
56
|
+
RESPONSE_TRACKING_AVAILABLE = False
|
|
57
|
+
|
|
50
58
|
# Socket.IO import
|
|
51
59
|
try:
|
|
52
60
|
import socketio
|
|
@@ -78,6 +86,8 @@ class ClaudeHookHandler:
|
|
|
78
86
|
self.active_delegations = {}
|
|
79
87
|
# Use deque to limit memory usage (keep last 100 delegations)
|
|
80
88
|
self.delegation_history = deque(maxlen=100)
|
|
89
|
+
# Store delegation request data for response correlation: session_id -> request_data
|
|
90
|
+
self.delegation_requests = {}
|
|
81
91
|
|
|
82
92
|
# Git branch cache (to avoid repeated subprocess calls)
|
|
83
93
|
self._git_branch_cache = {}
|
|
@@ -90,15 +100,29 @@ class ClaudeHookHandler:
|
|
|
90
100
|
if MEMORY_HOOKS_AVAILABLE:
|
|
91
101
|
self._initialize_memory_hooks()
|
|
92
102
|
|
|
103
|
+
# Initialize response tracking if available and enabled
|
|
104
|
+
self.response_tracker = None
|
|
105
|
+
self.response_tracking_enabled = False
|
|
106
|
+
if RESPONSE_TRACKING_AVAILABLE:
|
|
107
|
+
self._initialize_response_tracking()
|
|
108
|
+
|
|
93
109
|
# No fallback server needed - we only use Socket.IO now
|
|
94
110
|
|
|
95
|
-
def _track_delegation(self, session_id: str, agent_type: str):
|
|
96
|
-
"""Track a new agent delegation."""
|
|
111
|
+
def _track_delegation(self, session_id: str, agent_type: str, request_data: dict = None):
|
|
112
|
+
"""Track a new agent delegation with optional request data for response correlation."""
|
|
97
113
|
if session_id and agent_type and agent_type != 'unknown':
|
|
98
114
|
self.active_delegations[session_id] = agent_type
|
|
99
115
|
key = f"{session_id}:{datetime.now().timestamp()}"
|
|
100
116
|
self.delegation_history.append((key, agent_type))
|
|
101
117
|
|
|
118
|
+
# Store request data for response tracking correlation
|
|
119
|
+
if request_data:
|
|
120
|
+
self.delegation_requests[session_id] = {
|
|
121
|
+
'agent_type': agent_type,
|
|
122
|
+
'request': request_data,
|
|
123
|
+
'timestamp': datetime.now().isoformat()
|
|
124
|
+
}
|
|
125
|
+
|
|
102
126
|
# Clean up old delegations (older than 5 minutes)
|
|
103
127
|
cutoff_time = datetime.now().timestamp() - 300
|
|
104
128
|
keys_to_remove = []
|
|
@@ -115,7 +139,10 @@ class ClaudeHookHandler:
|
|
|
115
139
|
keys_to_remove.append(sid)
|
|
116
140
|
|
|
117
141
|
for key in keys_to_remove:
|
|
118
|
-
|
|
142
|
+
if key in self.active_delegations:
|
|
143
|
+
del self.active_delegations[key]
|
|
144
|
+
if key in self.delegation_requests:
|
|
145
|
+
del self.delegation_requests[key]
|
|
119
146
|
|
|
120
147
|
def _get_delegation_agent_type(self, session_id: str) -> str:
|
|
121
148
|
"""Get the agent type for a session's active delegation."""
|
|
@@ -174,6 +201,129 @@ class ClaudeHookHandler:
|
|
|
174
201
|
print(f"❌ Failed to initialize memory hooks: {e}", file=sys.stderr)
|
|
175
202
|
# Don't fail the entire handler - memory system is optional
|
|
176
203
|
|
|
204
|
+
def _initialize_response_tracking(self):
|
|
205
|
+
"""Initialize response tracking if enabled in configuration.
|
|
206
|
+
|
|
207
|
+
WHY: This enables automatic capture and storage of agent responses
|
|
208
|
+
for analysis, debugging, and learning purposes. Integration into the
|
|
209
|
+
existing hook handler avoids duplicate event capture.
|
|
210
|
+
|
|
211
|
+
DESIGN DECISION: Check configuration to allow enabling/disabling
|
|
212
|
+
response tracking without code changes.
|
|
213
|
+
"""
|
|
214
|
+
try:
|
|
215
|
+
# Create configuration with optional config file
|
|
216
|
+
config_file = os.environ.get('CLAUDE_PM_CONFIG_FILE')
|
|
217
|
+
config = Config(config_file=config_file) if config_file else Config()
|
|
218
|
+
|
|
219
|
+
# Check if response tracking is enabled (check both sections for compatibility)
|
|
220
|
+
response_tracking_enabled = config.get('response_tracking.enabled', False)
|
|
221
|
+
response_logging_enabled = config.get('response_logging.enabled', False)
|
|
222
|
+
|
|
223
|
+
if not (response_tracking_enabled or response_logging_enabled):
|
|
224
|
+
if DEBUG:
|
|
225
|
+
print("Response tracking disabled - skipping initialization", file=sys.stderr)
|
|
226
|
+
return
|
|
227
|
+
|
|
228
|
+
# Initialize response tracker with config
|
|
229
|
+
self.response_tracker = ResponseTracker(config=config)
|
|
230
|
+
self.response_tracking_enabled = self.response_tracker.is_enabled()
|
|
231
|
+
|
|
232
|
+
if DEBUG:
|
|
233
|
+
print("✅ Response tracking initialized", file=sys.stderr)
|
|
234
|
+
|
|
235
|
+
except Exception as e:
|
|
236
|
+
if DEBUG:
|
|
237
|
+
print(f"❌ Failed to initialize response tracking: {e}", file=sys.stderr)
|
|
238
|
+
# Don't fail the entire handler - response tracking is optional
|
|
239
|
+
|
|
240
|
+
def _track_agent_response(self, session_id: str, agent_type: str, event: dict):
|
|
241
|
+
"""Track agent response by correlating with original request and saving response.
|
|
242
|
+
|
|
243
|
+
WHY: This integrates response tracking into the existing hook flow,
|
|
244
|
+
capturing agent responses when Task delegations complete. It correlates
|
|
245
|
+
the response with the original request stored during pre-tool processing.
|
|
246
|
+
|
|
247
|
+
DESIGN DECISION: Only track responses if response tracking is enabled
|
|
248
|
+
and we have the original request data. Graceful error handling ensures
|
|
249
|
+
response tracking failures don't break hook processing.
|
|
250
|
+
"""
|
|
251
|
+
if not self.response_tracking_enabled or not self.response_tracker:
|
|
252
|
+
return
|
|
253
|
+
|
|
254
|
+
try:
|
|
255
|
+
# Get the original request data stored during pre-tool
|
|
256
|
+
request_info = self.delegation_requests.get(session_id)
|
|
257
|
+
if not request_info:
|
|
258
|
+
if DEBUG:
|
|
259
|
+
print(f"No request data found for session {session_id}, skipping response tracking", file=sys.stderr)
|
|
260
|
+
return
|
|
261
|
+
|
|
262
|
+
# Extract response from event output
|
|
263
|
+
response = event.get('output', '')
|
|
264
|
+
if not response:
|
|
265
|
+
# If no output, use error or construct a basic response
|
|
266
|
+
error = event.get('error', '')
|
|
267
|
+
exit_code = event.get('exit_code', 0)
|
|
268
|
+
if error:
|
|
269
|
+
response = f"Error: {error}"
|
|
270
|
+
else:
|
|
271
|
+
response = f"Task completed with exit code: {exit_code}"
|
|
272
|
+
|
|
273
|
+
# Convert response to string if it's not already
|
|
274
|
+
response_text = str(response)
|
|
275
|
+
|
|
276
|
+
# Get the original request (prompt + description)
|
|
277
|
+
original_request = request_info.get('request', {})
|
|
278
|
+
prompt = original_request.get('prompt', '')
|
|
279
|
+
description = original_request.get('description', '')
|
|
280
|
+
|
|
281
|
+
# Combine prompt and description for the full request
|
|
282
|
+
full_request = prompt
|
|
283
|
+
if description and description != prompt:
|
|
284
|
+
if full_request:
|
|
285
|
+
full_request += f"\n\nDescription: {description}"
|
|
286
|
+
else:
|
|
287
|
+
full_request = description
|
|
288
|
+
|
|
289
|
+
if not full_request:
|
|
290
|
+
full_request = f"Task delegation to {agent_type} agent"
|
|
291
|
+
|
|
292
|
+
# Prepare metadata
|
|
293
|
+
metadata = {
|
|
294
|
+
'exit_code': event.get('exit_code', 0),
|
|
295
|
+
'success': event.get('exit_code', 0) == 0,
|
|
296
|
+
'has_error': bool(event.get('error')),
|
|
297
|
+
'duration_ms': event.get('duration_ms'),
|
|
298
|
+
'working_directory': event.get('cwd', ''),
|
|
299
|
+
'timestamp': datetime.now().isoformat(),
|
|
300
|
+
'tool_name': 'Task',
|
|
301
|
+
'original_request_timestamp': request_info.get('timestamp')
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
# Track the response
|
|
305
|
+
file_path = self.response_tracker.track_response(
|
|
306
|
+
agent_name=agent_type,
|
|
307
|
+
request=full_request,
|
|
308
|
+
response=response_text,
|
|
309
|
+
session_id=session_id,
|
|
310
|
+
metadata=metadata
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
if file_path and DEBUG:
|
|
314
|
+
print(f"✅ Tracked response for {agent_type} agent in session {session_id}: {file_path.name}", file=sys.stderr)
|
|
315
|
+
elif DEBUG and not file_path:
|
|
316
|
+
print(f"Response tracking returned None for {agent_type} agent (might be excluded or disabled)", file=sys.stderr)
|
|
317
|
+
|
|
318
|
+
# Clean up the request data after successful tracking
|
|
319
|
+
if session_id in self.delegation_requests:
|
|
320
|
+
del self.delegation_requests[session_id]
|
|
321
|
+
|
|
322
|
+
except Exception as e:
|
|
323
|
+
if DEBUG:
|
|
324
|
+
print(f"❌ Failed to track agent response: {e}", file=sys.stderr)
|
|
325
|
+
# Don't fail the hook processing - response tracking is optional
|
|
326
|
+
|
|
177
327
|
def _get_git_branch(self, working_dir: str = None) -> str:
|
|
178
328
|
"""Get git branch for the given directory with caching.
|
|
179
329
|
|
|
@@ -232,12 +382,13 @@ class ClaudeHookHandler:
|
|
|
232
382
|
return 'Unknown'
|
|
233
383
|
|
|
234
384
|
def _get_socketio_client(self):
|
|
235
|
-
"""Get or create Socket.IO client.
|
|
385
|
+
"""Get or create Socket.IO client with improved reliability.
|
|
236
386
|
|
|
237
|
-
WHY
|
|
238
|
-
-
|
|
239
|
-
-
|
|
240
|
-
-
|
|
387
|
+
WHY improved approach:
|
|
388
|
+
- Implements retry logic with exponential backoff
|
|
389
|
+
- Properly tests connection before returning
|
|
390
|
+
- Ensures connection persists across events
|
|
391
|
+
- Better error handling and recovery
|
|
241
392
|
"""
|
|
242
393
|
if not SOCKETIO_AVAILABLE:
|
|
243
394
|
return None
|
|
@@ -248,33 +399,68 @@ class ClaudeHookHandler:
|
|
|
248
399
|
# Test if still connected
|
|
249
400
|
if self.sio_client.connected:
|
|
250
401
|
return self.sio_client
|
|
402
|
+
else:
|
|
403
|
+
# Connection lost, clear it
|
|
404
|
+
if DEBUG:
|
|
405
|
+
print("Hook handler: Socket.IO connection lost, reconnecting...", file=sys.stderr)
|
|
406
|
+
self.sio_connected = False
|
|
251
407
|
except:
|
|
252
|
-
|
|
408
|
+
self.sio_connected = False
|
|
253
409
|
|
|
254
|
-
# Need to create
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
410
|
+
# Need to create or reconnect client
|
|
411
|
+
port = int(os.environ.get('CLAUDE_MPM_SOCKETIO_PORT', '8765'))
|
|
412
|
+
max_retries = 3
|
|
413
|
+
retry_delay = 0.1 # Start with 100ms
|
|
414
|
+
|
|
415
|
+
for attempt in range(max_retries):
|
|
416
|
+
try:
|
|
417
|
+
# Clean up old client if exists
|
|
418
|
+
if self.sio_client and not self.sio_connected:
|
|
419
|
+
try:
|
|
420
|
+
self.sio_client.disconnect()
|
|
421
|
+
except:
|
|
422
|
+
pass
|
|
423
|
+
self.sio_client = None
|
|
424
|
+
|
|
425
|
+
# Create new client
|
|
426
|
+
self.sio_client = socketio.Client(
|
|
427
|
+
reconnection=True, # Enable auto-reconnection
|
|
428
|
+
reconnection_attempts=3,
|
|
429
|
+
reconnection_delay=0.5,
|
|
430
|
+
reconnection_delay_max=2,
|
|
431
|
+
logger=False,
|
|
432
|
+
engineio_logger=False
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
# Try to connect with proper wait
|
|
436
|
+
self.sio_client.connect(
|
|
437
|
+
f'http://localhost:{port}',
|
|
438
|
+
wait=True,
|
|
439
|
+
wait_timeout=1.0 # Reasonable timeout
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
# Verify connection
|
|
443
|
+
if self.sio_client.connected:
|
|
444
|
+
self.sio_connected = True
|
|
445
|
+
if DEBUG:
|
|
446
|
+
print(f"Hook handler: Successfully connected to Socket.IO server on port {port} (attempt {attempt + 1})", file=sys.stderr)
|
|
447
|
+
return self.sio_client
|
|
448
|
+
|
|
449
|
+
except Exception as e:
|
|
450
|
+
if DEBUG and attempt == max_retries - 1:
|
|
451
|
+
print(f"Hook handler: Failed to connect to Socket.IO after {max_retries} attempts: {e}", file=sys.stderr)
|
|
452
|
+
elif DEBUG:
|
|
453
|
+
print(f"Hook handler: Connection attempt {attempt + 1} failed, retrying...", file=sys.stderr)
|
|
454
|
+
|
|
455
|
+
# Exponential backoff
|
|
456
|
+
if attempt < max_retries - 1:
|
|
457
|
+
time.sleep(retry_delay)
|
|
458
|
+
retry_delay *= 2 # Double the delay for next attempt
|
|
459
|
+
|
|
460
|
+
# All attempts failed
|
|
461
|
+
self.sio_client = None
|
|
462
|
+
self.sio_connected = False
|
|
463
|
+
return None
|
|
278
464
|
|
|
279
465
|
def handle(self):
|
|
280
466
|
"""Process hook event with minimal overhead and zero blocking delays.
|
|
@@ -318,40 +504,75 @@ class ClaudeHookHandler:
|
|
|
318
504
|
print(json.dumps({"action": "continue"}))
|
|
319
505
|
|
|
320
506
|
def _emit_socketio_event(self, namespace: str, event: str, data: dict):
|
|
321
|
-
"""Emit Socket.IO event
|
|
507
|
+
"""Emit Socket.IO event with improved reliability and logging.
|
|
322
508
|
|
|
323
|
-
WHY
|
|
324
|
-
-
|
|
325
|
-
-
|
|
326
|
-
-
|
|
327
|
-
-
|
|
509
|
+
WHY improved approach:
|
|
510
|
+
- Better error handling and recovery
|
|
511
|
+
- Comprehensive event logging for debugging
|
|
512
|
+
- Automatic reconnection on failure
|
|
513
|
+
- Validates data before emission
|
|
328
514
|
"""
|
|
329
|
-
#
|
|
330
|
-
|
|
331
|
-
return
|
|
515
|
+
# Always try to emit Socket.IO events if available
|
|
516
|
+
# The daemon should be running when manager is active
|
|
332
517
|
|
|
333
518
|
# Get Socket.IO client
|
|
334
519
|
client = self._get_socketio_client()
|
|
335
|
-
if client:
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
520
|
+
if not client:
|
|
521
|
+
if DEBUG:
|
|
522
|
+
print(f"Hook handler: No Socket.IO client available for event: hook.{event}", file=sys.stderr)
|
|
523
|
+
return
|
|
524
|
+
|
|
525
|
+
try:
|
|
526
|
+
# Format event for Socket.IO server
|
|
527
|
+
claude_event_data = {
|
|
528
|
+
'type': f'hook.{event}', # Dashboard expects 'hook.' prefix
|
|
529
|
+
'timestamp': datetime.now().isoformat(),
|
|
530
|
+
'data': data
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
# Log important events for debugging
|
|
534
|
+
if DEBUG and event in ['subagent_stop', 'pre_tool']:
|
|
535
|
+
if event == 'subagent_stop':
|
|
536
|
+
agent_type = data.get('agent_type', 'unknown')
|
|
537
|
+
print(f"Hook handler: Emitting SubagentStop for agent '{agent_type}'", file=sys.stderr)
|
|
538
|
+
elif event == 'pre_tool' and data.get('tool_name') == 'Task':
|
|
539
|
+
delegation = data.get('delegation_details', {})
|
|
540
|
+
agent_type = delegation.get('agent_type', 'unknown')
|
|
541
|
+
print(f"Hook handler: Emitting Task delegation to agent '{agent_type}'", file=sys.stderr)
|
|
542
|
+
|
|
543
|
+
# Emit synchronously with verification
|
|
544
|
+
client.emit('claude_event', claude_event_data)
|
|
545
|
+
|
|
546
|
+
# Verify emission for critical events
|
|
547
|
+
if event in ['subagent_stop', 'pre_tool'] and DEBUG:
|
|
548
|
+
if client.connected:
|
|
549
|
+
print(f"✅ Successfully emitted Socket.IO event: hook.{event}", file=sys.stderr)
|
|
550
|
+
else:
|
|
551
|
+
print(f"⚠️ Event emitted but connection status uncertain: hook.{event}", file=sys.stderr)
|
|
552
|
+
self.sio_connected = False # Force reconnection next time
|
|
349
553
|
|
|
350
|
-
|
|
554
|
+
except Exception as e:
|
|
555
|
+
if DEBUG:
|
|
556
|
+
print(f"❌ Socket.IO emit failed for hook.{event}: {e}", file=sys.stderr)
|
|
557
|
+
# Mark as disconnected so next call will reconnect
|
|
558
|
+
self.sio_connected = False
|
|
559
|
+
|
|
560
|
+
# Try to reconnect immediately for critical events
|
|
561
|
+
if event in ['subagent_stop', 'pre_tool']:
|
|
351
562
|
if DEBUG:
|
|
352
|
-
print(f"
|
|
353
|
-
#
|
|
354
|
-
self.
|
|
563
|
+
print(f"Hook handler: Attempting immediate reconnection for critical event: hook.{event}", file=sys.stderr)
|
|
564
|
+
# Clear the client to force reconnection
|
|
565
|
+
self.sio_client = None
|
|
566
|
+
# Try to get a new client and emit again
|
|
567
|
+
retry_client = self._get_socketio_client()
|
|
568
|
+
if retry_client:
|
|
569
|
+
try:
|
|
570
|
+
retry_client.emit('claude_event', claude_event_data)
|
|
571
|
+
if DEBUG:
|
|
572
|
+
print(f"✅ Successfully re-emitted event after reconnection: hook.{event}", file=sys.stderr)
|
|
573
|
+
except Exception as retry_e:
|
|
574
|
+
if DEBUG:
|
|
575
|
+
print(f"❌ Re-emission failed: {retry_e}", file=sys.stderr)
|
|
355
576
|
|
|
356
577
|
def _handle_user_prompt_fast(self, event):
|
|
357
578
|
"""Handle user prompt with comprehensive data capture.
|
|
@@ -373,7 +594,6 @@ class ClaudeHookHandler:
|
|
|
373
594
|
|
|
374
595
|
# Extract comprehensive prompt data
|
|
375
596
|
prompt_data = {
|
|
376
|
-
'event_type': 'user_prompt',
|
|
377
597
|
'prompt_text': prompt,
|
|
378
598
|
'prompt_preview': prompt[:200] if len(prompt) > 200 else prompt,
|
|
379
599
|
'prompt_length': len(prompt),
|
|
@@ -411,7 +631,6 @@ class ClaudeHookHandler:
|
|
|
411
631
|
git_branch = self._get_git_branch(working_dir) if working_dir else 'Unknown'
|
|
412
632
|
|
|
413
633
|
pre_tool_data = {
|
|
414
|
-
'event_type': 'pre_tool',
|
|
415
634
|
'tool_name': tool_name,
|
|
416
635
|
'operation_type': operation_type,
|
|
417
636
|
'tool_parameters': tool_params,
|
|
@@ -428,21 +647,56 @@ class ClaudeHookHandler:
|
|
|
428
647
|
|
|
429
648
|
# Add delegation-specific data if this is a Task tool
|
|
430
649
|
if tool_name == 'Task' and isinstance(tool_input, dict):
|
|
431
|
-
|
|
650
|
+
# Normalize agent type to handle capitalized names like "Research", "Engineer", etc.
|
|
651
|
+
raw_agent_type = tool_input.get('subagent_type', 'unknown')
|
|
652
|
+
|
|
653
|
+
# Use AgentNameNormalizer if available, otherwise simple lowercase normalization
|
|
654
|
+
try:
|
|
655
|
+
from claude_mpm.core.agent_name_normalizer import AgentNameNormalizer
|
|
656
|
+
normalizer = AgentNameNormalizer()
|
|
657
|
+
# Convert to Task format (lowercase with hyphens)
|
|
658
|
+
agent_type = normalizer.to_task_format(raw_agent_type) if raw_agent_type != 'unknown' else 'unknown'
|
|
659
|
+
except ImportError:
|
|
660
|
+
# Fallback to simple normalization
|
|
661
|
+
agent_type = raw_agent_type.lower().replace('_', '-') if raw_agent_type != 'unknown' else 'unknown'
|
|
662
|
+
|
|
432
663
|
pre_tool_data['delegation_details'] = {
|
|
433
664
|
'agent_type': agent_type,
|
|
665
|
+
'original_agent_type': raw_agent_type, # Keep original for debugging
|
|
434
666
|
'prompt': tool_input.get('prompt', ''),
|
|
435
667
|
'description': tool_input.get('description', ''),
|
|
436
668
|
'task_preview': (tool_input.get('prompt', '') or tool_input.get('description', ''))[:100]
|
|
437
669
|
}
|
|
438
670
|
|
|
439
|
-
# Track this delegation for SubagentStop correlation
|
|
671
|
+
# Track this delegation for SubagentStop correlation and response tracking
|
|
440
672
|
session_id = event.get('session_id', '')
|
|
441
673
|
if session_id and agent_type != 'unknown':
|
|
442
|
-
|
|
674
|
+
# Prepare request data for response tracking correlation
|
|
675
|
+
request_data = {
|
|
676
|
+
'prompt': tool_input.get('prompt', ''),
|
|
677
|
+
'description': tool_input.get('description', ''),
|
|
678
|
+
'agent_type': agent_type
|
|
679
|
+
}
|
|
680
|
+
self._track_delegation(session_id, agent_type, request_data)
|
|
681
|
+
|
|
682
|
+
# Log important delegations for debugging
|
|
683
|
+
if DEBUG or agent_type in ['research', 'engineer', 'qa', 'documentation']:
|
|
684
|
+
print(f"Hook handler: Task delegation started - agent: '{agent_type}', session: '{session_id}'", file=sys.stderr)
|
|
443
685
|
|
|
444
686
|
# Trigger memory pre-delegation hook
|
|
445
687
|
self._trigger_memory_pre_delegation_hook(agent_type, tool_input, session_id)
|
|
688
|
+
|
|
689
|
+
# Emit a subagent_start event for better tracking
|
|
690
|
+
subagent_start_data = {
|
|
691
|
+
'agent_type': agent_type,
|
|
692
|
+
'agent_id': f"{agent_type}_{session_id}",
|
|
693
|
+
'session_id': session_id,
|
|
694
|
+
'prompt': tool_input.get('prompt', ''),
|
|
695
|
+
'description': tool_input.get('description', ''),
|
|
696
|
+
'timestamp': datetime.now().isoformat(),
|
|
697
|
+
'hook_event_name': 'SubagentStart' # For dashboard compatibility
|
|
698
|
+
}
|
|
699
|
+
self._emit_socketio_event('/hook', 'subagent_start', subagent_start_data)
|
|
446
700
|
|
|
447
701
|
self._emit_socketio_event('/hook', 'pre_tool', pre_tool_data)
|
|
448
702
|
|
|
@@ -468,7 +722,6 @@ class ClaudeHookHandler:
|
|
|
468
722
|
git_branch = self._get_git_branch(working_dir) if working_dir else 'Unknown'
|
|
469
723
|
|
|
470
724
|
post_tool_data = {
|
|
471
|
-
'event_type': 'post_tool',
|
|
472
725
|
'tool_name': tool_name,
|
|
473
726
|
'exit_code': exit_code,
|
|
474
727
|
'success': exit_code == 0,
|
|
@@ -484,11 +737,16 @@ class ClaudeHookHandler:
|
|
|
484
737
|
'output_size': len(str(result_data.get('output', ''))) if result_data.get('output') else 0
|
|
485
738
|
}
|
|
486
739
|
|
|
487
|
-
# Handle Task delegation completion for memory hooks
|
|
740
|
+
# Handle Task delegation completion for memory hooks and response tracking
|
|
488
741
|
if tool_name == 'Task':
|
|
489
742
|
session_id = event.get('session_id', '')
|
|
490
743
|
agent_type = self._get_delegation_agent_type(session_id)
|
|
744
|
+
|
|
745
|
+
# Trigger memory post-delegation hook
|
|
491
746
|
self._trigger_memory_post_delegation_hook(agent_type, event, session_id)
|
|
747
|
+
|
|
748
|
+
# Track agent response if response tracking is enabled
|
|
749
|
+
self._track_agent_response(session_id, agent_type, event)
|
|
492
750
|
|
|
493
751
|
self._emit_socketio_event('/hook', 'post_tool', post_tool_data)
|
|
494
752
|
|
|
@@ -687,7 +945,6 @@ class ClaudeHookHandler:
|
|
|
687
945
|
git_branch = self._get_git_branch(working_dir) if working_dir else 'Unknown'
|
|
688
946
|
|
|
689
947
|
notification_data = {
|
|
690
|
-
'event_type': 'notification',
|
|
691
948
|
'notification_type': notification_type,
|
|
692
949
|
'message': message,
|
|
693
950
|
'message_preview': message[:200] if len(message) > 200 else message,
|
|
@@ -721,7 +978,6 @@ class ClaudeHookHandler:
|
|
|
721
978
|
git_branch = self._get_git_branch(working_dir) if working_dir else 'Unknown'
|
|
722
979
|
|
|
723
980
|
stop_data = {
|
|
724
|
-
'event_type': 'stop',
|
|
725
981
|
'reason': reason,
|
|
726
982
|
'stop_type': stop_type,
|
|
727
983
|
'session_id': event.get('session_id', ''),
|
|
@@ -738,7 +994,7 @@ class ClaudeHookHandler:
|
|
|
738
994
|
self._emit_socketio_event('/hook', 'stop', stop_data)
|
|
739
995
|
|
|
740
996
|
def _handle_subagent_stop_fast(self, event):
|
|
741
|
-
"""Handle subagent stop events
|
|
997
|
+
"""Handle subagent stop events with improved agent type detection.
|
|
742
998
|
|
|
743
999
|
WHY comprehensive subagent stop capture:
|
|
744
1000
|
- Provides visibility into subagent lifecycle and delegation patterns
|
|
@@ -767,31 +1023,35 @@ class ClaudeHookHandler:
|
|
|
767
1023
|
elif 'pm' in task_desc or 'project' in task_desc:
|
|
768
1024
|
agent_type = 'pm'
|
|
769
1025
|
|
|
1026
|
+
# Always log SubagentStop events for debugging
|
|
1027
|
+
if DEBUG or agent_type != 'unknown':
|
|
1028
|
+
print(f"Hook handler: Processing SubagentStop - agent: '{agent_type}', session: '{session_id}', reason: '{reason}'", file=sys.stderr)
|
|
1029
|
+
|
|
770
1030
|
# Get working directory and git branch
|
|
771
1031
|
working_dir = event.get('cwd', '')
|
|
772
1032
|
git_branch = self._get_git_branch(working_dir) if working_dir else 'Unknown'
|
|
773
1033
|
|
|
774
1034
|
subagent_stop_data = {
|
|
775
|
-
'event_type': 'subagent_stop',
|
|
776
1035
|
'agent_type': agent_type,
|
|
777
1036
|
'agent_id': agent_id,
|
|
778
1037
|
'reason': reason,
|
|
779
|
-
'session_id':
|
|
1038
|
+
'session_id': session_id,
|
|
780
1039
|
'working_directory': working_dir,
|
|
781
1040
|
'git_branch': git_branch,
|
|
782
1041
|
'timestamp': datetime.now().isoformat(),
|
|
783
1042
|
'is_successful_completion': reason in ['completed', 'finished', 'done'],
|
|
784
1043
|
'is_error_termination': reason in ['error', 'timeout', 'failed', 'blocked'],
|
|
785
|
-
'is_delegation_related': agent_type in ['research', 'engineer', 'pm', 'ops'],
|
|
1044
|
+
'is_delegation_related': agent_type in ['research', 'engineer', 'pm', 'ops', 'qa', 'documentation', 'security'],
|
|
786
1045
|
'has_results': bool(event.get('results') or event.get('output')),
|
|
787
|
-
'duration_context': event.get('duration_ms')
|
|
1046
|
+
'duration_context': event.get('duration_ms'),
|
|
1047
|
+
'hook_event_name': 'SubagentStop' # Explicitly set for dashboard
|
|
788
1048
|
}
|
|
789
1049
|
|
|
790
|
-
# Debug log the
|
|
1050
|
+
# Debug log the processed data
|
|
791
1051
|
if DEBUG:
|
|
792
|
-
print(f"SubagentStop
|
|
1052
|
+
print(f"SubagentStop processed data: agent_type='{agent_type}', session_id='{session_id}'", file=sys.stderr)
|
|
793
1053
|
|
|
794
|
-
# Emit to /hook namespace
|
|
1054
|
+
# Emit to /hook namespace with high priority
|
|
795
1055
|
self._emit_socketio_event('/hook', 'subagent_stop', subagent_stop_data)
|
|
796
1056
|
|
|
797
1057
|
def _trigger_memory_pre_delegation_hook(self, agent_type: str, tool_input: dict, session_id: str):
|
|
@@ -22,7 +22,7 @@ logger = get_logger(__name__)
|
|
|
22
22
|
|
|
23
23
|
# Try to import memory manager with fallback handling
|
|
24
24
|
try:
|
|
25
|
-
from claude_mpm.services.
|
|
25
|
+
from claude_mpm.services.agents.memory import AgentMemoryManager
|
|
26
26
|
MEMORY_MANAGER_AVAILABLE = True
|
|
27
27
|
except ImportError as e:
|
|
28
28
|
logger.warning(f"AgentMemoryManager not available: {e}")
|