claude-mpm 4.0.19__py3-none-any.whl → 4.0.22__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/BUILD_NUMBER +1 -1
- claude_mpm/VERSION +1 -1
- claude_mpm/__main__.py +4 -0
- claude_mpm/agents/BASE_AGENT_TEMPLATE.md +38 -2
- claude_mpm/agents/INSTRUCTIONS.md +74 -0
- claude_mpm/agents/OUTPUT_STYLE.md +84 -0
- claude_mpm/agents/WORKFLOW.md +308 -4
- claude_mpm/agents/agents_metadata.py +52 -0
- claude_mpm/agents/base_agent_loader.py +75 -19
- claude_mpm/agents/templates/__init__.py +4 -0
- claude_mpm/agents/templates/api_qa.json +206 -0
- claude_mpm/agents/templates/qa.json +1 -1
- claude_mpm/agents/templates/research.json +24 -16
- claude_mpm/agents/templates/ticketing.json +18 -5
- claude_mpm/agents/templates/vercel_ops_agent.json +281 -0
- claude_mpm/agents/templates/vercel_ops_instructions.md +582 -0
- claude_mpm/cli/__init__.py +23 -1
- claude_mpm/cli/__main__.py +4 -0
- claude_mpm/cli/commands/mcp_command_router.py +87 -1
- claude_mpm/cli/commands/mcp_install_commands.py +207 -26
- claude_mpm/cli/commands/memory.py +32 -5
- claude_mpm/cli/commands/run.py +33 -6
- claude_mpm/cli/parsers/base_parser.py +5 -0
- claude_mpm/cli/parsers/mcp_parser.py +23 -0
- claude_mpm/cli/parsers/run_parser.py +5 -0
- claude_mpm/cli/utils.py +17 -4
- claude_mpm/constants.py +1 -0
- claude_mpm/core/base_service.py +8 -2
- claude_mpm/core/config.py +122 -32
- claude_mpm/core/framework_loader.py +385 -34
- claude_mpm/core/interactive_session.py +77 -12
- claude_mpm/core/oneshot_session.py +7 -1
- claude_mpm/core/output_style_manager.py +468 -0
- claude_mpm/core/unified_paths.py +190 -21
- claude_mpm/hooks/claude_hooks/hook_handler.py +91 -16
- claude_mpm/hooks/claude_hooks/hook_wrapper.sh +3 -0
- claude_mpm/init.py +1 -0
- claude_mpm/scripts/socketio_daemon.py +67 -7
- claude_mpm/scripts/socketio_daemon_hardened.py +897 -0
- claude_mpm/services/agents/deployment/agent_deployment.py +216 -10
- claude_mpm/services/agents/deployment/agent_template_builder.py +37 -1
- claude_mpm/services/agents/deployment/async_agent_deployment.py +65 -1
- claude_mpm/services/agents/deployment/multi_source_deployment_service.py +441 -0
- claude_mpm/services/agents/memory/__init__.py +0 -2
- claude_mpm/services/agents/memory/agent_memory_manager.py +577 -44
- claude_mpm/services/agents/memory/content_manager.py +144 -14
- claude_mpm/services/agents/memory/template_generator.py +7 -354
- claude_mpm/services/mcp_gateway/server/stdio_server.py +61 -169
- claude_mpm/services/memory_hook_service.py +62 -4
- claude_mpm/services/runner_configuration_service.py +5 -9
- claude_mpm/services/socketio/server/broadcaster.py +32 -1
- claude_mpm/services/socketio/server/core.py +4 -0
- claude_mpm/services/socketio/server/main.py +23 -4
- claude_mpm/services/subprocess_launcher_service.py +5 -0
- {claude_mpm-4.0.19.dist-info → claude_mpm-4.0.22.dist-info}/METADATA +1 -1
- {claude_mpm-4.0.19.dist-info → claude_mpm-4.0.22.dist-info}/RECORD +60 -54
- claude_mpm/services/agents/memory/analyzer.py +0 -430
- {claude_mpm-4.0.19.dist-info → claude_mpm-4.0.22.dist-info}/WHEEL +0 -0
- {claude_mpm-4.0.19.dist-info → claude_mpm-4.0.22.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.0.19.dist-info → claude_mpm-4.0.22.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.0.19.dist-info → claude_mpm-4.0.22.dist-info}/top_level.txt +0 -0
|
@@ -16,7 +16,7 @@ import asyncio
|
|
|
16
16
|
import json
|
|
17
17
|
import logging
|
|
18
18
|
import sys
|
|
19
|
-
from typing import Any, Dict,
|
|
19
|
+
from typing import Any, Dict, Optional
|
|
20
20
|
|
|
21
21
|
# Import MCP SDK components
|
|
22
22
|
from mcp.server import NotificationOptions, Server
|
|
@@ -190,7 +190,7 @@ class SimpleMCPServer:
|
|
|
190
190
|
# Default to brief
|
|
191
191
|
return self._create_brief_summary(sentences, max_length)
|
|
192
192
|
|
|
193
|
-
def _create_brief_summary(self, sentences:
|
|
193
|
+
def _create_brief_summary(self, sentences: list[str], max_length: int) -> str:
|
|
194
194
|
"""Create a brief summary by selecting most important sentences."""
|
|
195
195
|
if not sentences:
|
|
196
196
|
return ""
|
|
@@ -272,7 +272,7 @@ class SimpleMCPServer:
|
|
|
272
272
|
return " ".join(s[1] for s in selected)
|
|
273
273
|
|
|
274
274
|
def _create_detailed_summary(
|
|
275
|
-
self, sentences:
|
|
275
|
+
self, sentences: list[str], content: str, max_length: int
|
|
276
276
|
) -> str:
|
|
277
277
|
"""Create a detailed summary preserving document structure."""
|
|
278
278
|
import re
|
|
@@ -303,7 +303,7 @@ class SimpleMCPServer:
|
|
|
303
303
|
return " ".join(words) + ("..." if len(result.split()) > max_length else "")
|
|
304
304
|
|
|
305
305
|
def _create_bullet_summary(
|
|
306
|
-
self, sentences:
|
|
306
|
+
self, sentences: list[str], content: str, max_length: int
|
|
307
307
|
) -> str:
|
|
308
308
|
"""Extract key points as a bullet list."""
|
|
309
309
|
import re
|
|
@@ -347,7 +347,7 @@ class SimpleMCPServer:
|
|
|
347
347
|
return "\n".join(result_lines)
|
|
348
348
|
|
|
349
349
|
def _create_executive_summary(
|
|
350
|
-
self, sentences:
|
|
350
|
+
self, sentences: list[str], content: str, max_length: int
|
|
351
351
|
) -> str:
|
|
352
352
|
"""Create an executive summary with overview, findings, and recommendations."""
|
|
353
353
|
# Allocate words across sections
|
|
@@ -436,32 +436,9 @@ class SimpleMCPServer:
|
|
|
436
436
|
# NOTE: Defer initialization to avoid event loop issues
|
|
437
437
|
self.unified_ticket_tool = None
|
|
438
438
|
self._ticket_tool_initialized = False
|
|
439
|
-
|
|
440
|
-
async def _initialize_ticket_tool(self):
|
|
441
|
-
"""
|
|
442
|
-
Initialize the unified ticket tool asynchronously.
|
|
443
|
-
|
|
444
|
-
This is called lazily when the tool is first needed,
|
|
445
|
-
ensuring an event loop is available.
|
|
446
|
-
"""
|
|
447
|
-
if self._ticket_tool_initialized or not TICKET_TOOLS_AVAILABLE:
|
|
448
|
-
return
|
|
449
439
|
|
|
450
|
-
try:
|
|
451
|
-
self.logger.info("Initializing unified ticket tool...")
|
|
452
|
-
self.unified_ticket_tool = UnifiedTicketTool()
|
|
453
|
-
# If the tool has an async init method, call it
|
|
454
|
-
if hasattr(self.unified_ticket_tool, 'initialize'):
|
|
455
|
-
await self.unified_ticket_tool.initialize()
|
|
456
|
-
self._ticket_tool_initialized = True
|
|
457
|
-
self.logger.info("Unified ticket tool initialized successfully")
|
|
458
|
-
except Exception as e:
|
|
459
|
-
self.logger.warning(f"Failed to initialize unified ticket tool: {e}")
|
|
460
|
-
self.unified_ticket_tool = None
|
|
461
|
-
self._ticket_tool_initialized = True # Mark as attempted
|
|
462
|
-
|
|
463
440
|
@self.server.list_tools()
|
|
464
|
-
async def handle_list_tools() ->
|
|
441
|
+
async def handle_list_tools() -> list[Tool]:
|
|
465
442
|
"""List available tools."""
|
|
466
443
|
# Initialize ticket tool lazily if needed
|
|
467
444
|
if not self._ticket_tool_initialized and TICKET_TOOLS_AVAILABLE:
|
|
@@ -469,69 +446,22 @@ class SimpleMCPServer:
|
|
|
469
446
|
|
|
470
447
|
tools = [
|
|
471
448
|
Tool(
|
|
472
|
-
name="
|
|
473
|
-
description="
|
|
474
|
-
inputSchema={
|
|
475
|
-
"type": "object",
|
|
476
|
-
"properties": {
|
|
477
|
-
"message": {
|
|
478
|
-
"type": "string",
|
|
479
|
-
"description": "Message to echo",
|
|
480
|
-
}
|
|
481
|
-
},
|
|
482
|
-
"required": ["message"],
|
|
483
|
-
},
|
|
484
|
-
),
|
|
485
|
-
Tool(
|
|
486
|
-
name="calculator",
|
|
487
|
-
description="Perform basic arithmetic calculations",
|
|
488
|
-
inputSchema={
|
|
489
|
-
"type": "object",
|
|
490
|
-
"properties": {
|
|
491
|
-
"expression": {
|
|
492
|
-
"type": "string",
|
|
493
|
-
"description": "Mathematical expression to evaluate",
|
|
494
|
-
}
|
|
495
|
-
},
|
|
496
|
-
"required": ["expression"],
|
|
497
|
-
},
|
|
498
|
-
),
|
|
499
|
-
Tool(
|
|
500
|
-
name="system_info",
|
|
501
|
-
description="Get system information",
|
|
449
|
+
name="status",
|
|
450
|
+
description="Get system and service status information",
|
|
502
451
|
inputSchema={
|
|
503
452
|
"type": "object",
|
|
504
453
|
"properties": {
|
|
505
454
|
"info_type": {
|
|
506
455
|
"type": "string",
|
|
507
|
-
"enum": ["platform", "python_version", "cwd"],
|
|
508
|
-
"description": "Type of
|
|
456
|
+
"enum": ["platform", "python_version", "cwd", "all"],
|
|
457
|
+
"description": "Type of status information to retrieve (default: all)",
|
|
458
|
+
"default": "all",
|
|
509
459
|
}
|
|
510
460
|
},
|
|
511
|
-
"required": ["info_type"],
|
|
512
|
-
},
|
|
513
|
-
),
|
|
514
|
-
Tool(
|
|
515
|
-
name="run_command",
|
|
516
|
-
description="Execute a shell command",
|
|
517
|
-
inputSchema={
|
|
518
|
-
"type": "object",
|
|
519
|
-
"properties": {
|
|
520
|
-
"command": {
|
|
521
|
-
"type": "string",
|
|
522
|
-
"description": "Shell command to execute",
|
|
523
|
-
},
|
|
524
|
-
"timeout": {
|
|
525
|
-
"type": "number",
|
|
526
|
-
"description": "Command timeout in seconds",
|
|
527
|
-
"default": 30,
|
|
528
|
-
},
|
|
529
|
-
},
|
|
530
|
-
"required": ["command"],
|
|
531
461
|
},
|
|
532
462
|
),
|
|
533
463
|
Tool(
|
|
534
|
-
name="
|
|
464
|
+
name="document_summarizer",
|
|
535
465
|
description="Summarize documents or text content",
|
|
536
466
|
inputSchema={
|
|
537
467
|
"type": "object",
|
|
@@ -576,60 +506,17 @@ class SimpleMCPServer:
|
|
|
576
506
|
self.logger.info(f"Listing {len(tools)} available tools")
|
|
577
507
|
return tools
|
|
578
508
|
|
|
509
|
+
|
|
579
510
|
@self.server.call_tool()
|
|
580
511
|
async def handle_call_tool(
|
|
581
512
|
name: str, arguments: Dict[str, Any]
|
|
582
|
-
) ->
|
|
513
|
+
) -> list[TextContent]:
|
|
583
514
|
"""Handle tool invocation."""
|
|
584
515
|
self.logger.info(f"Invoking tool: {name} with arguments: {arguments}")
|
|
585
516
|
|
|
586
517
|
try:
|
|
587
|
-
if name == "
|
|
588
|
-
|
|
589
|
-
result = f"Echo: {message}"
|
|
590
|
-
|
|
591
|
-
elif name == "calculator":
|
|
592
|
-
expression = arguments.get("expression", "")
|
|
593
|
-
try:
|
|
594
|
-
# Safe evaluation of mathematical expressions
|
|
595
|
-
import ast
|
|
596
|
-
import operator as op
|
|
597
|
-
|
|
598
|
-
# Supported operators
|
|
599
|
-
ops = {
|
|
600
|
-
ast.Add: op.add,
|
|
601
|
-
ast.Sub: op.sub,
|
|
602
|
-
ast.Mult: op.mul,
|
|
603
|
-
ast.Div: op.truediv,
|
|
604
|
-
ast.Pow: op.pow,
|
|
605
|
-
ast.Mod: op.mod,
|
|
606
|
-
ast.USub: op.neg,
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
def eval_expr(expr):
|
|
610
|
-
"""Safely evaluate mathematical expression."""
|
|
611
|
-
|
|
612
|
-
def _eval(node):
|
|
613
|
-
if isinstance(node, ast.Constant):
|
|
614
|
-
return node.value
|
|
615
|
-
elif isinstance(node, ast.BinOp):
|
|
616
|
-
return ops[type(node.op)](
|
|
617
|
-
_eval(node.left), _eval(node.right)
|
|
618
|
-
)
|
|
619
|
-
elif isinstance(node, ast.UnaryOp):
|
|
620
|
-
return ops[type(node.op)](_eval(node.operand))
|
|
621
|
-
else:
|
|
622
|
-
raise TypeError(f"Unsupported operation: {node}")
|
|
623
|
-
|
|
624
|
-
return _eval(ast.parse(expr, mode="eval").body)
|
|
625
|
-
|
|
626
|
-
result_value = eval_expr(expression)
|
|
627
|
-
result = f"{expression} = {result_value}"
|
|
628
|
-
except Exception as e:
|
|
629
|
-
result = f"Error evaluating expression: {str(e)}"
|
|
630
|
-
|
|
631
|
-
elif name == "system_info":
|
|
632
|
-
info_type = arguments.get("info_type", "platform")
|
|
518
|
+
if name == "status":
|
|
519
|
+
info_type = arguments.get("info_type", "all")
|
|
633
520
|
|
|
634
521
|
if info_type == "platform":
|
|
635
522
|
import platform
|
|
@@ -643,49 +530,25 @@ class SimpleMCPServer:
|
|
|
643
530
|
import os
|
|
644
531
|
|
|
645
532
|
result = f"Working Directory: {os.getcwd()}"
|
|
533
|
+
elif info_type == "all":
|
|
534
|
+
import platform
|
|
535
|
+
import sys
|
|
536
|
+
import os
|
|
537
|
+
import datetime
|
|
538
|
+
|
|
539
|
+
result = (
|
|
540
|
+
f"=== System Status ===\n"
|
|
541
|
+
f"Platform: {platform.system()} {platform.release()}\n"
|
|
542
|
+
f"Python: {sys.version.split()[0]}\n"
|
|
543
|
+
f"Working Directory: {os.getcwd()}\n"
|
|
544
|
+
f"Server: {self.name} v{self.version}\n"
|
|
545
|
+
f"Timestamp: {datetime.datetime.now().isoformat()}\n"
|
|
546
|
+
f"Tools Available: status, document_summarizer{', ticket' if self.unified_ticket_tool else ''}"
|
|
547
|
+
)
|
|
646
548
|
else:
|
|
647
549
|
result = f"Unknown info type: {info_type}"
|
|
648
550
|
|
|
649
|
-
elif name == "
|
|
650
|
-
command = arguments.get("command", "")
|
|
651
|
-
timeout = arguments.get("timeout", 30)
|
|
652
|
-
|
|
653
|
-
import shlex
|
|
654
|
-
import subprocess
|
|
655
|
-
|
|
656
|
-
try:
|
|
657
|
-
# Split command string into a list to avoid shell injection
|
|
658
|
-
command_parts = shlex.split(command)
|
|
659
|
-
|
|
660
|
-
# Use create_subprocess_exec instead of create_subprocess_shell
|
|
661
|
-
# to prevent command injection vulnerabilities
|
|
662
|
-
proc = await asyncio.create_subprocess_exec(
|
|
663
|
-
*command_parts,
|
|
664
|
-
stdout=subprocess.PIPE,
|
|
665
|
-
stderr=subprocess.PIPE,
|
|
666
|
-
)
|
|
667
|
-
|
|
668
|
-
stdout, stderr = await asyncio.wait_for(
|
|
669
|
-
proc.communicate(), timeout=timeout
|
|
670
|
-
)
|
|
671
|
-
|
|
672
|
-
if proc.returncode == 0:
|
|
673
|
-
result = (
|
|
674
|
-
stdout.decode()
|
|
675
|
-
if stdout
|
|
676
|
-
else "Command completed successfully"
|
|
677
|
-
)
|
|
678
|
-
else:
|
|
679
|
-
result = f"Command failed with code {proc.returncode}: {stderr.decode()}"
|
|
680
|
-
except asyncio.TimeoutError:
|
|
681
|
-
result = f"Command timed out after {timeout} seconds"
|
|
682
|
-
except ValueError as e:
|
|
683
|
-
# Handle shlex parsing errors (e.g., unmatched quotes)
|
|
684
|
-
result = f"Invalid command syntax: {str(e)}"
|
|
685
|
-
except Exception as e:
|
|
686
|
-
result = f"Error running command: {str(e)}"
|
|
687
|
-
|
|
688
|
-
elif name == "summarize_document":
|
|
551
|
+
elif name == "document_summarizer":
|
|
689
552
|
content = arguments.get("content", "")
|
|
690
553
|
style = arguments.get("style", "brief")
|
|
691
554
|
max_length = arguments.get("max_length", 150)
|
|
@@ -734,6 +597,29 @@ class SimpleMCPServer:
|
|
|
734
597
|
return [TextContent(type="text", text=error_msg)]
|
|
735
598
|
|
|
736
599
|
|
|
600
|
+
async def _initialize_ticket_tool(self):
|
|
601
|
+
"""
|
|
602
|
+
Initialize the unified ticket tool asynchronously.
|
|
603
|
+
|
|
604
|
+
This is called lazily when the tool is first needed,
|
|
605
|
+
ensuring an event loop is available.
|
|
606
|
+
"""
|
|
607
|
+
if self._ticket_tool_initialized or not TICKET_TOOLS_AVAILABLE:
|
|
608
|
+
return
|
|
609
|
+
|
|
610
|
+
try:
|
|
611
|
+
self.logger.info("Initializing unified ticket tool...")
|
|
612
|
+
self.unified_ticket_tool = UnifiedTicketTool()
|
|
613
|
+
# If the tool has an async init method, call it
|
|
614
|
+
if hasattr(self.unified_ticket_tool, 'initialize'):
|
|
615
|
+
await self.unified_ticket_tool.initialize()
|
|
616
|
+
self._ticket_tool_initialized = True
|
|
617
|
+
self.logger.info("Unified ticket tool initialized successfully")
|
|
618
|
+
except Exception as e:
|
|
619
|
+
self.logger.warning(f"Failed to initialize unified ticket tool: {e}")
|
|
620
|
+
self.unified_ticket_tool = None
|
|
621
|
+
self._ticket_tool_initialized = True # Mark as attempted
|
|
622
|
+
|
|
737
623
|
async def run(self):
|
|
738
624
|
"""
|
|
739
625
|
Run the MCP server using stdio communication with backward compatibility.
|
|
@@ -799,9 +685,15 @@ async def main():
|
|
|
799
685
|
|
|
800
686
|
def main_sync():
|
|
801
687
|
"""Synchronous entry point for use as a console script."""
|
|
688
|
+
import os
|
|
689
|
+
# Disable telemetry by default
|
|
690
|
+
os.environ.setdefault('DISABLE_TELEMETRY', '1')
|
|
802
691
|
asyncio.run(main())
|
|
803
692
|
|
|
804
693
|
|
|
805
694
|
if __name__ == "__main__":
|
|
695
|
+
import os
|
|
696
|
+
# Disable telemetry by default
|
|
697
|
+
os.environ.setdefault('DISABLE_TELEMETRY', '1')
|
|
806
698
|
# Run the async main function
|
|
807
699
|
main_sync()
|
|
@@ -143,11 +143,69 @@ class MemoryHookService(BaseService, MemoryHookInterface):
|
|
|
143
143
|
HookResult with success status and any modifications
|
|
144
144
|
"""
|
|
145
145
|
try:
|
|
146
|
-
# This would integrate with a memory service to save new memories
|
|
147
|
-
# For now, this is a placeholder for future memory integration
|
|
148
|
-
self.logger.debug("Saving new memories from interaction")
|
|
149
|
-
|
|
150
146
|
from claude_mpm.hooks.base_hook import HookResult
|
|
147
|
+
|
|
148
|
+
# Extract agent_id and response from context
|
|
149
|
+
agent_id = None
|
|
150
|
+
response_text = None
|
|
151
|
+
|
|
152
|
+
# Try to get agent_id from various possible locations in context
|
|
153
|
+
if hasattr(context, 'data') and context.data:
|
|
154
|
+
data = context.data
|
|
155
|
+
|
|
156
|
+
# Check for agent_id in various locations
|
|
157
|
+
if isinstance(data, dict):
|
|
158
|
+
# Try direct agent_id field
|
|
159
|
+
agent_id = data.get('agent_id')
|
|
160
|
+
|
|
161
|
+
# Try agent_type field
|
|
162
|
+
if not agent_id:
|
|
163
|
+
agent_id = data.get('agent_type')
|
|
164
|
+
|
|
165
|
+
# Try subagent_type (for Task delegations)
|
|
166
|
+
if not agent_id:
|
|
167
|
+
agent_id = data.get('subagent_type')
|
|
168
|
+
|
|
169
|
+
# Try tool_parameters for Task delegations
|
|
170
|
+
if not agent_id and 'tool_parameters' in data:
|
|
171
|
+
params = data.get('tool_parameters', {})
|
|
172
|
+
if isinstance(params, dict):
|
|
173
|
+
agent_id = params.get('subagent_type')
|
|
174
|
+
|
|
175
|
+
# Extract response text
|
|
176
|
+
response_text = data.get('response') or data.get('result') or data.get('output')
|
|
177
|
+
|
|
178
|
+
# If response_text is a dict, try to get text from it
|
|
179
|
+
if isinstance(response_text, dict):
|
|
180
|
+
response_text = response_text.get('text') or response_text.get('content') or str(response_text)
|
|
181
|
+
|
|
182
|
+
# Default to PM if no agent_id found
|
|
183
|
+
if not agent_id:
|
|
184
|
+
agent_id = "PM"
|
|
185
|
+
self.logger.debug("No agent_id found in context, defaulting to PM")
|
|
186
|
+
|
|
187
|
+
# Only process if we have response text
|
|
188
|
+
if response_text and isinstance(response_text, str):
|
|
189
|
+
self.logger.debug(f"Processing memory extraction for agent: {agent_id}")
|
|
190
|
+
|
|
191
|
+
# Import and use the memory manager
|
|
192
|
+
from claude_mpm.services.agents.memory.agent_memory_manager import get_memory_manager
|
|
193
|
+
|
|
194
|
+
try:
|
|
195
|
+
memory_manager = get_memory_manager()
|
|
196
|
+
|
|
197
|
+
# Extract and update memory
|
|
198
|
+
success = memory_manager.extract_and_update_memory(agent_id, response_text)
|
|
199
|
+
|
|
200
|
+
if success:
|
|
201
|
+
self.logger.info(f"Successfully extracted and saved memories for {agent_id}")
|
|
202
|
+
else:
|
|
203
|
+
self.logger.debug(f"No memories found to extract for {agent_id}")
|
|
204
|
+
|
|
205
|
+
except Exception as mem_error:
|
|
206
|
+
self.logger.warning(f"Failed to extract/save memories for {agent_id}: {mem_error}")
|
|
207
|
+
else:
|
|
208
|
+
self.logger.debug("No response text found in context for memory extraction")
|
|
151
209
|
|
|
152
210
|
return HookResult(success=True, data=context.data, modified=False)
|
|
153
211
|
|
|
@@ -72,11 +72,12 @@ class RunnerConfigurationService(BaseService, RunnerConfigurationInterface):
|
|
|
72
72
|
Loaded configuration dictionary
|
|
73
73
|
"""
|
|
74
74
|
try:
|
|
75
|
+
# Use singleton Config instance to prevent duplicate loading
|
|
75
76
|
if config_path:
|
|
76
|
-
#
|
|
77
|
-
config = Config(config_path)
|
|
77
|
+
# Only pass config_path if it's different from what might already be loaded
|
|
78
|
+
config = Config({}, config_path)
|
|
78
79
|
else:
|
|
79
|
-
#
|
|
80
|
+
# Use existing singleton instance
|
|
80
81
|
config = Config()
|
|
81
82
|
|
|
82
83
|
return {
|
|
@@ -162,14 +163,9 @@ class RunnerConfigurationService(BaseService, RunnerConfigurationInterface):
|
|
|
162
163
|
"websocket_port": kwargs.get("websocket_port", 8765),
|
|
163
164
|
}
|
|
164
165
|
|
|
165
|
-
# Initialize main configuration
|
|
166
|
+
# Initialize main configuration (singleton will prevent duplicate loading)
|
|
166
167
|
try:
|
|
167
168
|
config = Config()
|
|
168
|
-
except FileNotFoundError as e:
|
|
169
|
-
self.logger.warning(
|
|
170
|
-
"Configuration file not found, using defaults", extra={"error": str(e)}
|
|
171
|
-
)
|
|
172
|
-
config = Config() # Will use defaults
|
|
173
169
|
except Exception as e:
|
|
174
170
|
self.logger.error("Failed to load configuration", exc_info=True)
|
|
175
171
|
raise RuntimeError(f"Configuration initialization failed: {e}") from e
|
|
@@ -184,10 +184,41 @@ class SocketIOEventBroadcaster:
|
|
|
184
184
|
|
|
185
185
|
WHY: Failed broadcasts need to be retried automatically
|
|
186
186
|
to ensure reliable event delivery.
|
|
187
|
+
|
|
188
|
+
IMPORTANT: This method must handle being called from a different thread
|
|
189
|
+
than the one running the event loop.
|
|
187
190
|
"""
|
|
188
191
|
if self.loop and not self.retry_task:
|
|
192
|
+
try:
|
|
193
|
+
# Check if the loop is running in the current thread
|
|
194
|
+
try:
|
|
195
|
+
running_loop = asyncio.get_running_loop()
|
|
196
|
+
if running_loop == self.loop:
|
|
197
|
+
# Same thread, can use create_task directly
|
|
198
|
+
self.retry_task = asyncio.create_task(self._process_retry_queue())
|
|
199
|
+
self.logger.info("🔄 Started retry queue processor (same thread)")
|
|
200
|
+
else:
|
|
201
|
+
# Different thread, need to schedule in the target loop
|
|
202
|
+
self._start_retry_in_loop()
|
|
203
|
+
except RuntimeError:
|
|
204
|
+
# No running loop in current thread, schedule in target loop
|
|
205
|
+
self._start_retry_in_loop()
|
|
206
|
+
except Exception as e:
|
|
207
|
+
self.logger.error(f"Failed to start retry processor: {e}")
|
|
208
|
+
|
|
209
|
+
def _start_retry_in_loop(self):
|
|
210
|
+
"""Helper to start retry processor from a different thread."""
|
|
211
|
+
async def _create_retry_task():
|
|
189
212
|
self.retry_task = asyncio.create_task(self._process_retry_queue())
|
|
190
|
-
self.logger.info("🔄 Started retry queue processor")
|
|
213
|
+
self.logger.info("🔄 Started retry queue processor (cross-thread)")
|
|
214
|
+
|
|
215
|
+
# Schedule the task creation in the target loop
|
|
216
|
+
future = asyncio.run_coroutine_threadsafe(_create_retry_task(), self.loop)
|
|
217
|
+
try:
|
|
218
|
+
# Wait briefly to ensure it's scheduled
|
|
219
|
+
future.result(timeout=1.0)
|
|
220
|
+
except Exception as e:
|
|
221
|
+
self.logger.error(f"Failed to schedule retry processor: {e}")
|
|
191
222
|
|
|
192
223
|
def stop_retry_processor(self):
|
|
193
224
|
"""Stop the background retry processor."""
|
|
@@ -138,8 +138,12 @@ class SocketIOServerCore:
|
|
|
138
138
|
"""Run the server event loop."""
|
|
139
139
|
try:
|
|
140
140
|
# Create new event loop for this thread
|
|
141
|
+
# WHY: We create and assign the loop immediately to minimize the race
|
|
142
|
+
# condition window where other threads might try to access it.
|
|
141
143
|
self.loop = asyncio.new_event_loop()
|
|
142
144
|
asyncio.set_event_loop(self.loop)
|
|
145
|
+
|
|
146
|
+
self.logger.debug("Event loop created and set for background thread")
|
|
143
147
|
|
|
144
148
|
# Run the server
|
|
145
149
|
self.loop.run_until_complete(self._start_server())
|
|
@@ -117,11 +117,30 @@ class SocketIOServer(SocketIOServiceInterface):
|
|
|
117
117
|
server=self, # Pass server reference for event history access
|
|
118
118
|
)
|
|
119
119
|
|
|
120
|
-
#
|
|
121
|
-
|
|
120
|
+
# Wait for the event loop to be initialized in the background thread
|
|
121
|
+
# WHY: The core server starts in a background thread and creates the event
|
|
122
|
+
# loop asynchronously. We must wait for it to be ready before using it.
|
|
123
|
+
max_wait = 5.0 # Maximum wait time in seconds
|
|
124
|
+
wait_interval = 0.1 # Check interval
|
|
125
|
+
waited = 0.0
|
|
122
126
|
|
|
123
|
-
|
|
124
|
-
|
|
127
|
+
while self.core.loop is None and waited < max_wait:
|
|
128
|
+
time.sleep(wait_interval)
|
|
129
|
+
waited += wait_interval
|
|
130
|
+
|
|
131
|
+
if self.core.loop is None:
|
|
132
|
+
self.logger.warning(
|
|
133
|
+
f"Event loop not initialized after {max_wait}s wait. "
|
|
134
|
+
"Retry processor may not function correctly."
|
|
135
|
+
)
|
|
136
|
+
else:
|
|
137
|
+
self.logger.debug(f"Event loop ready after {waited:.1f}s")
|
|
138
|
+
|
|
139
|
+
# Set the loop reference for broadcaster
|
|
140
|
+
self.broadcaster.loop = self.core.loop
|
|
141
|
+
|
|
142
|
+
# Start the retry processor for resilient event delivery
|
|
143
|
+
self.broadcaster.start_retry_processor()
|
|
125
144
|
|
|
126
145
|
# Register events
|
|
127
146
|
self._register_events()
|
|
@@ -313,4 +313,9 @@ class SubprocessLauncherService(BaseService, SubprocessLauncherInterface):
|
|
|
313
313
|
env = os.environ.copy()
|
|
314
314
|
if base_env:
|
|
315
315
|
env.update(base_env)
|
|
316
|
+
|
|
317
|
+
# Disable telemetry for Claude Code subprocesses
|
|
318
|
+
# This ensures Claude Code doesn't send telemetry data during runtime
|
|
319
|
+
env["DISABLE_TELEMETRY"] = "1"
|
|
320
|
+
|
|
316
321
|
return env
|