CLI deadlock on lazy-dep install prompt under prompt_toolkit
NousResearch/hermes-agent · cluster 40490
Data as of
Nothing open on GitHub. Fix merged in #40901. Check it covers your case.
The short answer
- Known?
- Yes. 6 related items grouped here, 1 issue already closed.
- Fix event
- A fix was merged to main in PR #40901. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
resolved, deciding PR #40901. That is separate from the fix event. - Still open
- Nothing open on GitHub.
A bug where using bare input() for lazy-dependency install prompts causes an unrecoverable lockup when prompt_toolkit owns the terminal. Multiple fix attempts address this by skipping or properly routing the interactive prompt in that context.
Open issues0
Open PRs0
Fixes on record
-
#40901 fix(cli): never deadlock on lazy-dep prompt + make Pillow a core dep (#40490)
files:
pyproject.toml,tools/lazy_deps.py,tools/transcription_tools.py,tools/vision_tools.pyShow the diff
-
pyproject.toml@@ -91,6 +91,14 @@ dependencies = [ "uvicorn[standard]>=0.24.0,<1", "ptyprocess>=0.7.0,<1; sys_platform != 'win32'", "pywinpty>=2.0.0,<3; sys_platform == 'win32'", + # Image resize recovery for the vision tools. Pillow shrinks oversized images + # (>5 MB or >8000px) at embed time; without it the byte AND pixel-dimension + # shrink paths no-op, so an oversized image bakes into immutable history and + # bricks the session on Anthropic's non-retryable 400. Pure-wheel, no system + # libs required for the codecs we use, so it's safe to ship in the base + # install rather than gating it behind an extra + a mid-session lazy install + # (which deadlocked the CLI under prompt_toolkit — see #40490). + "Pillow==12.2.0", ] [project.optional-dependencies] @@ -137,14 +145,12 @@ pty = [ # without pulling in extra packages. ] honcho = ["honcho-ai==2.0.1"] -# Image resize recovery for the vision tools. Pillow is a soft dependency: -# vision_tools / conversation_compression degrade gracefully without it (they -# log and skip the resize), but without it the byte AND pixel-dimension shrink -# paths silently no-op, so an oversized image (>5 MB or >8000px) bakes into -# immutable history and bricks the session on Anthropic's non-retryable 400. -# Declared here so packagers (Nix, Homebrew) ship it with [all] and so -# `pip install hermes-agent[vision]` / the lazy-install path can resolve it. -vision = ["Pillow==12.2.0"] +# Image resize recovery for the vision tools. Pillow is now a CORE dependency +# (see the main `dependencies` list) since the byte/pixel shrink paths are on +# the default vision-embed path and the mid-session lazy install deadlocked the +# CLI under prompt_toolkit (#40490). This extra is kept as a no-op back-compat +# alias so existing `pip install hermes-agent[vision]` invocations still resolve. +vision = [] # CVE-2026-48710 (BadHost): Starlette is pulled transitively by mcp's # sse-starlette / HTTP-SSE stack (and by fastapi in the `web` extra). Before # 1.0.1, a malformed Host header makes `request.url.path` desync from the path -
tools/lazy_deps.py@@ -174,11 +174,11 @@ "uvicorn[standard]==0.41.0", "starlette==1.0.1", # CVE-2026-48710 (BadHost) — keep lazy-install in sync with pyproject [web] ), - # Vision image-resize recovery (Pillow). Soft dependency: vision_tools and - # conversation_compression degrade gracefully without it, but the byte AND - # pixel-dimension shrink paths no-op when it's absent, so an oversized - # image can brick a session on Anthropic's non-retryable 400. Keep in sync - # with pyproject [vision]. + # Vision image-resize recovery (Pillow). Pillow is now a CORE dependency + # (pyproject `dependencies`), so this entry is a belt-and-suspenders fallback + # for stripped/source-build installs that somehow dropped it. The vision + # call site uses prompt=False so it can never raise a blocking input() + # prompt mid-session (#40490). "tool.vision": ("Pillow==12.2.0",), } @@ -456,7 +456,23 @@ def ensure(feature: str, *, prompt: bool = True) -> None: "lazy installs disabled (security.allow_lazy_installs=false)" ) - if prompt and sys.stdin.isatty() and sys.stdout.isatty(): + # Only show the interactive confirmation when we own a TTY and + # prompt_toolkit isn't running. A bare input() deadlocks when a + # prompt_toolkit app owns the terminal because keystrokes route to + # its event loop rather than stdin, so the prompt blocks forever. + # Under the TUI we skip the prompt and proceed — lazy installs are + # gated by security.allow_lazy_installs, so reaching here is + # already user opt-in. + _pt_active = False + if "prompt_toolkit.application.current" in sys.modules: + try: + from prompt_toolkit.application.current import get_app_or_none + _app = get_app_or_none() + _pt_active = _app is not None and getattr(_app, "is_running", False) + except Exception: + _pt_active = False + + if prompt and not _pt_active and sys.stdin.isatty() and sys.stdout.isatty(): spec_list = ", ".join(missing) try: answer = input( -
tools/transcription_tools.py@@ -214,7 +214,11 @@ def _try_lazy_install_stt() -> bool: """ try: from tools.lazy_deps import ensure - ensure("stt.faster_whisper") + # prompt=False: never raise a blocking input() prompt mid-session. + # Under the interactive CLI prompt_toolkit owns stdin, so a bare + # input() deadlocks the terminal (#40490). The install is already + # gated by security.allow_lazy_installs, so reaching here is opt-in. + ensure("stt.faster_whisper", prompt=False) # Re-check dynamically after install import importlib.util as _iu if _iu.find_spec("faster_whisper"): -
tools/vision_tools.py@@ -422,7 +422,11 @@ def _resize_image_for_vision(image_path: Path, mime_type: Optional[str] = None, # the raw bytes and let the caller raise the size error. try: from tools.lazy_deps import ensure as _ensure_dep - _ensure_dep("tool.vision") + # prompt=False: never raise a blocking input() prompt mid-session. + # Under the interactive CLI prompt_toolkit owns stdin, so a bare + # input() deadlocks the terminal (#40490). The install is already + # gated by security.allow_lazy_installs, so reaching here is opt-in. + _ensure_dep("tool.vision", prompt=False) from PIL import Image import io as _io except Exception: -
uv.lock@@ -1401,6 +1401,7 @@ dependencies = [ { name = "markdown" }, { name = "openai" }, { name = "pathspec" }, + { name = "pillow" }, { name = "prompt-toolkit" }, { name = "psutil" }, { name = "ptyprocess", marker = "sys_platform != 'win32'" }, @@ -1563,9 +1564,6 @@ termux-all = [ tts-premium = [ { name = "elevenlabs" }, ] -vision = [ - { name = "pillow" }, -] voice = [ { name = "faster-whisper" }, { name = "numpy" }, @@ -1654,7 +1652,7 @@ requires-dist = [ { name = "openai", specifier = "==2.24.0" }, { name = "parallel-web", marker = "extra == 'parallel-web'", specifier = "==0.4.2" }, { name = "pathspec", specifier = "==1.1.1" }, - { name = "pillow", marker = "extra == 'vision'", specifier = "==12.2.0" }, + { name = "pillow", specifier = "==12.2.0" }, { name = "prompt-toolkit", specifier = "==3.0.52" }, { name = "psutil", specifier = "==7.2.2" }, { name = "ptyprocess", marker = "sys_platform != 'win32'", specifier = ">=0.7.0,<1" },
-
4 PRs closed without merging
From the close comments: done elsewhere 1 · merged via another change 1 · other 2
-
#40506 fix(cli): skip interactive lazy-install prompt when under prompt_toolkit (#40490) · other →
#40561,#40490 -
#40520 fix(cli): use prompt_toolkit run_in_terminal for lazy-dep install prompt (#40490) · other →
#40561,#40490 -
#40561 fix(lazy-deps): don't deadlock the lazy-install prompt under the TUI (#40490) · done elsewhere →
d47f919,c3d750c,#40490 -
#40579 fix(cli): skip lazy-dep prompt when prompt_toolkit owns terminal (#40490) · merged via another change →
d47f919,#40901
Closed issues · 1
- Closed#40490[Bug]: CLI input locks up unrecoverably on lazy-dep install prompt (lazy_deps.py uses bare input() under prompt_toolkit)
How this cluster was grouped
-
Link evidence (
structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference. -
Pairwise link density
0.33: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the LabAt a glance
- Merged PRs
- #40901
- Closed issues
- 1
- Attempts
- 4 PRs closed without merging
- Fix link checked
- 7 Jun 2026