hermes - ✅(Solved) Fix run_in_terminal() coroutines not awaited — silent message loss (output) + broken input on WSL [2 pull requests, 4 comments, 3 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
NousResearch/hermes-agent#23185Fetched 2026-05-11 03:30:38
View on GitHub
Comments
4
Participants
3
Timeline
12
Reactions
0
Timeline (top)
commented ×4cross-referenced ×3labeled ×3closed ×1

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.


Error Message

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

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 Action

Fixed

PR fix notes

PR #23302: fix: 4 small surgical bugs — kanban delete alias, gateway title noise, bg truncation, cprint coroutine

Description (problem / solution / changelog)

Four independent one-area fixes, each with its own root cause analysis.

Fix 1 — kanban boards delete alias archives instead of deletes (#23139)

The delete subcommand alias shares _cmd_boards_rm but the --delete flag belongs only to the rm subparser. getattr(args, 'delete', False) therefore returns False when the alias is used, so remove_board(archive=True) runs — archiving the board instead of deleting it.

Fix: detect boards_action == 'delete' and treat it as force_delete=True. Test added.

Fix 2 — Gateway auto-title failure leaks as user-visible message (#23246)

When background title generation fails (e.g. HTTP 401 on auxiliary provider), _emit_auxiliary_failure routes the error through _emit_warning, which delivers it as a visible ⚠ message in Slack/Telegram/WhatsApp. Not actionable to the end user.

Fix: replace the failure_callback in Gateway mode with a debug-log-only lambda.

Fix 3 — Background process notification starts mid-line when output is truncated (#23284)

session.output_buffer[-2000:] truncates at an arbitrary byte offset, potentially starting the notification mid-line.

Fix: snap to the next newline boundary after the cut and prepend [… output truncated — showing last N chars] when content was dropped.

Fix 4 — _cprint() silently drops output from background threads (#23185 Bug A)

run_in_terminal() returns a coroutine (Future). Passing it bare to call_soon_threadsafe schedules the callback but never awaits the coroutine — Python garbage-collects it with RuntimeWarning, silently dropping the output.

Fix: wrap with asyncio.ensure_future() so the coroutine is properly scheduled on the running loop.

Changed files

  • cli.py (modified, +18/-5)
  • gateway/run.py (modified, +23/-8)
  • hermes_cli/kanban.py (modified, +6/-1)
  • scripts/release.py (modified, +1/-0)
  • tests/hermes_cli/test_kanban_boards.py (modified, +33/-0)
  • tools/process_registry.py (modified, +1/-1)

PR #23454: fix(cli): drive _prompt_text_input directly when off main thread (#23185)

Description (problem / solution / changelog)

Summary

Confirmation prompts for /clear, /new, /undo, and /reload-mcp now actually display in the classic CLI instead of swallowing the user's keystrokes.

Root cause: _prompt_text_input calls prompt_toolkit.run_in_terminal unconditionally. run_in_terminal returns a coroutine that only the main-thread event loop can drive. Slash commands dispatch from the process_loop daemon thread, so the coroutine was orphaned, _ask never ran, and the next keypress flowed into the composer instead of the prompt.

Changes

  • cli.py _prompt_text_input: thread-aware guard mirroring _run_curses_picker (line 5830) — when off the main thread, call input() directly. Also wrap run_in_terminal in try/except so WSL / Warp emulators that silently drop the scheduled coroutine fall back to input() instead of returning None.
  • tests/cli/test_prompt_text_input_thread_safety.py — 5 new tests: main-thread dispatch, background-thread fallback, no-app, run_in_terminal-raises, EOF handling.

Validation

BeforeAfter
Daemon-thread _prompt_text_inputrun_in_terminal called, coroutine orphaned, returns Noneinput() called, returns user choice
WSL / Warp main threadrun_in_terminal silently drops, returns Nonefalls back to input()
Targeted suiten/a29/29 passing (slash_confirm, mcp_reload_confirm, cli_reload_skills, this)
E2E from named 'process_loop' threadrit_called=True, value=Nonerit_called=False, value='2'

Notes on issue #23185

This addresses Bug B (input prompt path). Bug A (_cprint orphaned-coroutine output loss) is a separate fix — the existing fallback at line 1517 keeps messages from being lost; only the RuntimeWarning remains. Will follow up with that as a separate PR.

The commenter's claim that ruamel.yaml is not in requirements.txt is incorrect: it's pinned in pyproject.toml line 23 (ruamel.yaml>=0.18.16,<0.19), so the 'Always Approve' persistence path works on a clean install. No need to disable the feature.

Changed files

  • cli.py (modified, +22/-2)
  • tests/cli/test_prompt_text_input_thread_safety.py (added, +109/-0)

Code Example

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

---

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

---

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)
RAW_BUFFERClick to expand / collapse

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)

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

hermes - ✅(Solved) Fix run_in_terminal() coroutines not awaited — silent message loss (output) + broken input on WSL [2 pull requests, 4 comments, 3 participants]