Skip to content

Telegram DM topic bindings are not pruned when Bot API returns Thread not found #31501

@PintaAI

Description

@PintaAI

Bug Description

When Telegram DM topic mode is enabled, Hermes stores topic/session mappings in state.db (telegram_dm_topic_bindings). If a user manually deletes Telegram DM topics in the Telegram client, Hermes does not automatically prune the stale binding rows.

Later, when the user sends a message from a new Telegram topic, Hermes may treat the inbound thread_id as unknown and recover/redirect the session back to the stale topic binding. This causes tool progress, approvals, activity messages, and replies to appear in the wrong place or fall back to the main DM.

Observed Logs

After deleting Telegram topics manually, gateway logs showed:

WARNING gateway.platforms.telegram: [Telegram] Thread 15344 not found, retrying once with same thread_id
WARNING gateway.platforms.telegram: [Telegram] Thread 15344 not found, retrying without message_thread_id

Then later, after a new topic was created:

INFO gateway.run: telegram topic recovery: chat=5595856929 user=5595856929 '15418' -> 15287

The stale binding in SQLite was:

telegram_dm_topic_bindings:
chat_id=5595856929
thread_id=15287
user_id=5595856929
session_key=agent:main:telegram:dm:5595856929:15287

Deleting that row manually fixed the recovery behavior:

DELETE FROM telegram_dm_topic_bindings
WHERE chat_id = '5595856929' OR user_id = '5595856929';

Root Cause

The recovery logic in gateway/run.py::_recover_telegram_topic_thread_id() intentionally rewrites an unknown/missing/lobby thread_id to the user's most recent known topic binding:

bindings = session_db.list_telegram_topic_bindings_for_chat(...)
...
for b in bindings:  # newest-first
    if str(b.get("user_id") or "") == user_id:
        recovered = str(b.get("thread_id") or "")
        if recovered and recovered != inbound:
            return recovered

This works for cross-topic replies, but it becomes harmful when the stored binding points to a Telegram topic that was deleted externally.

The Telegram adapter already observes the deletion indirectly when Bot API returns “Thread not found”, but the stale row remains in telegram_dm_topic_bindings.

Expected Behavior

When Telegram send fails with “Thread not found” / invalid message_thread_id, Hermes should prune the corresponding stale topic binding from local state, e.g.:

DELETE FROM telegram_dm_topic_bindings
WHERE chat_id = ? AND thread_id = ?;

It should also consider removing the stale entry from channel_directory.json if present.

After pruning, _recover_telegram_topic_thread_id() should no longer redirect future messages to the deleted topic.

Actual Behavior

Hermes falls back to sending without message_thread_id, but the stale DB binding remains. Future inbound messages from a new topic can be recovered back to the stale thread.

Proposed Fix

  1. In Telegram send fallback handling, when Bot API returns “Thread not found” or equivalent invalid-thread error:

    • delete telegram_dm_topic_bindings row for (chat_id, thread_id)
    • optionally remove matching channel directory entry
    • log the pruning action
  2. Add a helper on SessionDB, e.g.:

    • delete_telegram_topic_binding(chat_id, thread_id)
  3. Add tests:

    • seed telegram_dm_topic_bindings with a stale topic
    • simulate Telegram send error “Thread not found”
    • assert the binding row is deleted
    • assert subsequent recovery no longer returns the stale thread
  4. Optional: expose a manual command:

    • /topic prune
    • /topic clear
      for users who delete topics manually.

Environment

  • Hermes Agent gateway running as systemd service
  • Platform: Telegram DM topic mode
  • OS: Linux
  • Relevant files:
    • state.db
    • table: telegram_dm_topic_bindings
    • gateway/run.py::_recover_telegram_topic_thread_id()
    • gateway/platforms/telegram.py send fallback path

Workaround

Manually delete stale rows from SQLite:

import sqlite3

conn = sqlite3.connect("/root/.hermes/state.db")
conn.execute(
    "DELETE FROM telegram_dm_topic_bindings WHERE chat_id=? OR user_id=?",
    ("<telegram_chat_id>", "<telegram_user_id>"),
)
conn.commit()
conn.close()

Then restart the gateway.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Medium — degraded but workaround existscomp/gatewayGateway runner, session dispatch, deliveryplatform/telegramTelegram bot adaptertype/bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions