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
-
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
-
Add a helper on SessionDB, e.g.:
delete_telegram_topic_binding(chat_id, thread_id)
-
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
-
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.
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_idas 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:
Then later, after a new topic was created:
The stale binding in SQLite was:
Deleting that row manually fixed the recovery behavior:
Root Cause
The recovery logic in
gateway/run.py::_recover_telegram_topic_thread_id()intentionally rewrites an unknown/missing/lobbythread_idto the user's most recent known topic binding: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.:It should also consider removing the stale entry from
channel_directory.jsonif 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
In Telegram send fallback handling, when Bot API returns “Thread not found” or equivalent invalid-thread error:
telegram_dm_topic_bindingsrow for(chat_id, thread_id)Add a helper on
SessionDB, e.g.:delete_telegram_topic_binding(chat_id, thread_id)Add tests:
telegram_dm_topic_bindingswith a stale topicOptional: expose a manual command:
/topic prune/topic clearfor users who delete topics manually.
Environment
state.dbtelegram_dm_topic_bindingsgateway/run.py::_recover_telegram_topic_thread_id()gateway/platforms/telegram.pysend fallback pathWorkaround
Manually delete stale rows from SQLite:
Then restart the gateway.