Bug Description
The CLI pet animation daemon thread (_pet_anim_loop in cli.py) crashes with:
RuntimeError: There is no current event loop in thread 'Thread-6 (_pet_anim_loop)'.
This happens because the daemon thread calls app.invalidate() and load_config() directly, but it has no asyncio event loop. The error is especially visible on macOS with uv-managed Python 3.12, but the underlying thread-safety issue is platform-independent.
Additionally, _pet_start_anim() spawns the daemon thread unconditionally before every CLI session, even when display.pet.enabled is false (the default). Users without a pet still get the log noise and event-loop races.
Steps to Reproduce
- Ensure no pet is enabled (default
display.pet.enabled: false, or no display.pet section at all).
- Run
hermes chat or hermes.
- Send any message, or resize the terminal during a response.
- Check
~/.hermes/logs/errors.log for RuntimeError: There is no current event loop in thread 'Thread-6 (_pet_anim_loop)'.
Expected Behavior
- No
RuntimeError from the pet animation thread.
- The daemon thread should not start at all when no pet is enabled.
- When a pet is enabled, redraws should be scheduled on the main event loop thread-safely.
Actual Behavior
errors.log fills with:
RuntimeError: There is no current event loop in thread 'Thread-6 (_pet_anim_loop)'.
The TUI stays alive because the exception is swallowed, but the log noise indicates a real thread-safety bug.
Affected Component
CLI (interactive chat)
Operating System
macOS 15.x / macOS-27.0-arm64 (also affects other platforms)
Python Version
3.12.13
Hermes Version
0.18.2 (2026.7.7.2) · upstream b8880f1
Root Cause Analysis
In cli.py:
_pet_anim_loop() runs as a threading.Thread(..., daemon=True).
- It calls
_pet_resolve_config() → load_config() → yaml.load() every 2.5 seconds.
- It calls
app.invalidate() directly from the daemon thread.
- Because the thread has no asyncio event loop, any async cleanup or prompt_toolkit exception handling (e.g. during terminal resize recovery /
_replay_output_history) crashes when ensure_future(in_term()) is called.
The same class of bug was previously fixed for process_loop in #19285 (edbbc96b5) by using get_running_loop() and loop.call_soon_threadsafe().
Proposed Fix
- In
_pet_anim_loop, schedule app.invalidate() via app.loop.call_soon_threadsafe(app.invalidate).
- In
_pet_start_anim, return early if _pet_enabled is False so the thread is never spawned when the feature is disabled.
I have a branch ready with this fix plus regression tests.
Bug Description
The CLI pet animation daemon thread (
_pet_anim_loopincli.py) crashes with:This happens because the daemon thread calls
app.invalidate()andload_config()directly, but it has no asyncio event loop. The error is especially visible on macOS with uv-managed Python 3.12, but the underlying thread-safety issue is platform-independent.Additionally,
_pet_start_anim()spawns the daemon thread unconditionally before every CLI session, even whendisplay.pet.enabledisfalse(the default). Users without a pet still get the log noise and event-loop races.Steps to Reproduce
display.pet.enabled: false, or nodisplay.petsection at all).hermes chatorhermes.~/.hermes/logs/errors.logforRuntimeError: There is no current event loop in thread 'Thread-6 (_pet_anim_loop)'.Expected Behavior
RuntimeErrorfrom the pet animation thread.Actual Behavior
errors.logfills with:The TUI stays alive because the exception is swallowed, but the log noise indicates a real thread-safety bug.
Affected Component
CLI (interactive chat)
Operating System
macOS 15.x / macOS-27.0-arm64 (also affects other platforms)
Python Version
3.12.13
Hermes Version
0.18.2 (2026.7.7.2) · upstream b8880f1
Root Cause Analysis
In
cli.py:_pet_anim_loop()runs as athreading.Thread(..., daemon=True)._pet_resolve_config()→load_config()→yaml.load()every 2.5 seconds.app.invalidate()directly from the daemon thread._replay_output_history) crashes whenensure_future(in_term())is called.The same class of bug was previously fixed for
process_loopin #19285 (edbbc96b5) by usingget_running_loop()andloop.call_soon_threadsafe().Proposed Fix
_pet_anim_loop, scheduleapp.invalidate()viaapp.loop.call_soon_threadsafe(app.invalidate)._pet_start_anim, return early if_pet_enabledisFalseso the thread is never spawned when the feature is disabled.I have a branch ready with this fix plus regression tests.