hermes - 💡(How to fix) Fix fix(cli): /new, /clear, /reset freeze permanently on Linux after fae815a (PR #30773)

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…

Error Message

Obtain the app's event loop for cross-thread scheduling.

try: _loop = self._app.loop except Exception: _loop = None

in_main_thread = threading.current_thread() is threading.main_thread()

if in_main_thread or _loop is None: self._capture_modal_input_snapshot() # ... set state, invalidate ... else: _ready = threading.Event() def _setup_then_signal(): self._capture_modal_input_snapshot() # ... set state, invalidate ... _ready.set() _loop.call_soon_threadsafe(_setup_then_signal) _ready.wait(timeout=5)

daemon thread blocks here — safe from any thread

result = response_queue.get(timeout=1)

teardown also via call_soon_threadsafe if off main thread

Root Cause

process_loop is a daemon thread. When it calls _prompt_text_input_modal, the non-main-thread guard at line 7186 fires and falls back to _prompt_text_input, which calls input("Choice [1/2/3]: ") directly.

But prompt_toolkit's event loop on the main thread already owns stdin via select(). The two contend for the same fd — input() blocks forever waiting for bytes that never arrive, the confirmation dialog is never rendered, and since process_loop handles all keystrokes, the entire CLI becomes unresponsive.

Diagnosed live with py-spy dump --pid <PID>:

Thread 2210 (idle): "Thread-3 (process_loop)"
    _ask (cli.py:7099)                      ← blocked in input()
    _prompt_text_input (cli.py:7124)
    _prompt_text_input_modal (cli.py:7187)
    _confirm_destructive_slash (cli.py:10222)
    process_command (cli.py:8442)
    process_loop (cli.py:14351)

Fix Action

Fix

Remove the non-main-thread bailout. Schedule _capture_modal_input_snapshot and _restore_modal_input_snapshot onto the main event loop via loop.call_soon_threadsafe when called from a daemon thread. The daemon thread then blocks on response_queue.get() as intended.

# Obtain the app's event loop for cross-thread scheduling.
try:
    _loop = self._app.loop
except Exception:
    _loop = None

in_main_thread = threading.current_thread() is threading.main_thread()

if in_main_thread or _loop is None:
    self._capture_modal_input_snapshot()
    # ... set state, invalidate ...
else:
    _ready = threading.Event()
    def _setup_then_signal():
        self._capture_modal_input_snapshot()
        # ... set state, invalidate ...
        _ready.set()
    _loop.call_soon_threadsafe(_setup_then_signal)
    _ready.wait(timeout=5)

# daemon thread blocks here — safe from any thread
result = response_queue.get(timeout=1)

# teardown also via call_soon_threadsafe if off main thread

Workaround

Disable the confirmation dialog entirely:

hermes config set approvals.destructive_slash_confirm false

Code Example

Thread 2210 (idle): "Thread-3 (process_loop)"
    _ask (cli.py:7099)                      ← blocked in input()
    _prompt_text_input (cli.py:7124)
    _prompt_text_input_modal (cli.py:7187)
    _confirm_destructive_slash (cli.py:10222)
    process_command (cli.py:8442)
    process_loop (cli.py:14351)

---

# Obtain the app's event loop for cross-thread scheduling.
try:
    _loop = self._app.loop
except Exception:
    _loop = None

in_main_thread = threading.current_thread() is threading.main_thread()

if in_main_thread or _loop is None:
    self._capture_modal_input_snapshot()
    # ... set state, invalidate ...
else:
    _ready = threading.Event()
    def _setup_then_signal():
        self._capture_modal_input_snapshot()
        # ... set state, invalidate ...
        _ready.set()
    _loop.call_soon_threadsafe(_setup_then_signal)
    _ready.wait(timeout=5)

# daemon thread blocks here — safe from any thread
result = response_queue.get(timeout=1)

# teardown also via call_soon_threadsafe if off main thread

---

hermes config set approvals.destructive_slash_confirm false
RAW_BUFFERClick to expand / collapse

Bug Description

Commit fae815adc (PR #30773, merged 2026-05-25) added a non-main-thread guard to _prompt_text_input_modal that falls back to _prompt_text_input (raw input()) when called from the process_loop daemon thread. This causes a permanent freeze on Linux when /new, /clear, or /reset is typed — the only recovery is killing the process.

Steps to Reproduce

  1. Run hermes chat on Linux
  2. Have a conversation (so there is session state)
  3. Type /new and press Enter
  4. CLI freezes — no confirmation dialog appears, no input is accepted

Root Cause

process_loop is a daemon thread. When it calls _prompt_text_input_modal, the non-main-thread guard at line 7186 fires and falls back to _prompt_text_input, which calls input("Choice [1/2/3]: ") directly.

But prompt_toolkit's event loop on the main thread already owns stdin via select(). The two contend for the same fd — input() blocks forever waiting for bytes that never arrive, the confirmation dialog is never rendered, and since process_loop handles all keystrokes, the entire CLI becomes unresponsive.

Diagnosed live with py-spy dump --pid <PID>:

Thread 2210 (idle): "Thread-3 (process_loop)"
    _ask (cli.py:7099)                      ← blocked in input()
    _prompt_text_input (cli.py:7124)
    _prompt_text_input_modal (cli.py:7187)
    _confirm_destructive_slash (cli.py:10222)
    process_command (cli.py:8442)
    process_loop (cli.py:14351)

Why the Fix Was Too Broad

The sys.platform == "win32" guard added in the same commit was already sufficient for Windows. The additional threading.current_thread() is not threading.main_thread() guard was unnecessary and introduces this regression on all non-Windows platforms.

The queue-based modal path works correctly from any thread: the daemon thread sets _slash_confirm_state and blocks on response_queue.get(); the main thread's event loop renders the dialog and fires key bindings which call _submit_slash_confirm_response. The only non-thread-safe operations are _capture_modal_input_snapshot and _restore_modal_input_snapshot which touch app.current_buffer directly.

Fix

Remove the non-main-thread bailout. Schedule _capture_modal_input_snapshot and _restore_modal_input_snapshot onto the main event loop via loop.call_soon_threadsafe when called from a daemon thread. The daemon thread then blocks on response_queue.get() as intended.

# Obtain the app's event loop for cross-thread scheduling.
try:
    _loop = self._app.loop
except Exception:
    _loop = None

in_main_thread = threading.current_thread() is threading.main_thread()

if in_main_thread or _loop is None:
    self._capture_modal_input_snapshot()
    # ... set state, invalidate ...
else:
    _ready = threading.Event()
    def _setup_then_signal():
        self._capture_modal_input_snapshot()
        # ... set state, invalidate ...
        _ready.set()
    _loop.call_soon_threadsafe(_setup_then_signal)
    _ready.wait(timeout=5)

# daemon thread blocks here — safe from any thread
result = response_queue.get(timeout=1)

# teardown also via call_soon_threadsafe if off main thread

Environment

  • OS: Linux (Slackware-current)
  • Python: 3.11.15
  • Introduced by: fae815adc / PR #30773
  • References: #30768

Workaround

Disable the confirmation dialog entirely:

hermes config set approvals.destructive_slash_confirm false

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