Severity: π High (subprocess + descriptor leak; defeats timeout)
_call_tool in src/perseus/mcp.py:245β251:
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
future = executor.submit(_call_resolver, spec, args_str, cfg, workspace)
result = future.result(timeout=timeout)
return result
Two problems:
future.result(timeout=β¦) only abandons the future β the worker thread (and its child subprocess from @query) continues running.
- The
with block calls executor.shutdown(wait=True) on exit, which blocks the MCP response until the abandoned subprocess completes β defeating the entire timeout mechanism.
Repro
perseus mcp serve &
# Send MCP tools/call for perseus_query with { "command": "sleep 600" } and tool_timeout_s=5.
# The response will block for ~600s, not 5s.
Suggested fix
executor = concurrent.futures.ThreadPoolExecutor(max_workers=1)
try:
future = executor.submit(_call_resolver, spec, args_str, cfg, workspace)
result = future.result(timeout=timeout)
except concurrent.futures.TimeoutError:
executor.shutdown(wait=False, cancel_futures=True)
return f"Error executing {directive_name}: timed out after {timeout}s"
else:
executor.shutdown(wait=False)
return result
Better long-term: enforce timeout via subprocess.run(timeout=β¦) inside directives/query.py only, and have the MCP wrapper pass through (don't double-wrap). Track spawned PIDs in a process group so the wrapper can os.killpg on timeout.
Acceptance criteria
- Test: invoke
perseus_query via MCP with cmd="sleep 30", tool_timeout_s=2. Response returns within 5s. pgrep -f "sleep 30" returns nothing within 7s.
Severity: π High (subprocess + descriptor leak; defeats timeout)
_call_toolinsrc/perseus/mcp.py:245β251:Two problems:
future.result(timeout=β¦)only abandons the future β the worker thread (and its child subprocess from@query) continues running.withblock callsexecutor.shutdown(wait=True)on exit, which blocks the MCP response until the abandoned subprocess completes β defeating the entire timeout mechanism.Repro
Suggested fix
Better long-term: enforce timeout via
subprocess.run(timeout=β¦)insidedirectives/query.pyonly, and have the MCP wrapper pass through (don't double-wrap). Track spawned PIDs in a process group so the wrapper canos.killpgon timeout.Acceptance criteria
perseus_queryvia MCP withcmd="sleep 30",tool_timeout_s=2. Response returns within 5s.pgrep -f "sleep 30"returns nothing within 7s.