fix(aibox): registry timeout cleanup + reader thread finalization (F2/N6+N4)
N6: both streaming-forward timeout paths (deadline hit, join(5) expiry) previously returned early and skipped the tool.streaming_output / tool.output_queue cleanup, leaving the daemon thread writing through the instance attribute — zombie output could leak into the next session's queue once the attribute got reassigned. Now unified through a timed_out flag with cleanup at the single exit; stream_tool_timeout becomes an instance attribute (3600s default, test-tunable). N4: reader-thread finalization after timeout/grace break. First implementation followed the audit suggestion (close stdout + join), but PoC regression proved close() blocks: CPython stream objects hold an internal lock, so closing while readline is blocked parks until readline returns (PoC-1 regressed 3s -> 15.2s, exactly the Start-Sleep duration). Corrected strategy: when reader already got EOF, close stdout to release the pipe fd; when reader is still blocked, only join(timeout=2) then give up (daemon-bounded, thread ends naturally when the grandchild exits and EOF arrives), keeping the main flow prompt. Audit report updated with this correction. Tests: new tests/test_f2_fixes.py (5 cases), PoC regression 5/5 (PoC-1/4 back to ~5s), full suite 272 passed.
This commit is contained in:
1 parent
6ef5d11d9d
commit
5bcf22f9a7
4 files changed
+221
-8
No files matched your search
+23
-8
@@ -36,6 +36,8 @@ class ToolRegistry:
|
||||
self.logger = logger or Logger("ToolRegistry")
|
||||
self.current_output_queue = None # 用于流式输出的队列
|
||||
self.skill_manager: Optional['SkillManager'] = None # 技能管理器引用
|
||||
# 单工具流式执行总上限(工具自身秒级超时之外的兜底;测试可调小)
|
||||
self.stream_tool_timeout = 3600
|
||||
|
||||
def register(self, tool: Tool, is_builtin: bool = False):
|
||||
"""注册工具,可以标记是否为内置工具"""
|
||||
@@ -100,14 +102,20 @@ class ToolRegistry:
|
||||
# 此前 while thread.is_alive() 在工具内部卡死(如子进程
|
||||
# 等待 stdin)时永不退出;超时后放弃等待,避免本调用
|
||||
# 线程被连带挂起。
|
||||
tool_timeout = 3600 # 单工具总上限 1 小时(工具自身的秒级超时之外的兜底)
|
||||
# N6 修复(审计 2026-08-18):超时路径此前直接 return,
|
||||
# 跳过下方清理——daemon 线程继续持有 tool.output_queue
|
||||
# 实例属性,属性被后续调用覆盖到新会话队列后,僵尸输出
|
||||
# 会混入后续对话流。现在统一在出口清理。
|
||||
tool_timeout = self.stream_tool_timeout
|
||||
deadline = time.time() + tool_timeout
|
||||
final_result = None
|
||||
timed_out = False
|
||||
while thread.is_alive() or not output_queue.empty():
|
||||
if time.time() > deadline:
|
||||
self.logger.warning(
|
||||
f"工具 {name} 流式执行超过 {tool_timeout}s 总上限,放弃等待")
|
||||
return f"❌ 工具执行超时(>{tool_timeout}s,已放弃等待)"
|
||||
timed_out = True
|
||||
break
|
||||
try:
|
||||
msg_type, content = output_queue.get(timeout=0.1)
|
||||
if msg_type == "line":
|
||||
@@ -121,14 +129,21 @@ class ToolRegistry:
|
||||
self.current_output_queue.put(("complete", content))
|
||||
except queue.Empty:
|
||||
continue
|
||||
|
||||
thread.join(timeout=5)
|
||||
if thread.is_alive():
|
||||
return "❌ 工具执行超时"
|
||||
|
||||
result = final_result or ""
|
||||
|
||||
if not timed_out:
|
||||
thread.join(timeout=5)
|
||||
if thread.is_alive():
|
||||
timed_out = True
|
||||
|
||||
# 统一清理(正常与超时路径都必须执行):断开实例属性,
|
||||
# 僵尸 daemon 线程的 send_line 落到 None 上不再外写
|
||||
tool.streaming_output = False
|
||||
tool.output_queue = None
|
||||
|
||||
if timed_out:
|
||||
result = f"❌ 工具执行超时(>{tool_timeout}s,已放弃等待)"
|
||||
else:
|
||||
result = final_result or ""
|
||||
else:
|
||||
# 普通执行
|
||||
result = tool.execute(**kwargs)
|
||||
|
||||
Reference in new issue
Block a user