Skip to content

fix(telegram): restore thread_id=1 handling for DMs (regression from 19b8416a8) #10926

@garnetlyx

Description

@garnetlyx

fix(telegram): restore thread_id=1 handling for DMs (regression from 19b8416)

Problem Description

Sub-agent announce messages fail in Telegram DMs with error: "Bad Request: message thread not found"

When a sub-agent completes its task and OpenClaw attempts to announce the result back to the main session via Telegram, the delivery fails with Telegram API error 400. This only affects private chats (DMs), not group chats or forum topics.

Root Cause Analysis

1. PR #848 (Jan 16) - Correct Fix

PR #848 (fix(telegram): skip message_thread_id for General topic messages) correctly handled the Telegram General topic (thread_id=1) inconsistency:

  • Problem: Telegram rejects sendMessage with message_thread_id=1 for General forum topics
  • Fix: In buildTelegramThreadParams(), skip thread_id=1 entirely:
    if (normalized === TELEGRAM_GENERAL_TOPIC_ID) {
      return undefined;
    }

2. Commit 19b8416 (Feb 2) - Regression

Commit 19b8416a8 ("fix: unify telegram thread handling") introduced a regression by:

  • Adding scope concept ("dm" | "forum" | "none")
  • Modifying the logic to only skip thread_id=1 when scope === "forum":
    if (normalized === TELEGRAM_GENERAL_TOPIC_ID && thread.scope === "forum") {
      return undefined;
    }

3. The Bug

In private chats (DMs):

  • threadSpec.scope = "dm"
  • threadSpec.id = 1 (default or from configuration)
  • New logic: 1 === 1 && "dm" === "forum"false (does NOT skip)
  • Result: Sends message_thread_id: 1 to Telegram API
  • Telegram response: "Bad Request: message thread not found"

Code Evidence

Before (PR #848 correct):

// src/telegram/bot/helpers.ts (after PR #848)
export function buildTelegramThreadParams(messageThreadId?: number) {
  if (messageThreadId == null) {
    return undefined;
  }
  const normalized = Math.trunc(messageThreadId);
  if (normalized === TELEGRAM_GENERAL_TOPIC_ID) {
    return undefined;  // ← SKIPS thread_id=1 for ALL cases
  }
  return { message_thread_id: normalized };
}

After (Commit 19b8416 broken):

// src/telegram/bot/helpers.ts (current)
export function buildTelegramThreadParams(thread?: TelegramThreadSpec | null) {
  if (!thread?.id) {
    return undefined;
  }
  const normalized = Math.trunc(thread.id);
  if (normalized === TELEGRAM_GENERAL_TOPIC_ID && thread.scope === "forum") {
    return undefined;  // ← Only skips when scope="forum"
  }
  return { message_thread_id: normalized };
}

Impact

  1. Sub-agent announcements fail in Telegram DMs
  2. User sees: Main session receives the result, but Telegram delivery fails
  3. Error logs: Frequent "Bad Request: message thread not found" errors
  4. Affected flow: Sub-agent → OpenClaw announce → Telegram delivery (step 2 fails)

Error Logs (Redacted)

Actual error logs from production (user IDs and tokens redacted):

2026-02-06T22:51:39.868Z [telegram] message failed: Call to 'sendMessage' failed! (400: Bad Request: message thread not found)
2026-02-06T22:51:39.871Z [tools] message failed: Call to 'sendMessage' failed! (400: Bad Request: message thread not found)
2026-02-07T01:32:51.469Z [telegram] message failed: Call to 'sendMessage' failed! (400: Bad Request: message thread not found)
2026-02-07T01:32:51.471Z Delivery failed (telegram to telegram:[REDACTED_USER_ID]): GrammyError: Call to 'sendMessage' failed! (400: Bad Request: message thread not found)

Pattern: The error occurs consistently when OpenClaw attempts to deliver sub-agent announce messages to Telegram DMs.

Environment

  • OpenClaw version: 2026.2.4+
  • Telegram Bot API: Latest
  • Chat type: Private chat (DM)
  • Feature affected: Sub-agent announce delivery

Proposed Fix

Option A (Restore PR #848 logic):

export function buildTelegramThreadParams(thread?: TelegramThreadSpec | null) {
  if (!thread?.id) {
    return undefined;
  }
  const normalized = Math.trunc(thread.id);
  // Restore: skip thread_id=1 for ALL scopes
  if (normalized === TELEGRAM_GENERAL_TOPIC_ID) {
    return undefined;
  }
  return { message_thread_id: normalized };
}

Option B (More precise):

export function buildTelegramThreadParams(thread?: TelegramThreadSpec | null) {
  if (!thread?.id) {
    return undefined;
  }
  const normalized = Math.trunc(thread.id);
  // Skip thread_id=1 OR skip for DM scope (DMs shouldn't have threads)
  if (normalized === TELEGRAM_GENERAL_TOPIC_ID || thread.scope === "dm") {
    return undefined;
  }
  return { message_thread_id: normalized };
}

Option C (Recommended - clearest semantics):

export function buildTelegramThreadParams(thread?: TelegramThreadSpec | null) {
  if (!thread?.id) {
    return undefined;
  }
  const normalized = Math.trunc(thread.id);
  
  // Never send thread_id for DMs (threads don't exist in private chats)
  if (thread.scope === "dm") {
    return undefined;
  }
  
  // Telegram rejects message_thread_id=1 for General forum topic
  if (normalized === TELEGRAM_GENERAL_TOPIC_ID) {
    return undefined;
  }
  
  return { message_thread_id: normalized };
}

Testing Checklist

1. DM (Private Chat)

  • Sub-agent announcement succeeds
  • No "message thread not found" errors in logs
  • Works with defaultThreadId = 1 in config (if applicable)

2. Forum Topics

  • General topic (id=1) messages work (thread_id omitted)
  • Specific topics (id>1) work (thread_id included)
  • Typing indicators appear in General topic (requires thread_id)

3. Regular Groups

  • Messages deliver without thread_id
  • Reply threads don't create separate sessions

4. Typing Indicators

  • buildTypingThreadParams() still works correctly (separate function)
  • General topic typing indicators appear (requires thread_id=1)

Note: buildTypingThreadParams() uses raw messageThreadId and is NOT affected by this regression. It correctly includes thread_id=1 for typing indicators in General forum topics.

Related Issues/PRs

Additional Context

Why DMs have thread_id=1

The messageThreadId=1 in DMs typically comes from:

  • User's openclaw.json config: agent.defaultThreadId = 1
  • OR session state from previous forum interactions
  • OR default value in some configuration

Telegram API Design

  • Private chats (DMs): Do NOT support threads/topics
  • Forum groups: Require thread_id for topics (except General topic id=1)
  • Regular groups: Ignore thread_id (reply threads ≠ topics)

Note on Typing Indicators

buildTypingThreadParams() is a separate function that:

  • Uses raw messageThreadId, not TelegramThreadSpec
  • General topic (id=1) NEEDS thread_id for typing indicators to appear
  • This function is NOT affected by the regression

The resolveTelegramThreadSpec function returns scope: "dm" for private chats, which is correct. However, DMs should not have thread_id at all (or it should be ignored). The bug occurs because some configuration or session state sets messageThreadId=1 for DMs, and the new logic incorrectly tries to send it.

This is a clear regression that needs to be fixed to restore proper sub-agent announce functionality in Telegram DMs.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions