fix(swarm): unblock run_swarm retries and clean Ctrl+C shutdown (#42)

Two follow-ups to b769580 reported by @codewings:

1. "Blocked duplicate call: run_swarm" — loop.py dedups by tool name only,
   and SwarmTool defaulted to repeatable=False. A single successful
   run_swarm per session blocked every subsequent call (retry with a
   different preset, different market, etc). Mark SwarmTool.repeatable=True.

2. threading._shutdown noise on Ctrl+C — `with ThreadPoolExecutor` calls
   shutdown(wait=True) on exit, which blocks indefinitely while kimi
   workers are mid-stream. Switch to manual executor + try/finally with
   shutdown(wait=False, cancel_futures=True); catch KeyboardInterrupt at
   layer boundary to set cancel_event. CLI now returns immediately instead
   of deadlocking on exit.
This commit is contained in:
warren618 committed 2026-04-19 22:05:43 +08:00
1 parent a1040bbb71
commit fc708d15c5
2 files changed
+15 -4

No files matched your search

+14 -4
View File
@@ -343,10 +343,14 @@ class SwarmRuntime:
def _event_callback(event: SwarmEvent) -> None:
self._emit_event(run.id, event)
with ThreadPoolExecutor(max_workers=self._max_workers) as executor:
futures: dict[Future[WorkerResult], str] = {}
layer_budget = 0 # seconds — max per-task (retries × timeout) across layer
# Manual executor lifecycle (not `with`) so KeyboardInterrupt and
# the layer deadline don't block main on `shutdown(wait=True)` —
# `wait=False + cancel_futures=True` lets pending work drop and
# the CLI return immediately. Running workers finish naturally.
executor = ThreadPoolExecutor(max_workers=self._max_workers)
futures: dict[Future[WorkerResult], str] = {}
layer_budget = 0 # seconds — max per-task (retries × timeout) across layer
try:
for tid in layer_task_ids:
task = task_store.load_task(tid)
agent_spec = agent_map.get(task.agent_id)
@@ -417,6 +421,12 @@ class SwarmRuntime:
status="timeout", summary="",
error=f"Worker exceeded layer deadline of {layer_deadline}s",
)
except KeyboardInterrupt:
cancel_event.set()
logger.warning("Swarm layer interrupted — cancelling pending workers")
raise
finally:
executor.shutdown(wait=False, cancel_futures=True)
return results
+1
View File
@@ -583,6 +583,7 @@ class SwarmTool(BaseTool):
"required": ["prompt"],
}
is_readonly = False
repeatable = True # loop.py dedups by tool name; each prompt is a distinct run (#42)
def execute(self, **kwargs: Any) -> str:
"""Start a swarm run: auto-match preset, extract variables, wait for completion.