Skip to content

[Bug]: pet animation daemon thread crashes with "There is no current event loop" #62288

Description

@sabadia

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

  1. Ensure no pet is enabled (default display.pet.enabled: false, or no display.pet section at all).
  2. Run hermes chat or hermes.
  3. Send any message, or resize the terminal during a response.
  4. 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

  1. In _pet_anim_loop, schedule app.invalidate() via app.loop.call_soon_threadsafe(app.invalidate).
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P3Low — cosmetic, nice to havecomp/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