NousResearch/hermes-agent

Telegram topic routing fixes for replies and cron

Cluster 24409. Data as of

Nothing open on GitHub. Fix merged in #28518 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 #28518. 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 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.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #28518 fix(cron): route Telegram cron deliveries to dedicated topic via TELEGRAM_CRON_THREAD_ID (closes #24409)

    merged to main

    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

    Show the diff
    • .env.example
      @@ -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
      @@ -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
      @@ -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
      @@ -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
      @@ -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
      @@ -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
      @@ -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)

1 PR closed without merging

From the close comments: merged via another change 1

  • #24798 fix(cron): route Telegram cron deliveries to a dedicated topic via TELEGRAM_CRON_THREAD_ID · merged via another change → #28518
Closed issues · 1
  • Closed#24409[Bug]: Replying to telegram topic main chat, not allowed.
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.00 means 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 Lab

At a glance

Merged PRs
#28518
Closed issues
1
Attempts
1 PR closed without merging
Fix link checked
19 May 2026

For your agent

The same facts, typed.