Skip to content

run_in_terminal() coroutines not awaited — silent message loss (output) + broken input on WSL #23185

@yonghuatrc

Description

@yonghuatrc

Summary

run_in_terminal() from prompt_toolkit is an async function that always returns a coroutine. Multiple call sites in cli.py invoke it without await and without a fallback, causing two distinct bugs that share the same root cause.


Bug A — Silent output loss in _cprint() (line 1515)

File: cli.py, function _cprint(), inside closure _schedule()

def _schedule():
    try:
        run_in_terminal(lambda: _pt_print(_PT_ANSI(text)))  # NOT awaited
    except Exception:
        try:
            _pt_print(_PT_ANSI(text))
        except Exception:
            pass

try:
    loop.call_soon_threadsafe(_schedule)
except Exception:
    ...

What happens: run_in_terminal() returns a coroutine that is passed to call_soon_threadsafe. The callback is scheduled, but the coroutine object itself is never awaited — it gets garbage collected by Python, triggering RuntimeWarning: coroutine 'run_in_terminal.<locals>.run' was never awaited.

Impact: When _cprint fires from a background thread (e.g., process_loop, voice auto-restart thread, self-improvement summaries), the printed text is silently dropped. The warning message "process_loop unhandled error (msg may be lost)" at line 12338 is the symptom of this.

Trigger: Background thread calls _cprint while the prompt_toolkit app loop is running on a different thread — specifically when get_running_loop() succeeds and differs from app.loop.


Bug B — Broken input prompt on WSL in _prompt_text_input() (line 5876)

File: cli.py, function _prompt_text_input()

def _prompt_text_input(self, prompt_text: str) -> str | None:
    ...
    if self._app:
        from prompt_toolkit.application import run_in_terminal
        ...
        try:
            run_in_terminal(_ask)  # NOT awaited, no fallback on WSL
        finally:
            ...

What happens: On WSL terminals (Warp, PowerShell, etc.), the run_in_terminal() coroutine is scheduled but the event loop fails silently to execute it. No exception propagates up, no fallback runs, result[0] stays None.

Impact: The input prompt never displays. User keystrokes go to the agent buffer instead of the confirmation prompt — making it appear the agent is consuming their input.


Root Cause

prompt_toolkit.application.run_in_terminal() (v3.0.52+):

def run_in_terminal(func: Callable[[], _T], ...) -> Awaitable[_T]:
    async def run() -> _T:
        async with in_terminal(...):
            return func()
    return ensure_future(run())  # always returns a coroutine (Future)

Every call site must either await the returned coroutine or wrap in try/except with a direct fallback.


Fix Summary

  • Bug A (_cprint): Await the coroutine via asyncio.ensure_future() inside the callback, or use loop.call_soon_threadsafe with asyncio.create_task pattern.
  • Bug B (_prompt_text_input): Add except Exception: _ask() after run_in_terminal(_ask) to provide the direct fallback when run_in_terminal fails on WSL.

Environment:

  • prompt_toolkit>=3.0.52,<4
  • Python 3.11
  • WSL (for Bug B)
  • Background thread scenario (for Bug A)

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Medium — degraded but workaround existscomp/cliCLI entry point, hermes_cli/, setup wizardtype/bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions