fix(cli): fall back to SelectSelector when kqueue can't watch stdin

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.
This commit is contained in:
Jeremy Irish 2026-05-06 16:08:52 -07:00 committed by Teknium
parent 4695d2716f
commit eabd8c1fd1

38
cli.py
View file

@ -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: