Skip to content

[Bug]: Telegram duplicate replies when interrupted session sends stale response before processing pending message #8221

Description

@medqhuang

Bug Description

When a new Telegram message arrives for an already-active session, Hermes marks the running session as interrupted and queues the new message in _pending_messages. However, the running task still sends its old response before checking whether it has been interrupted. After that, the queued message is processed and produces another response.

This causes duplicate or near-duplicate visible replies, especially when the two prompts are closely related.

Telegram flood control makes the symptom worse because the stale response may be delayed and arrive noticeably later, making it look like Hermes repeated itself.

Steps to Reproduce

  1. Start a Telegram conversation with Hermes in a single DM or single session thread.
  2. Send a prompt that takes some time to process, especially one likely to produce a long answer.
  3. Before Hermes has fully finished sending the previous reply, send another related prompt in the same session.
  4. Observe that Hermes may first send a stale reply from the earlier turn and then send a second reply for the newer turn.
  5. Under Telegram flood control, the stale reply may arrive late, making the two responses look like duplicate answers.

Expected Behavior

If a running session has been interrupted by a newer incoming message, the stale response from the older turn should not be sent. Hermes should suppress the old reply and continue with the queued newer message only.

Actual Behavior

The interrupted older turn still sends its response, and then the queued newer message is processed and also sends a response. This creates duplicate or highly overlapping visible replies.

Affected Component

Gateway (Telegram/Discord/Slack/WhatsApp)

Messaging Platform (if gateway-related)

Telegram

Operating System

Ubuntu 24.04.4 LTS (Noble Numbat) Kernel: Linux 6.17.0-1007-aws

Python Version

Python 3.11.15

Hermes Version

0.8.0

Relevant Logs / Traceback

Supporting Evidence From Logs

Observed in /home/ubuntu/.hermes/logs/agent.log for Telegram DM chat 5769327191:

- 06:42:57 inbound: Hermes Live 重复回答
- 06:43:09 response ready: 514 chars
- 06:44:08 Sending response (111 chars)
- 06:44:13 inbound again: Hermes Live 重复回答

Also earlier in the same DM session:

- 06:17:52 response ready: 2658 chars
- 06:19:36 new inbound arrives before the prior turn has fully cleared
- 06:22:06 another response ready: 2871 chars
- 06:29:16 another inbound arrives while the session is still busy

This matches the active-session interrupt path rather than duplicate execution by two gateway processes.

Flood Control Interaction

Telegram flood control is present in the same log window, e.g.:

- Telegram flood control on send (attempt 1/3), retrying in ...
- Telegram flood control, waiting ...

Flood control is not the primary bug, but it amplifies the user-visible symptom by delaying stale sends.

Root Cause Analysis (optional)

The interrupt path is incomplete:

  • gateway/platforms/base.py:1507-1559
    • if a session is active, a new incoming message is stored in _pending_messages[session_key]
    • the interrupt flag is set via self._active_sessions[session_key].set()
  • gateway/platforms/base.py:1628-1718
    • _process_message_background() awaits self._message_handler(event)
    • then immediately sends the response with _send_with_retry()
    • there is no check for interrupt_event.is_set() / pending replacement before sending
  • gateway/platforms/base.py:1831-1845
    • only after sending does the code pop _pending_messages and process the queued event

So the stale response from the interrupted turn is still delivered, followed by the response for the newer turn.

Why this is the definitive cause

The current code path deterministically allows this sequence:

  1. message A starts processing
  2. message B arrives while the same session is active
  3. B is queued and interrupt flag is set
  4. A still sends its response
  5. queued B is then processed and also sends a response

No duplicate gateway instance is required, and no duplicate inbound delivery is required.

Proposed Fix (optional)

Suggested Fix

In gateway/platforms/base.py, after response = await self._message_handler(event) and before any send call:

  • check whether interrupt_event.is_set() or whether session_key in self._pending_messages
  • if true, skip sending the stale response from the old turn
  • proceed directly to the queued pending event

Possible patch shape:

response = await self._message_handler(event)

if interrupt_event.is_set() or session_key in self._pending_messages:
logger.info("[%s] Suppressing stale response for interrupted session %s", self.name, session_key)
response = None

A more careful version should distinguish true cancel/replacement from benign queued photo-burst behavior.

Are you willing to submit a PR for this?

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    type/bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions