{"repo":"NousResearch/hermes-agent","public_id":"24409","display_label":"Telegram topic routing fixes for replies and cron","summary":"A bug report and several fixes addressing incorrect message routing in Telegram, covering DM-topic reply threading and cron delivery routing to dedicated topics via thread ID configuration.","subsystem":null,"counts":{"members":3,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":1},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":28518,"merged_at":"2026-05-19T05:36:12+00:00"},"resolution":{"state":"unresolved","by":null},"freshness":{"state":"verified","last_verified_at":"2026-05-19T05:36:12+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.6666666666666666},"fix_edges":[{"issue_number":24409,"pr_number":28518,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-05-19T05:36:12+00:00"}],"members":[{"kind":"issue","number":24409,"title":"[Bug]: Replying to telegram topic main chat, not allowed.","url":null,"state":"closed"},{"kind":"pr","number":24798,"title":"fix(cron): route Telegram cron deliveries to a dedicated topic via TELEGRAM_CRON_THREAD_ID","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#28518","number":28518}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":28518,"title":"fix(cron): route Telegram cron deliveries to dedicated topic via TELEGRAM_CRON_THREAD_ID (closes #24409)","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":[".env.example","cron/scheduler.py","tests/conftest.py","tests/cron/test_scheduler.py","website/docs/reference/environment-variables.md","website/docs/user-guide/features/cron.md","website/docs/user-guide/messaging/telegram.md"],"fix_hunks":[{"file":".env.example","hunks":["@@ -339,6 +339,7 @@ BROWSER_INACTIVITY_TIMEOUT=120\n # TELEGRAM_ALLOWED_USERS=                  # Comma-separated user IDs\n # TELEGRAM_HOME_CHANNEL=                   # Default chat for cron delivery\n # TELEGRAM_HOME_CHANNEL_NAME=              # Display name for home channel\n+# TELEGRAM_CRON_THREAD_ID=                 # Forum topic ID for cron deliveries; overrides TELEGRAM_HOME_CHANNEL_THREAD_ID for cron so replies work in topic mode\n \n # Webhook mode (optional — for cloud deployments like Fly.io/Railway)\n # Default is long polling. Setting TELEGRAM_WEBHOOK_URL switches to webhook mode."]},{"file":"cron/scheduler.py","hunks":["@@ -292,10 +292,23 @@ def _get_home_target_chat_id(platform_name: str) -> str:\n \n \n def _get_home_target_thread_id(platform_name: str) -> Optional[str]:\n-    \"\"\"Return the optional thread/topic ID for a platform home target.\"\"\"\n+    \"\"\"Return the optional thread/topic ID for a platform home target.\n+\n+    Telegram-only override: ``TELEGRAM_CRON_THREAD_ID`` takes precedence over\n+    ``TELEGRAM_HOME_CHANNEL_THREAD_ID`` for cron delivery. When topic mode is\n+    enabled, deliveries that land in the root DM (thread_id unset) end up in\n+    the system-only lobby where the user cannot reply — the gateway returns\n+    the lobby reminder and drops ``reply_to_message_id`` (#24409). Pointing\n+    cron at a dedicated topic via this env var lets replies work as expected\n+    without changing the lobby invariant.\n+    \"\"\"\n     env_var = _resolve_home_env_var(platform_name)\n     if not env_var:\n         return None\n+    if platform_name.lower() == \"telegram\":\n+        cron_thread = os.getenv(\"TELEGRAM_CRON_THREAD_ID\", \"\").strip()\n+        if cron_thread:\n+            return cron_thread\n     value = os.getenv(f\"{env_var}_THREAD_ID\", \"\").strip()\n     if not value:\n         legacy = _LEGACY_HOME_TARGET_ENV_VARS.get(env_var)"]},{"file":"tests/conftest.py","hunks":["@@ -243,6 +243,7 @@ def _looks_like_credential(name: str) -> bool:\n     \"TELEGRAM_HOME_CHANNEL\",\n     \"TELEGRAM_HOME_CHANNEL_THREAD_ID\",\n     \"TELEGRAM_HOME_CHANNEL_NAME\",\n+    \"TELEGRAM_CRON_THREAD_ID\",\n     \"DISCORD_HOME_CHANNEL\",\n     \"DISCORD_HOME_CHANNEL_THREAD_ID\",\n     \"DISCORD_HOME_CHANNEL_NAME\","]},{"file":"tests/cron/test_scheduler.py","hunks":["@@ -151,6 +151,53 @@ def test_bare_platform_delivery_preserves_home_thread_id(self, monkeypatch):\n             \"thread_id\": \"topic-7\",\n         }\n \n+    def test_telegram_cron_thread_id_overrides_home_thread_id(self, monkeypatch):\n+        \"\"\"TELEGRAM_CRON_THREAD_ID wins over TELEGRAM_HOME_CHANNEL_THREAD_ID for cron (#24409).\"\"\"\n+        monkeypatch.setenv(\"TELEGRAM_HOME_CHANNEL\", \"-1001234567890\")\n+        monkeypatch.setenv(\"TELEGRAM_HOME_CHANNEL_THREAD_ID\", \"5\")\n+        monkeypatch.setenv(\"TELEGRAM_CRON_THREAD_ID\", \"42\")\n+\n+        assert _resolve_delivery_target({\"deliver\": \"telegram\"}) == {\n+            \"platform\": \"telegram\",\n+            \"chat_id\": \"-1001234567890\",\n+            \"thread_id\": \"42\",\n+        }\n+\n+    def test_telegram_cron_thread_id_sets_thread_when_home_thread_unset(self, monkeypatch):\n+        \"\"\"TELEGRAM_CRON_THREAD_ID supplies a thread when no home thread is configured.\"\"\"\n+        monkeypatch.setenv(\"TELEGRAM_HOME_CHANNEL\", \"-1001234567890\")\n+        monkeypatch.delenv(\"TELEGRAM_HOME_CHANNEL_THREAD_ID\", raising=False)\n+        monkeypatch.setenv(\"TELEGRAM_CRON_THREAD_ID\", \"42\")\n+\n+        assert _resolve_delivery_target({\"deliver\": \"telegram\"}) == {\n+            \"platform\": \"telegram\",\n+            \"chat_id\": \"-1001234567890\",\n+            \"thread_id\": \"42\",\n+        }\n+\n+    def test_telegram_cron_thread_id_does_not_leak_to_other_platforms(self, monkeypatch):\n+        \"\"\"TELEGRAM_CRON_THREAD_ID is Telegram-only; other platforms keep their own thread resolution.\"\"\"\n+        monkeypatch.setenv(\"DISCORD_HOME_CHANNEL\", \"parent-42\")\n+        monkeypatch.setenv(\"DISCORD_HOME_CHANNEL_THREAD_ID\", \"topic-7\")\n+        monkeypatch.setenv(\"TELEGRAM_CRON_THREAD_ID\", \"42\")\n+\n+        assert _resolve_delivery_target({\"deliver\": \"discord\"}) == {\n+            \"platform\": \"discord\",\n+            \"chat_id\": \"parent-42\",\n+            \"thread_id\": \"topic-7\",\n+        }\n+\n+    def test_explicit_telegram_topic_target_overrides_cron_thread_id(self, monkeypatch):\n+        \"\"\"Explicit ``telegram:chat:thread`` targets bypass TELEGRAM_CRON_THREAD_ID.\"\"\"\n+        monkeypatch.setenv(\"TELEGRAM_CRON_THREAD_ID\", \"999\")\n+\n+        job = {\"deliver\": \"telegram:-1003724596514:17\"}\n+        assert _resolve_delivery_target(job) == {\n+            \"platform\": \"telegram\",\n+            \"chat_id\": \"-1003724596514\",\n+            \"thread_id\": \"17\",\n+        }\n+\n     def test_explicit_telegram_topic_target_with_thread_id(self):\n         \"\"\"deliver: 'telegram:chat_id:thread_id' parses correctly.\"\"\"\n         job = {"]},{"file":"website/docs/reference/environment-variables.md","hunks":["@@ -248,6 +248,7 @@ For cloud sandbox backends, persistence is filesystem-oriented. `TERMINAL_LIFETI\n | `TELEGRAM_GROUP_ALLOWED_CHATS` | Comma-separated group/forum chat IDs; any member is authorized |\n | `TELEGRAM_HOME_CHANNEL` | Default Telegram chat/channel for cron delivery |\n | `TELEGRAM_HOME_CHANNEL_NAME` | Display name for the Telegram home channel |\n+| `TELEGRAM_CRON_THREAD_ID` | Forum topic ID to receive cron deliveries; overrides `TELEGRAM_HOME_CHANNEL_THREAD_ID` for cron only. Use in topic mode so replies to cron messages open a new session instead of hitting the system lobby (#24409). |\n | `TELEGRAM_WEBHOOK_URL` | Public HTTPS URL for webhook mode (enables webhook instead of polling) |\n | `TELEGRAM_WEBHOOK_PORT` | Local listen port for webhook server (default: `8443`) |\n | `TELEGRAM_WEBHOOK_SECRET` | Secret token Telegram echoes back in each update for verification. **Required whenever `TELEGRAM_WEBHOOK_URL` is set** — the gateway refuses to start without it (GHSA-3vpc-7q5r-276h). Generate with `openssl rand -hex 32`. |"]},{"file":"website/docs/user-guide/features/cron.md","hunks":["@@ -258,6 +258,17 @@ Semantics: `all` expands to every platform with a configured home channel. Zero\n \n `all` composes with explicit targets. `origin,all` delivers to the origin chat *plus* every other connected home channel, de-duplicating by `(platform, chat_id, thread_id)`.\n \n+### Telegram cron topic (`TELEGRAM_CRON_THREAD_ID`)\n+\n+When Telegram topic mode is enabled, the root DM is reserved as a system lobby — replies sent there are rebuffed with a lobby reminder and `reply_to_message_id` is dropped, so you cannot reply to a cron message that landed in the main chat.\n+\n+Point cron at a dedicated forum topic instead:\n+\n+1. In Telegram, open the bot DM and create a topic named e.g. `Cron`. Long-press the topic header → **Copy link**; the trailing integer is the topic's `message_thread_id`.\n+2. Set `TELEGRAM_CRON_THREAD_ID=<that id>` in your `.env`.\n+\n+This applies only to cron deliveries. `TELEGRAM_HOME_CHANNEL_THREAD_ID` (used elsewhere, e.g. restart notifications) is unchanged. Explicit `deliver=\"telegram:chat_id:thread_id\"` targets continue to win over the env var. Replies to cron messages now arrive in the existing topic session, so you can act on them directly.\n+\n ### Response wrapping\n \n By default, delivered cron output is wrapped with a header and footer so the recipient knows it came from a scheduled task:"]},{"file":"website/docs/user-guide/messaging/telegram.md","hunks":["@@ -256,6 +256,16 @@ TELEGRAM_HOME_CHANNEL_NAME=\"My Notes\"\n Group chat IDs are negative numbers (e.g., `-1001234567890`). Your personal DM chat ID is the same as your user ID.\n :::\n \n+### Cron deliveries in topic mode\n+\n+If you have topic mode enabled in your bot DM, cron messages delivered to the root chat land in the system-only lobby — replying there opens no session and you see the \"main chat is reserved for system commands\" notice. Create a dedicated forum topic (e.g. `Cron`) and set:\n+\n+```bash\n+TELEGRAM_CRON_THREAD_ID=<topic_thread_id>\n+```\n+\n+`TELEGRAM_CRON_THREAD_ID` overrides `TELEGRAM_HOME_CHANNEL_THREAD_ID` for cron deliveries only. Replies in that topic continue the topic's existing session.\n+\n ## Voice Messages\n \n ### Incoming Voice (Speech-to-Text)\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}