Description
When attempting to quit the cecli TUI, a RuntimeError: Event loop is closed is raised, causing the application to crash.
Steps to Reproduce
- Start cecli TUI
- Attempt to quit (e.g., by pressing
q or Ctrl+C)
- The following traceback appears:
Would you like to see what's new in this version? (Y)es/(N)o [Yes]: n
Starting cecli TUI...
╭─────────────────────────────────────────────────────────────────────────────────────────────── Traceback (most recent call last) ────────────────────────────────────────────────────────────────────────────────────────────────╮
│ C:\Users\PDitty\AppData\Roaming\uv\tools\cecli-dev\Lib\site-packages\cecli\tui\app.py:1291 in _do_quit │
│ │
│ 1288 │ ╭───────────────────────────────────── locals ──────────────────────────────────────╮ │
│ 1289 │ def _do_quit(self): │ self = TUI(title='TUI', classes={'-dark-mode'}, pseudo_classes={'focus', 'dark'}) │ │
│ 1290 │ │ """Perform the actual quit after UI updates.""" ╰───────────────────────────────────────────────────────────────────────────────────╯ │
│ ❱ 1291 │ │ self.worker.stop() │
│ 1292 │ │ self.exit() │
│ 1293 │ │
│ 1294 │ def run_obstructive(self, func, *args, **kwargs): │
│ │
│ C:\Users\PDitty\AppData\Roaming\uv\tools\cecli-dev\Lib\site-packages\cecli\tui\worker.py:190 in stop │
│ │
│ 187 │ │ │ # We'll just pass to allow the thread to exit gracefully ╭────────────────────────────── locals ──────────────────────────────╮ │
│ 188 │ │ │ # without a scary traceback. │ self = <cecli.tui.worker.CoderWorker object at 0x000001F2BE3842F0> │ │
│ 189 │ │ │ pass ╰────────────────────────────────────────────────────────────────────╯ │
│ ❱ 190 │ │ self.interrupt() │
│ │
│ 191 │ │
│ 192 │ # Wait for thread to finish │
│ │
│ 193 │ if self.thread and self.thread.is_alive(): │
│ │
│ C:\Users\PDitty\AppData\Roaming\uv\tools\cecli-dev\Lib\site-packages\cecli\tui\worker.py:160 in interrupt │
│ │
│ 157 │ │ if target_coder and hasattr(target_coder, "io") and target_coder.io: ╭────────────────────────────────────────── locals ──────────────────────────────────────────╮ │
│ 158 │ │ # Cancel the output task if it exists agent_service = <cecli.helpers.agents.service.AgentService object at 0x000001F2BF46DFD0> │ │
│ 159 │ │ if hasattr(target_coder.io, "output_task") and target_coder.io.output_task: │ foreground = <cecli.coders.editblock_coder.EditBlockCoder object at 0x000001F30126C2D0> │ │
│ ❱ 160 │ │ │ target_coder.io.output_task.cancel() │ self = <cecli.tui.worker.CoderWorker object at 0x000001F2BE3842F0> │
│ 161 │ │ │ # Also set output_running to False to stop the output_task loop │ target_coder = <cecli.coders.editblock_coder.EditBlockCoder object at 0x000001F30126C2D0> │ │
│ 162 │ │ │ if hasattr(target_coder, "output_running"): ╰────────────────────────────────────────────────────────────────────────────────────────────╯ │
│ 163 │ │ │ │ target_coder.output_running = False │
│ │
│ D:\Python\Python3_13_9\Lib\asyncio\base_events.py:833 in call_soon │
│ │
│ 830 │ │ Any positional arguments after the callback will be passed to ╭──────────────────────────────────────── locals ────────────────────────────────────────╮ │
│ 831 │ │ the callback when it is called. args = (<Future cancelled>,) │
│ 832 │ │ """ │
│ 833 │ │ callback = <built-in method task_wakeup of _asyncio.Task object at 0x000001F3025E4F70> │
│ ❱ 833 │ │ self._check_closed() │
│ 834 │ │ context = <_contextvars.Context object at 0x000001F3019E3080> │
│ │
│ 835 │ │ if self._debug: │
│ 836 │ │ │ self._check_thread() │
│ 837 │ │ self = <_WindowsSelectorEventLoop running=False closed=True debug=False> ╰───────────────────────────────────────────────────────────────────────────────────────────╯ │
│ │
│ 838 │ │ │ self._check_callback(callback, 'call_soon') │
│ │
│ D:\Python\Python3_13_9\Lib\asyncio\base_events.py:556 in _check_closed │
│ │
│ 553 │ │ │
│ 554 │ def _check_closed(self): │
│ 555 │ │ if self._closed: │
│ 556 │ │ │ raise RuntimeError('Event loop is closed') │
Environment
- cecli version: Latest (from cecli-dev)
- Python version: 3.13.9
- OS: Windows 11
Additional Context
The error occurs in the following call chain:
TUI._do_quit() calls self.worker.stop()
CoderWorker.stop() calls self.interrupt()
CoderWorker.interrupt() attempts to cancel target_coder.io.output_task
- This triggers
asyncio.base_events.call_soon() which calls _check_closed()
- The event loop is already closed, raising the
RuntimeError
Expected Behavior
The TUI should quit gracefully without raising an exception.
Possible Fix
The issue appears to be a race condition where the event loop is closed before the output task is properly cancelled. A potential fix would be to check if the event loop is still running before attempting to cancel tasks.
Description
When attempting to quit the cecli TUI, a
RuntimeError: Event loop is closedis raised, causing the application to crash.Steps to Reproduce
qor Ctrl+C)Environment
Additional Context
The error occurs in the following call chain:
TUI._do_quit()callsself.worker.stop()CoderWorker.stop()callsself.interrupt()CoderWorker.interrupt()attempts to canceltarget_coder.io.output_taskasyncio.base_events.call_soon()which calls_check_closed()RuntimeErrorExpected Behavior
The TUI should quit gracefully without raising an exception.
Possible Fix
The issue appears to be a race condition where the event loop is closed before the output task is properly cancelled. A potential fix would be to check if the event loop is still running before attempting to cancel tasks.