Skip to content

Bug: RuntimeError: Event loop is closed when quitting TUI #558

@szmania

Description

@szmania

Description

When attempting to quit the cecli TUI, a RuntimeError: Event loop is closed is raised, causing the application to crash.

Steps to Reproduce

  1. Start cecli TUI
  2. Attempt to quit (e.g., by pressing q or Ctrl+C)
  3. 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:

  1. TUI._do_quit() calls self.worker.stop()
  2. CoderWorker.stop() calls self.interrupt()
  3. CoderWorker.interrupt() attempts to cancel target_coder.io.output_task
  4. This triggers asyncio.base_events.call_soon() which calls _check_closed()
  5. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions