Skip to content

fix(cli): prevent 'Press ENTER to continue...' on exit#2555

Merged
teknium1 merged 1 commit intomainfrom
hermes/hermes-fdcb4c4a
Mar 22, 2026
Merged

fix(cli): prevent 'Press ENTER to continue...' on exit#2555
teknium1 merged 1 commit intomainfrom
hermes/hermes-fdcb4c4a

Conversation

@teknium1
Copy link
Copy Markdown
Contributor

@teknium1 teknium1 commented Mar 22, 2026

Problem

During normal CLI usage (not on exit), users see this interrupting their session:

Unhandled exception in event loop:
  File httpx/_client.py in aclose
  ...
  RuntimeError: Event loop is closed

Press ENTER to continue...

Root Cause

The OpenAI SDK wraps httpx's AsyncClient in AsyncHttpxClientWrapper which has a __del__:

class AsyncHttpxClientWrapper(DefaultAsyncHttpxClient):
    def __del__(self) -> None:
        if self.is_closed:
            return
        try:
            asyncio.get_running_loop().create_task(self.aclose())
        except Exception:
            pass

The mid-session flow that triggers this:

  1. Turn 1: Agent tool call runs on worker thread → creates AsyncOpenAI client bound to thread's event loop → client is cached in _client_cache
  2. Worker thread finishes → its event loop closes
  3. Turn 2: New worker thread → _get_cached_client() detects stale entry (loop is closed) → evicts it with del
  4. del triggers __del__asyncio.get_running_loop() finds prompt_toolkit's running loop → schedules aclose()
  5. aclose() tries to close TCP connections on the dead worker loop → RuntimeError: Event loop is closed
  6. prompt_toolkit catches the unhandled exception and shows "Press ENTER to continue..."

Fix

  • _force_close_async_httpx() — sets httpx AsyncClient._state = ClientState.CLOSED, so __del__ checks is_closed and returns immediately (no-op)
  • Stale client eviction (_get_cached_client) — now calls _force_close_async_httpx() before del, preventing the mid-session error
  • shutdown_cached_clients() — called from _run_cleanup() on CLI exit to clean up any remaining cached clients
  • CLI cleanup_run_cleanup() in cli.py calls shutdown_cached_clients()

Files Changed

  • agent/auxiliary_client.py — new shutdown functions + fix stale eviction
  • cli.py — call shutdown in cleanup
When AsyncOpenAI clients are garbage-collected after the event loop
closes, their AsyncHttpxClientWrapper.__del__ tries to schedule
aclose() on the dead loop, causing RuntimeError: Event loop is closed.
prompt_toolkit catches this as an unhandled exception and shows
'Press ENTER to continue...' which blocks CLI exit.

Fix: Add shutdown_cached_clients() to auxiliary_client.py that marks
all cached async clients' underlying httpx transport as CLOSED before
GC runs. This prevents __del__ from attempting the aclose() call.

- _force_close_async_httpx(): sets httpx AsyncClient._state to CLOSED
- shutdown_cached_clients(): iterates _client_cache, closes sync clients
  normally and marks async clients as closed
- Also fix stale client eviction in _get_cached_client to mark evicted
  async clients as closed (was just del-ing them, triggering __del__)
- Call shutdown_cached_clients() from _run_cleanup() in cli.py
@teknium1 teknium1 merged commit 2b3c1d8 into main Mar 22, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant