hermes - 💡(How to fix) Fix Bug: Background process notifications are treated as user messages, polluting conversation history [1 pull requests]

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…

Background process completion notifications from drain_notifications() and system-generated messages (Goal continuation, Memory injection) are injected directly into _pending_input as plain strings. The process_loop consumer treats every queue item identically — it calls chat() on each one, sending system notifications to the LLM as if they were user messages. This produces phantom turns in the conversation history where the LLM analyzes or responds to background task logs.

Root Cause

_pending_input is a single untyped FIFO queue that carries multiple message types from different sources, but process_loop performs no type discrimination when consuming:

SourceCode LocationMessage Type
handle_entercli.py:12866User input
drain_notifications()cli.py:14629System notification
Goal continuationcli.py:L9301, L9492System prompt
Re-queue (interrupt)cli.py:L12316User input
/steer leftovercli.py:L12345System prompt
Voice transcriptcli.py:L10899User input

All sources put() raw strings into the same queue. The consumer at cli.py:L14460 blindly takes whatever comes out and passes it to chat().

This is a semantic queue pollution bug — the queue contract assumes "user input only" but multiple subsystems write system messages to it without any tagging.

Fix Action

Fixed

Code Example

# cli.py drain_notifications loop:
for _evt, _synth in process_registry.drain_notifications():
    if isinstance(_synth, str) and _synth.startswith("[IMPORTANT:"):
        wrapped = (
            "[SYSTEM NOTIFICATION - Background task status]\n"
            f"{_synth}\n"
            "[END SYSTEM NOTIFICATION]\n"
            "(Note: This is an automated system event, not a user message.)"
        )
        self._pending_input.put(wrapped)
    else:
        self._pending_input.put(_synth)
RAW_BUFFERClick to expand / collapse

Bug: Background process notifications are treated as user messages, polluting conversation history

Summary

Background process completion notifications from drain_notifications() and system-generated messages (Goal continuation, Memory injection) are injected directly into _pending_input as plain strings. The process_loop consumer treats every queue item identically — it calls chat() on each one, sending system notifications to the LLM as if they were user messages. This produces phantom turns in the conversation history where the LLM analyzes or responds to background task logs.

What Happens (User-facing symptoms)

Scenario A — Background notification appears as a user message:

You run a background task. When it finishes, instead of just seeing a brief CLI status line, your chat history shows something like:

You: [IMPORTANT: Background process proc_abc123 completed (exit code 0). Command: python train.py Output: training finished]

Hermes: Great! I see your training completed successfully. The exit code was 0...

Then Hermes keeps chatting about the log output you never asked about.

Scenario B — Multiple "old messages" appear consecutively:

Two or three background tasks complete in quick succession. Suddenly your conversation shows 2-3 phantom turns where Hermes analyzes each system notification in detail, consuming context window budget on content you didn't request. This is what the reporter described as "连续出现两条老消息".

Scenario C — Interrupt causes token explosion:

While Hermes is running, you press Enter with a new question. Before you get a response, you press Enter again (maybe retyping or adding follow-up). All those intermediate inputs get concatenated into one giant prompt with no type checking, wasting tokens and confusing the model.

Reproduction Steps

  1. Start Hermes CLI
  2. Run a long-running background task: terminal(background=true, notify_on_complete=true, script="sleep 10 && echo done")
  3. Wait for the task to complete while the CLI is idle
  4. Observe: The completion notification text ([IMPORTANT: Background process proc_xxx completed...]) is queued into _pending_input, dequeued by process_loop, and sent to the LLM as a user message — producing a full turn where Hermes analyzes the system log
  5. (Optional) Repeat with multiple simultaneous background tasks — observe consecutive phantom turns

Root Cause

_pending_input is a single untyped FIFO queue that carries multiple message types from different sources, but process_loop performs no type discrimination when consuming:

SourceCode LocationMessage Type
handle_entercli.py:12866User input
drain_notifications()cli.py:14629System notification
Goal continuationcli.py:L9301, L9492System prompt
Re-queue (interrupt)cli.py:L12316User input
/steer leftovercli.py:L12345System prompt
Voice transcriptcli.py:L10899User input

All sources put() raw strings into the same queue. The consumer at cli.py:L14460 blindly takes whatever comes out and passes it to chat().

This is a semantic queue pollution bug — the queue contract assumes "user input only" but multiple subsystems write system messages to it without any tagging.

Impact

  • Conversation history pollution: System logs appear as user messages in the chat, producing LLM responses to technical output the user never asked about
  • "Old messages reappearing": When multiple background tasks complete in quick succession, consecutive phantom turns appear
  • Token waste: Each phantom turn consumes context window budget
  • User confusion: The CLI shows the LLM responding to system events as if they were user questions
  • No upstream fix yet: Verified against upstream HEAD (NousResearch/hermes-agent) — the affected code is identical

Proposed Fix (Prompt-level)

The minimal non-breaking fix is to wrap system notifications with structured tags before queuing, so the LLM can distinguish them from user messages:

# cli.py drain_notifications loop:
for _evt, _synth in process_registry.drain_notifications():
    if isinstance(_synth, str) and _synth.startswith("[IMPORTANT:"):
        wrapped = (
            "[SYSTEM NOTIFICATION - Background task status]\n"
            f"{_synth}\n"
            "[END SYSTEM NOTIFICATION]\n"
            "(Note: This is an automated system event, not a user message.)"
        )
        self._pending_input.put(wrapped)
    else:
        self._pending_input.put(_synth)

Plus hardening the interrupt re-queue logic with type safety and a character budget to prevent token explosion.

Proposed Fix (Architectural)

For a proper long-term solution, split _pending_input into typed queues:

  • _user_input — user-entered messages
  • _system_notifications — background task completions
  • _system_prompts — goal continuation, steer leftovers

The consumer would prioritize _user_input > _system_prompts > _system_notifications, avoiding the race condition entirely.

Environment

  • Hermes Agent: v0.15.1 (2026.5.29) and later
  • Affected file: cli.py (process_loop + drain_notifications + interrupt re-queue)
  • Upstream: NousResearch/hermes-agent

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 - 💡(How to fix) Fix Bug: Background process notifications are treated as user messages, polluting conversation history [1 pull requests]