# NousResearch/hermes-agent · CLI deadlock on lazy-dep install prompt under prompt_toolkit

## Open now

- 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.

## TL;DR

- 6 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 4 PR(s) closed without merging.
- A fix was merged to main in PR #40901. Verify it addresses your case.
- **Recommended:** read PR #40901 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `resolved` · deciding PR(s): `pr-40901`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.33`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Closed / duplicate issues

- #40490: \[Bug\]: CLI input locks up unrecoverably on lazy-dep install prompt (lazy_deps.py uses bare input() under prompt_toolkit)

## Prior & competing PRs

- **#40506: fix(cli): skip interactive lazy-install prompt when under prompt_toolkit (#40490)**. closed without merge · fix tier `suggested`
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `#40561`, `#40490`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#40561: fix(lazy-deps): don't deadlock the lazy-install prompt under the TUI (#40490)**. closed without merge · fix tier `suggested`
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `d47f919ef`, `c3d750c1a`, `#40490`, `#40490`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#40901: fix(cli): never deadlock on lazy-dep prompt + make Pillow a core dep (#40490)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `pyproject.toml`, `tools/lazy_deps.py`, `tools/transcription_tools.py`, `tools/vision_tools.py`
  - fix hunks:
  - `pyproject.toml`

    ```diff
    @@ -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]
    ```

    ```diff
    @@ -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`

    ```diff
    @@ -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",),
     }
     
    ```

    ```diff
    @@ -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`

    ```diff
    @@ -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`

    ```diff
    @@ -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`

    ```diff
    @@ -1401,6 +1401,7 @@ dependencies = [
         { name = "markdown" },
         { name = "openai" },
         { name = "pathspec" },
    +    { name = "pillow" },
         { name = "prompt-toolkit" },
         { name = "psutil" },
         { name = "ptyprocess", marker = "sys_platform != 'win32'" },
    ```

    ```diff
    @@ -1563,9 +1564,6 @@ termux-all = [
     tts-premium = [
         { name = "elevenlabs" },
     ]
    -vision = [
    -    { name = "pillow" },
    -]
     voice = [
         { name = "faster-whisper" },
         { name = "numpy" },
    ```

    ```diff
    @@ -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" },
    ```
- **#40520: fix(cli): use prompt_toolkit run_in_terminal for lazy-dep install prompt (#40490)**. closed without merge
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `#40561`, `#40490`
- **#40579: fix(cli): skip lazy-dep prompt when prompt_toolkit owns terminal (#40490)**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `d47f919ef`, `#40901`

## Fix status

- Merged to main in PR #40901 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-06-07`.
