@@ -329,20 +329,21 @@ async def stop(self) -> None:
329329 self ._running = False
330330
331331 # Cancel all active executions
332+ tasks_to_wait = []
332333 for _ , task in self ._execution_tasks .items ():
333334 if not task .done ():
334335 task .cancel ()
335- try :
336- await task
337- except asyncio . CancelledError :
338- pass
339- except RuntimeError as e :
340- # Task may be attached to a different event loop (e.g., when TUI
341- # uses a separate loop). Log and continue cleanup.
342- if "attached to a different loop" in str ( e ):
343- logger . warning ( f"Task cleanup skipped (different event loop): { e } " )
344- else :
345- raise
336+ tasks_to_wait . append ( task )
337+
338+ if tasks_to_wait :
339+ # Wait briefly — don't block indefinitely if tasks are stuck
340+ # in long-running operations (LLM calls, tool executions).
341+ _ , pending = await asyncio . wait ( tasks_to_wait , timeout = 5.0 )
342+ if pending :
343+ logger . warning (
344+ "%d execution task(s) did not finish within 5s after cancellation" ,
345+ len ( pending ),
346+ )
346347
347348 self ._execution_tasks .clear ()
348349 self ._active_executions .clear ()
@@ -878,10 +879,11 @@ async def cancel_execution(self, execution_id: str) -> bool:
878879 task = self ._execution_tasks .get (execution_id )
879880 if task and not task .done ():
880881 task .cancel ()
881- try :
882- await task
883- except asyncio .CancelledError :
884- pass
882+ # Wait briefly for the task to finish. Don't block indefinitely —
883+ # the task may be stuck in a long LLM API call that doesn't
884+ # respond to cancellation quickly. The cancellation is already
885+ # requested; the task will clean up in the background.
886+ done , _ = await asyncio .wait ({task }, timeout = 5.0 )
885887 return True
886888 return False
887889
0 commit comments