From eabd8c1fd12d6e386d636e564444ef661ce99e81 Mon Sep 17 00:00:00 2001 From: Jeremy Irish Date: Wed, 6 May 2026 16:08:52 -0700 Subject: [PATCH] fix(cli): fall back to SelectSelector when kqueue can't watch stdin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On macOS with uv-managed cPython 3.11, the default kqueue selector cannot register fd 0, so prompt_toolkit's loop.add_reader raises OSError(EINVAL) ("[Errno 22] Invalid argument") from kqueue.control() and the agent crashes immediately on startup (#5884, also reported in #6393). Probe KqueueSelector.register(0, EVENT_READ) before launching prompt_toolkit. If it fails, install an event-loop policy that returns a SelectorEventLoop backed by SelectSelector — select() works fine on stdin in this Python build, so add_reader succeeds and the agent launches normally. Also extend the existing #6393 fallback handler to recognize EINVAL / EBADF / "Invalid argument" so that any future selector failure on stdin shows the friendly "reinstall Python via pyenv or Homebrew" guidance instead of an opaque traceback. Verified on macOS (Darwin 24.6.0) with uv-managed cPython 3.11.15: the kqueue probe fails, the policy switch fires, and `hermes` launches cleanly. No effect on platforms where kqueue can register fd 0. --- cli.py | 38 +++++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/cli.py b/cli.py index 728309733b6..af179c86c13 100644 --- a/cli.py +++ b/cli.py @@ -13401,6 +13401,30 @@ class HermesCLI: self._print_exit_summary() return + # On macOS with uv-managed Python, kqueue's selector cannot register + # fd 0, raising OSError(EINVAL) from kqueue.control() when prompt_toolkit + # calls loop.add_reader (#6393). Probe kqueue and, if it can't watch + # stdin, switch to a SelectSelector-backed event loop policy. + if sys.platform == "darwin": + try: + import selectors as _selectors + if hasattr(_selectors, "KqueueSelector"): + _kq = _selectors.KqueueSelector() + try: + _kq.register(0, _selectors.EVENT_READ) + _kq.unregister(0) + finally: + _kq.close() + except (OSError, ValueError, KeyError): + import asyncio as _aio_probe + import selectors as _selectors + + class _SelectEventLoopPolicy(_aio_probe.DefaultEventLoopPolicy): + def new_event_loop(self): + return _aio_probe.SelectorEventLoop(_selectors.SelectSelector()) + + _aio_probe.set_event_loop_policy(_SelectEventLoopPolicy()) + # Run the application with patch_stdout for proper output handling try: with patch_stdout(): @@ -13421,12 +13445,20 @@ class HermesCLI: except (KeyError, OSError) as _stdin_err: # Catch selector registration failures from broken stdin (#6393) # and I/O errors from broken stdout during interrupt (#13710). - if isinstance(_stdin_err, OSError) and getattr(_stdin_err, "errno", None) == errno.EIO: + _errno = getattr(_stdin_err, "errno", None) if isinstance(_stdin_err, OSError) else None + _msg = str(_stdin_err) + if _errno == errno.EIO: pass # suppress broken-stdout I/O errors on interrupt (#13710) - elif "is not registered" in str(_stdin_err) or "Bad file descriptor" in str(_stdin_err): + elif ( + _errno in (errno.EINVAL, errno.EBADF) + or "is not registered" in _msg + or "Bad file descriptor" in _msg + or "Invalid argument" in _msg + ): print( f"\nError: stdin is not usable ({_stdin_err}).\n" - "This can happen with certain Python installations (e.g. uv-managed cPython on macOS).\n" + "This can happen with certain Python installations (e.g. uv-managed cPython on macOS)\n" + "where kqueue cannot register fd 0.\n" "Try reinstalling Python via pyenv or Homebrew, then re-run: hermes setup" ) else: