hermes - ✅(Solved) Fix [Bug]: /clear, /new, /reset, /undo confirmation prompt cannot be answered — keystrokes leak into chat composer [3 pull requests, 2 comments, 2 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#22958Fetched 2026-05-11 03:32:06
View on GitHub
Comments
2
Participants
2
Timeline
12
Reactions
0
Timeline (top)
cross-referenced ×3labeled ×3commented ×2referenced ×2

Error Message

Additional Logs / Traceback (optional)

WARNING cli: process_loop unhandled error (msg may be lost):

Root Cause

Root Cause Analysis (optional)

Fix Action

Fix / Workaround

Workaround for affected users

PR fix notes

PR #22989: fix(cli): use prompt_toolkit.prompt() for TUI confirmation prompts (Fixes #22958)

Description (problem / solution / changelog)

Problem

When using destructive slash commands (/clear, /new, /reset, /undo) in TUI mode, the confirmation prompt renders correctly but cannot be answered. Typing 1, 2, or 3 sends that character to the agent as a chat message instead of resolving the prompt.

Root Cause

_prompt_text_input() in cli.py uses Python's built-in input() inside prompt_toolkit.application.run_in_terminal(). When the TUI Application is active, input() cannot receive keystrokes because prompt_toolkit's input reader intercepts them before they reach the stdin buffer that input() reads from.

Fix

Replace input() with prompt_toolkit.shortcuts.prompt() (aliased as pt_prompt) inside run_in_terminal(). pt_prompt properly negotiates terminal state with the running event loop, ensuring keystrokes are handled correctly.

Changes

  • Import: Add from prompt_toolkit.shortcuts import prompt as pt_prompt
  • _prompt_text_input(): Use pt_prompt() when self._app is active (TUI mode), keep input() fallback for non-TUI mode
  • Options passed to pt_prompt(): multiline=False, enable_history_search=False, enable_suspend=False, mouse_support=False
  • Error handling: Both paths catch KeyboardInterrupt and EOFError, returning None

Testing

  • TUI path uses pt_prompt() when self._app is active
  • Non-TUI path falls back to input() when self._app is None
  • Both paths handle KeyboardInterrupt and EOFError
  • Status bar visibility is restored in finally block

Fixes #22958

Changed files

  • cli.py (modified, +71/-478)

PR #22987: fix(cli): schedule prompt_toolkit terminal awaitables

Description (problem / solution / changelog)

Summary

  • Schedule prompt_toolkit.application.run_in_terminal(...) on the active prompt_toolkit app loop before calling it from background slash-command threads.
  • Fix destructive slash command confirmation prompts such as /new, /reset, /clear, and /undo when prompt_toolkit returns an awaitable.
  • Apply the same awaitable handling to related terminal-bridged call sites for curses picker, background _cprint, and Ctrl+Z suspend.
  • Add a regression test covering _prompt_text_input from a process-loop-like background thread while run_in_terminal executes on the app-loop thread.

Related issues / PRs

  • Related to #22958: destructive slash-command confirmation prompts in TUI mode.
  • Addresses the run_in_terminal(...) awaitable root cause of #22970 and duplicate report #23009: RuntimeWarning: coroutine 'run_in_terminal.<locals>.run' was never awaited.
  • Overlaps with #22989, which changes the prompt implementation itself (input() -> prompt_toolkit prompt). This PR focuses on safely scheduling/awaiting run_in_terminal(...) from background-thread call sites and includes a regression test for that path.

Why

/new and other destructive slash commands run their confirmation flow from the CLI process_loop background thread while prompt_toolkit owns the terminal on its application event loop. In prompt_toolkit 3.x, run_in_terminal(...) returns/schedules an awaitable and must be called from the app loop. Calling it directly from the background thread can drop the prompt and emit:

RuntimeWarning: coroutine 'run_in_terminal.<locals>.run' was never awaited

How to test

Reproduction before this fix:

  1. Start interactive Hermes CLI.
  2. Enter /new.
  3. Observe the destructive-command confirmation fail with the unawaited coroutine warning.

After this fix:

  1. Start interactive Hermes CLI.
  2. Enter /new.
  3. The confirmation prompt appears.
  4. Enter 3 to cancel.
  5. Hermes reports /new cancelled without the coroutine warning.

Platforms tested

  • macOS
  • Python 3.11
  • prompt_toolkit 3.0.52

Test Plan

Verified in a fresh clone following the CONTRIBUTING setup instructions on commit 3bc8df818.

Passed:

  • venv/bin/python -m py_compile cli.py tests/cli/test_cprint_bg_thread.py
  • venv/bin/python scripts/check-windows-footguns.py cli.py tests/cli/test_cprint_bg_thread.py
  • scripts/run_tests.sh tests/cli/test_cprint_bg_thread.py tests/hermes_cli/test_destructive_slash_confirm_gate.py
    • 17 passed

Manual verification:

  • Started Hermes with an isolated temporary home directory, ran /new, selected 3 to cancel, and verified the confirmation prompt worked without the unawaited coroutine warning.

Full-suite note:

  • Attempted scripts/run_tests.sh tests in a fresh clone. It did not complete green (16333 passed, 75 skipped, 33 failed, 5057 errors), with broad unrelated-looking failures across plugin/skill/TUI/website tests. A representative reported failing test passed when rerun in isolation, so I did not treat this as related to this CLI fix.
  • scripts/run_tests.sh with no args currently fails locally with ARGS[@]: unbound variable; scripts/run_tests.sh tests was used for the full-suite attempt.

Changed files

  • cli.py (modified, +80/-13)
  • tests/cli/test_cprint_bg_thread.py (modified, +72/-0)

PR #23348: fix(cli): use prompt_toolkit prompt for confirmation input

Description (problem / solution / changelog)

Replaces bare input() with prompt_toolkit.shortcuts.prompt inside _prompt_text_input() so keystrokes are captured by the TUI renderer instead of leaking into the chat composer. This fixes the /clear, /new, /reset, and /undo confirmation prompts on prompt_toolkit v3.0.52+ where run_in_terminal returns a coroutine.

Closes #22958

Changed files

  • cli.py (modified, +2/-1)
  • scripts/release.py (modified, +1/-0)

Code Example

/clear — destroys conversation state

    This clears the screen and starts a new session.
    The current conversation history will be discarded.

    [1] Approve Once   — proceed this time only
    [2] Always Approve — proceed and silence this prompt permanently
    [3] Cancel         — keep current conversation

---

N/A, contains identifiable personal information due to machine setup

---



---

def _prompt_text_input(self, prompt_text: str) -> str | None:
      result = [None]
      def _ask():
          try:
              result[0] = input(prompt_text).strip() or None
          except (KeyboardInterrupt, EOFError):
              pass
      if self._app:
          from prompt_toolkit.application import run_in_terminal
          was_visible = self._status_bar_visible
          self._status_bar_visible = False
          self._app.invalidate()
          try:
              run_in_terminal(_ask)
          finally:
              self._status_bar_visible = was_visible
              self._app.invalidate()
      else:
          _ask()
      return result[0]

---

def _prompt_text_input(self, prompt_text: str) -> str | None:
      if self._app:
          from prompt_toolkit.application import run_in_terminal
          from prompt_toolkit.shortcuts import prompt as pt_prompt
          result = [None]
          def _ask():
              try:
                  raw = pt_prompt(prompt_text)
                  result[0] = raw.strip() or None
              except (KeyboardInterrupt, EOFError):
                  pass
          was_visible = self._status_bar_visible
          self._status_bar_visible = False
          self._app.invalidate()
          try:
              run_in_terminal(_ask)
          finally:
              self._status_bar_visible = was_visible
              self._app.invalidate()
          return result[0]
      # Non-TUI fallback unchanged
      try:
          return input(prompt_text).strip() or None
      except (KeyboardInterrupt, EOFError):
          return None

---

approvals:
    destructive_slash_confirm: false
RAW_BUFFERClick to expand / collapse

Bug Description

Version: 0.13.0 Platform: Linux

The destructive-slash confirmation prompt added to cli.py (_confirm_destructive_slash, around line 8362) renders correctly but its input intercept never fires. Typing the answer (1, 2, or 3) sends that character to the agent as a chat message instead of resolving the prompt.

Steps to Reproduce

  1. Start Hermes CLI in TUI mode (hermes chat).
  2. Type /clear (or /new, /reset, /undo).
  3. Confirmation prompt prints:
⚠   /clear — destroys conversation state

  This clears the screen and starts a new session.
  The current conversation history will be discarded.

  [1] Approve Once   — proceed this time only
  [2] Always Approve — proceed and silence this prompt permanently
  [3] Cancel         — keep current conversation
  1. Type 2 and press Enter.

Expected Behavior

prompt resolves, "Always Approve" path runs, approvals.destructive_slash_confirm is persisted to false.

Actual Behavior

the 2 is delivered to the agent as a user message ("2"). The prompt remains unresolved. The destructive command never runs and never cancels — it's just abandoned.

Affected Component

CLI (interactive chat)

Messaging Platform (if gateway-related)

No response

Debug Report

N/A, contains identifiable personal information due to machine setup

Operating System

Pop!OS 24.04

Python Version

3.11.15

Hermes Version

0.13.0

Additional Logs / Traceback (optional)

Root Cause Analysis (optional)

Most likely cause: _prompt_text_input (cli.py:5860) calls Python's built-in input() from inside prompt_toolkit.application.run_in_terminal:

def _prompt_text_input(self, prompt_text: str) -> str | None:
    result = [None]
    def _ask():
        try:
            result[0] = input(prompt_text).strip() or None
        except (KeyboardInterrupt, EOFError):
            pass
    if self._app:
        from prompt_toolkit.application import run_in_terminal
        was_visible = self._status_bar_visible
        self._status_bar_visible = False
        self._app.invalidate()
        try:
            run_in_terminal(_ask)
        finally:
            self._status_bar_visible = was_visible
            self._app.invalidate()
    else:
        _ask()
    return result[0]

When the TUI Application is active, run_in_terminal is supposed to suspend the app's screen and hand stdin back to the wrapped callable. In this code path that handoff doesn't appear to happen — the prompt prints, but keystrokes continue to be consumed by prompt_toolkit's main input handler and end up in _pending_input, which process_loop then forwards to the agent as a normal user message. The result [None] is never populated, the prompt returns None, and _confirm_destructive_slash treats that as "cancelled (no input)", except the cancel message is also never printed because the function is sitting on input() that will never return.

May be related — same session as a broken-prompt repro shows these in ~/.hermes/logs/errors.log:

WARNING cli: process_loop unhandled error (msg may be lost): There is no current event loop in thread 'Thread-3 (process_loop)'.

process_loop is the thread that drains _pending_input, so an asyncio-context bug there could plausibly interfere with run_in_terminal's stdin handoff. Not confirmed, just a correlation worth checking.

Proposed Fix (optional)

Replace the built-in input() in _prompt_text_input with prompt_toolkit's native prompt(), which cooperates with the active Application instead of competing for stdin:

def _prompt_text_input(self, prompt_text: str) -> str | None:
    if self._app:
        from prompt_toolkit.application import run_in_terminal
        from prompt_toolkit.shortcuts import prompt as pt_prompt
        result = [None]
        def _ask():
            try:
                raw = pt_prompt(prompt_text)
                result[0] = raw.strip() or None
            except (KeyboardInterrupt, EOFError):
                pass
        was_visible = self._status_bar_visible
        self._status_bar_visible = False
        self._app.invalidate()
        try:
            run_in_terminal(_ask)
        finally:
            self._status_bar_visible = was_visible
            self._app.invalidate()
        return result[0]
    # Non-TUI fallback unchanged
    try:
        return input(prompt_text).strip() or None
    except (KeyboardInterrupt, EOFError):
        return None

A more invasive but cleaner alternative: render the choice as a prompt_toolkit modal, similar to the /model picker at cli.py:5884 (_open_model_picker). That reuses the existing input-capture machinery and gets you arrow-key navigation for free, but it's a bigger change.

Workaround for affected users

Add to ~/.hermes/config.yaml:

  approvals:
    destructive_slash_confirm: false

This bypasses the prompt entirely (cli.py:8391–8392). Confirmed working on 0.13.0.

Are you willing to submit a PR for this?

  • I'd like to fix this myself and submit a PR

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