# NousResearch/hermes-agent · Telegram topic routing fixes for replies and cron

## Open now

- Nothing open on GitHub.

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.

## TL;DR

- 3 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 1 PR(s) closed without merging.
- A fix was merged to main in PR #28518. Verify it addresses your case.
- **Recommended:** read PR #28518 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `unresolved`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.67`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Closed / duplicate issues

- #24409: \[Bug\]: Replying to telegram topic main chat, not allowed.

## Prior & competing PRs

- **#28518: fix(cron): route Telegram cron deliveries to dedicated topic via TELEGRAM_CRON_THREAD_ID (closes #24409)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `.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:
  - `.env.example`

    ```diff
    @@ -339,6 +339,7 @@ BROWSER_INACTIVITY_TIMEOUT=120
     # TELEGRAM_ALLOWED_USERS=                  # Comma-separated user IDs
     # TELEGRAM_HOME_CHANNEL=                   # Default chat for cron delivery
     # TELEGRAM_HOME_CHANNEL_NAME=              # Display name for home channel
    +# TELEGRAM_CRON_THREAD_ID=                 # Forum topic ID for cron deliveries; overrides TELEGRAM_HOME_CHANNEL_THREAD_ID for cron so replies work in topic mode
     
     # Webhook mode (optional — for cloud deployments like Fly.io/Railway)
     # Default is long polling. Setting TELEGRAM_WEBHOOK_URL switches to webhook mode.
    ```
  - `cron/scheduler.py`

    ```diff
    @@ -292,10 +292,23 @@ def _get_home_target_chat_id(platform_name: str) -> str:
     
     
     def _get_home_target_thread_id(platform_name: str) -> Optional[str]:
    -    """Return the optional thread/topic ID for a platform home target."""
    +    """Return the optional thread/topic ID for a platform home target.
    +
    +    Telegram-only override: ``TELEGRAM_CRON_THREAD_ID`` takes precedence over
    +    ``TELEGRAM_HOME_CHANNEL_THREAD_ID`` for cron delivery. When topic mode is
    +    enabled, deliveries that land in the root DM (thread_id unset) end up in
    +    the system-only lobby where the user cannot reply — the gateway returns
    +    the lobby reminder and drops ``reply_to_message_id`` (#24409). Pointing
    +    cron at a dedicated topic via this env var lets replies work as expected
    +    without changing the lobby invariant.
    +    """
         env_var = _resolve_home_env_var(platform_name)
         if not env_var:
             return None
    +    if platform_name.lower() == "telegram":
    +        cron_thread = os.getenv("TELEGRAM_CRON_THREAD_ID", "").strip()
    +        if cron_thread:
    +            return cron_thread
         value = os.getenv(f"{env_var}_THREAD_ID", "").strip()
         if not value:
             legacy = _LEGACY_HOME_TARGET_ENV_VARS.get(env_var)
    ```
  - `tests/conftest.py`

    ```diff
    @@ -243,6 +243,7 @@ def _looks_like_credential(name: str) -> bool:
         "TELEGRAM_HOME_CHANNEL",
         "TELEGRAM_HOME_CHANNEL_THREAD_ID",
         "TELEGRAM_HOME_CHANNEL_NAME",
    +    "TELEGRAM_CRON_THREAD_ID",
         "DISCORD_HOME_CHANNEL",
         "DISCORD_HOME_CHANNEL_THREAD_ID",
         "DISCORD_HOME_CHANNEL_NAME",
    ```
  - `tests/cron/test_scheduler.py`

    ```diff
    @@ -151,6 +151,53 @@ def test_bare_platform_delivery_preserves_home_thread_id(self, monkeypatch):
                 "thread_id": "topic-7",
             }
     
    +    def test_telegram_cron_thread_id_overrides_home_thread_id(self, monkeypatch):
    +        """TELEGRAM_CRON_THREAD_ID wins over TELEGRAM_HOME_CHANNEL_THREAD_ID for cron (#24409)."""
    +        monkeypatch.setenv("TELEGRAM_HOME_CHANNEL", "-1001234567890")
    +        monkeypatch.setenv("TELEGRAM_HOME_CHANNEL_THREAD_ID", "5")
    +        monkeypatch.setenv("TELEGRAM_CRON_THREAD_ID", "42")
    +
    +        assert _resolve_delivery_target({"deliver": "telegram"}) == {
    +            "platform": "telegram",
    +            "chat_id": "-1001234567890",
    +            "thread_id": "42",
    +        }
    +
    +    def test_telegram_cron_thread_id_sets_thread_when_home_thread_unset(self, monkeypatch):
    +        """TELEGRAM_CRON_THREAD_ID supplies a thread when no home thread is configured."""
    +        monkeypatch.setenv("TELEGRAM_HOME_CHANNEL", "-1001234567890")
    +        monkeypatch.delenv("TELEGRAM_HOME_CHANNEL_THREAD_ID", raising=False)
    +        monkeypatch.setenv("TELEGRAM_CRON_THREAD_ID", "42")
    +
    +        assert _resolve_delivery_target({"deliver": "telegram"}) == {
    +            "platform": "telegram",
    +            "chat_id": "-1001234567890",
    +            "thread_id": "42",
    +        }
    +
    +    def test_telegram_cron_thread_id_does_not_leak_to_other_platforms(self, monkeypatch):
    +        """TELEGRAM_CRON_THREAD_ID is Telegram-only; other platforms keep their own thread resolution."""
    +        monkeypatch.setenv("DISCORD_HOME_CHANNEL", "parent-42")
    +        monkeypatch.setenv("DISCORD_HOME_CHANNEL_THREAD_ID", "topic-7")
    +        monkeypatch.setenv("TELEGRAM_CRON_THREAD_ID", "42")
    +
    +        assert _resolve_delivery_target({"deliver": "discord"}) == {
    +            "platform": "discord",
    +            "chat_id": "parent-42",
    +            "thread_id": "topic-7",
    +        }
    +
    +    def test_explicit_telegram_topic_target_overrides_cron_thread_id(self, monkeypatch):
    +        """Explicit ``telegram:chat:thread`` targets bypass TELEGRAM_CRON_THREAD_ID."""
    +        monkeypatch.setenv("TELEGRAM_CRON_THREAD_ID", "999")
    +
    +        job = {"deliver": "telegram:-1003724596514:17"}
    +        assert _resolve_delivery_target(job) == {
    +            "platform": "telegram",
    +            "chat_id": "-1003724596514",
    +            "thread_id": "17",
    +        }
    +
         def test_explicit_telegram_topic_target_with_thread_id(self):
             """deliver: 'telegram:chat_id:thread_id' parses correctly."""
             job = {
    ```
  - `website/docs/reference/environment-variables.md`

    ```diff
    @@ -248,6 +248,7 @@ For cloud sandbox backends, persistence is filesystem-oriented. `TERMINAL_LIFETI
     | `TELEGRAM_GROUP_ALLOWED_CHATS` | Comma-separated group/forum chat IDs; any member is authorized |
     | `TELEGRAM_HOME_CHANNEL` | Default Telegram chat/channel for cron delivery |
     | `TELEGRAM_HOME_CHANNEL_NAME` | Display name for the Telegram home channel |
    +| `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). |
     | `TELEGRAM_WEBHOOK_URL` | Public HTTPS URL for webhook mode (enables webhook instead of polling) |
     | `TELEGRAM_WEBHOOK_PORT` | Local listen port for webhook server (default: `8443`) |
     | `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`. |
    ```
  - `website/docs/user-guide/features/cron.md`

    ```diff
    @@ -258,6 +258,17 @@ Semantics: `all` expands to every platform with a configured home channel. Zero
     
     `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)`.
     
    +### Telegram cron topic (`TELEGRAM_CRON_THREAD_ID`)
    +
    +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.
    +
    +Point cron at a dedicated forum topic instead:
    +
    +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`.
    +2. Set `TELEGRAM_CRON_THREAD_ID=<that id>` in your `.env`.
    +
    +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.
    +
     ### Response wrapping
     
     By default, delivered cron output is wrapped with a header and footer so the recipient knows it came from a scheduled task:
    ```
  - `website/docs/user-guide/messaging/telegram.md`

    ```diff
    @@ -256,6 +256,16 @@ TELEGRAM_HOME_CHANNEL_NAME="My Notes"
     Group chat IDs are negative numbers (e.g., `-1001234567890`). Your personal DM chat ID is the same as your user ID.
     :::
     
    +### Cron deliveries in topic mode
    +
    +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:
    +
    +```bash
    +TELEGRAM_CRON_THREAD_ID=<topic_thread_id>
    +```
    +
    +`TELEGRAM_CRON_THREAD_ID` overrides `TELEGRAM_HOME_CHANNEL_THREAD_ID` for cron deliveries only. Replies in that topic continue the topic's existing session.
    +
     ## Voice Messages
     
     ### Incoming Voice (Speech-to-Text)
    ```
- **#24798: fix(cron): route Telegram cron deliveries to a dedicated topic via TELEGRAM_CRON_THREAD_ID**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#28518`

## Fix status

- Merged to main in PR #28518 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-05-19`.
