NousResearch/hermes-agent
Telegram DM topic thread_id lost after session split
Cluster 27166 · gateway. Data as of
Nothing open on GitHub. Fix merged in #28524 on 19 May 2026. Check it covers your case.
The short answer
0open issues
0open PRs
3items grouped
- Known?
- Yes. 3 related items grouped here, 1 issue already closed.
- Fix event
- A fix was merged to main in PR #28524. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
unresolved. That is separate from the fix event. - Still open
- Nothing open on GitHub.
A bug where Telegram DM topic responses are routed to the wrong thread after a session split, and related fix attempts targeting thread_id restoration in the gateway.
Open issues0
Open PRs0
Fixes on record
-
#28524 fix(gateway): restore Telegram DM topic thread_id after session split (closes #27166)
files:
gateway/run.py,hermes_state.py,scripts/release.py,tests/gateway/test_telegram_topic_mode.pyShow the diff
-
gateway/run.py@@ -16544,6 +16544,37 @@ def _approval_notify_sync(approval_data: dict) -> None: entry.session_id = agent.session_id self.session_store._save() + # If this is a Telegram DM and source.thread_id was lost during + # the session split (synthetic / recovered event), restore it + # from the binding so _thread_metadata_for_source produces the + # correct message_thread_id instead of routing to the General + # thread. Failure here is non-fatal — we log and continue; + # worst case the message lands in General, which is the + # pre-fix behaviour. + if ( + getattr(source, "platform", None) == Platform.TELEGRAM + and getattr(source, "chat_type", None) == "dm" + and getattr(source, "thread_id", None) is None + and self._session_db is not None + ): + try: + _binding = self._session_db.get_telegram_topic_binding_by_session( + session_id=agent.session_id, + ) + if _binding and _binding.get("thread_id"): + source.thread_id = str(_binding["thread_id"]) + logger.debug( + "Restored source.thread_id=%s from binding after session split %s → %s", + source.thread_id, + session_id, + agent.session_id, + ) + except Exception: + logger.debug( + "Failed to restore thread_id from binding after session split", + exc_info=True, + ) + effective_session_id = getattr(agent, 'session_id', session_id) if agent else session_id # When compression created a new session, the messages list was -
hermes_state.py@@ -2852,6 +2852,30 @@ def list_telegram_topic_bindings_for_chat( return [] return [dict(row) for row in rows] + def get_telegram_topic_binding_by_session( + self, + *, + session_id: str, + ) -> Optional[Dict[str, Any]]: + """Return the Telegram DM topic binding for a given session_id, if present. + + Uses the UNIQUE INDEX on telegram_dm_topic_bindings(session_id) for an + efficient reverse lookup. Returns None when the session has no binding or + the table does not exist yet. + """ + with self._lock: + try: + row = self._conn.execute( + """ + SELECT * FROM telegram_dm_topic_bindings + WHERE session_id = ? + """, + (str(session_id),), + ).fetchone() + except sqlite3.OperationalError: + return None + return dict(row) if row else None + def bind_telegram_topic( self, *, -
scripts/release.py@@ -182,6 +182,7 @@ "[redacted:email]": "kiranvk-2011", # PR #24815 salvage (image documents → vision) "[redacted:email]": "rak135", # PR #25960 salvage (Windows /restart) "[redacted:email]": "B0Tch1", # PR #27634 salvage (disable_topic_auto_rename) + "[redacted:email]": "jackjin1997", # PR #27239 salvage (restore DM topic thread_id after split) "[email protected]": "eliteworkstation94-ai", # PR #28157 salvage (group reply session splits) "[redacted:email]": "hllqkb", "[email protected]": "Bongulielmi", -
tests/gateway/test_telegram_topic_mode.py@@ -1229,6 +1229,100 @@ def test_list_telegram_topic_bindings_for_chat_no_table(tmp_path): assert tables == set() +# --------------------------------------------------------------------------- +# Tests for get_telegram_topic_binding_by_session (issue #27166) +# --------------------------------------------------------------------------- +def test_get_telegram_topic_binding_by_session_returns_binding(tmp_path): + """Reverse lookup by session_id returns the binding row.""" + db = SessionDB(db_path=tmp_path / "state.db") + db.enable_telegram_topic_mode(chat_id="208214988", user_id="208214988") + db.create_session(session_id="sess-27166", source="telegram", user_id="208214988") + db.bind_telegram_topic( + chat_id="208214988", + thread_id="17585", + user_id="208214988", + session_key="agent:main:telegram:dm:208214988:17585", + session_id="sess-27166", + ) + + binding = db.get_telegram_topic_binding_by_session(session_id="sess-27166") + + assert binding is not None + assert binding["chat_id"] == "208214988" + assert binding["thread_id"] == "17585" + assert binding["session_id"] == "sess-27166" + + +def test_get_telegram_topic_binding_by_session_returns_none_for_unknown(tmp_path): + """Returns None when no binding exists for the given session_id.""" + db = SessionDB(db_path=tmp_path / "state.db") + db.apply_telegram_topic_migration() + + result = db.get_telegram_topic_binding_by_session(session_id="nonexistent-sess") + + assert result is None + + +# --------------------------------------------------------------------------- +# Test for session-split thread_id recovery (issue #27166) +# --------------------------------------------------------------------------- +def test_session_split_restores_source_thread_id_from_binding(tmp_path): + """After a session split, source.thread_id is restored from the binding. + + Simulates the case where context compression creates a new session_id and + source.thread_id is None (synthetic/recovered event). The recovery block + must look up the binding by the new session_id and restore thread_id on + source so that _thread_metadata_for_source returns the correct thread. + """ + from gateway.run import GatewayRunner + from gateway.config import Platform + + db = SessionDB(db_path=tmp_path / "state.db") + db.enable_telegram_topic_mode(chat_id="208214988", user_id="208214988") + db.create_session(session_id="sess-split-new", source="telegram", user_id="208214988") + db.bind_telegram_topic( + chat_id="208214988", + thread_id="17585", + user_id="208214988", + session_key="agent:main:telegram:dm:208214988:17585", + session_id="sess-split-new", + ) + + runner = object.__new__(GatewayRunner) + runner._session_db = db + + # Build a source that looks like it came from a synthetic/recovered event: + # platform and chat_type match a Telegram DM, but thread_id is None. + source = _make_source(thread_id=None) + assert source.platform == Platform.TELEGRAM + assert source.chat_type == "dm" + assert source.thread_id is None + + # Simulate the session-split recovery block logic directly. + if ( + getattr(source, "platform", None) == Platform.TELEGRAM + and getattr(source, "chat_type", None) == "dm" + and getattr(source, "thread_id", None) is None + and runner._session_db is not None + ): + try: + _binding = runner._session_db.get_telegram_topic_binding_by_session( + session_id="sess-split-new", + ) + if _binding and _binding.get("thread_id"): + source.thread_id = str(_binding["thread_id"]) + except Exception: + pass + + assert source.thread_id == "17585", ( + "thread_id must be restored from the binding after session split" + ) + # Confirm _thread_metadata_for_source now returns non-None. + runner.config = _make_runner(session_db=db).config + runner.adapters = _make_runner(session_db=db).adapters + meta = GatewayRunner._thread_metadata_for_source(runner, source) + assert meta is not None + assert meta["thread_id"] == "17585"
-
1 PR closed without merging
From the close comments: merged via another change 1
-
#27239 fix(gateway): restore Telegram DM topic thread_id after session split · merged via another change →
#28524
Closed issues · 1
- Closed#27166[Bug] Telegram DM topic response routed to All Messages after session split
How this cluster was grouped
-
Link evidence (
structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference. -
Pairwise link density
0.67: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the LabAt a glance
- Subsystem
- gateway
- Merged PRs
- #28524
- Closed issues
- 1
- Attempts
- 1 PR closed without merging
- Fix link checked
- 19 May 2026