diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ed56348723..1c58e61cf95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ - Browser: extension mode recovers when only one tab is attached (stale targetId fallback). - Browser: fix `tab not found` for extension relay snapshots/actions when Playwright blocks `newCDPSession` (use the single available Page). - Telegram: add bidirectional reaction support with configurable notifications and agent guidance. (#964) — thanks @bohdanpodvirnyi. +- Telegram: skip `message_thread_id=1` for General topic sends while keeping typing indicators. (#848) — thanks @azade-c. - Discord: allow allowlisted guilds without channel lists to receive messages when `groupPolicy="allowlist"`. — thanks @thewilloftheshadow. ## 2026.1.14-1 diff --git a/docs/channels/telegram.md b/docs/channels/telegram.md index 5862e908ce9..fdcc1379c42 100644 --- a/docs/channels/telegram.md +++ b/docs/channels/telegram.md @@ -188,6 +188,7 @@ Disable with: Telegram forum topics include a `message_thread_id` per message. Clawdbot: - Appends `:topic:` to the Telegram group session key so each topic is isolated. - Sends typing indicators and replies with `message_thread_id` so responses stay in the topic. +- General topic (thread id `1`) is special: message sends omit `message_thread_id` (Telegram rejects it), but typing indicators still include it. - Exposes `MessageThreadId` + `IsForum` in template context for routing/templating. - Topic-specific configuration is available under `channels.telegram.groups..topics.` (skills, allowlists, auto-reply, system prompts, disable). diff --git a/src/telegram/bot-message-context.ts b/src/telegram/bot-message-context.ts index 30075564d0f..8b0279e5c4b 100644 --- a/src/telegram/bot-message-context.ts +++ b/src/telegram/bot-message-context.ts @@ -17,7 +17,6 @@ import { buildSenderName, buildTelegramGroupFrom, buildTelegramGroupPeerId, - buildTelegramThreadParams, buildTypingThreadParams, describeReplyTarget, extractTelegramLocation, diff --git a/src/telegram/bot.create-telegram-bot.matches-tg-prefixed-allowfrom-entries-case-insensitively.test.ts b/src/telegram/bot.create-telegram-bot.matches-tg-prefixed-allowfrom-entries-case-insensitively.test.ts index 6debf1854a5..331389a3504 100644 --- a/src/telegram/bot.create-telegram-bot.matches-tg-prefixed-allowfrom-entries-case-insensitively.test.ts +++ b/src/telegram/bot.create-telegram-bot.matches-tg-prefixed-allowfrom-entries-case-insensitively.test.ts @@ -352,10 +352,8 @@ describe("createTelegramBot", () => { getFile: async () => ({ download: async () => new Uint8Array() }), }); - expect(sendMessageSpy).toHaveBeenCalledWith( - "-1001234567890", - expect.any(String), - expect.objectContaining({ message_thread_id: 1 }), - ); + expect(sendMessageSpy).toHaveBeenCalledTimes(1); + const sendParams = sendMessageSpy.mock.calls[0]?.[2] as { message_thread_id?: number }; + expect(sendParams?.message_thread_id).toBeUndefined(); }); }); diff --git a/src/telegram/bot.test.ts b/src/telegram/bot.test.ts index a218adc4d15..e05c1129b07 100644 --- a/src/telegram/bot.test.ts +++ b/src/telegram/bot.test.ts @@ -1710,11 +1710,9 @@ describe("createTelegramBot", () => { getFile: async () => ({ download: async () => new Uint8Array() }), }); - expect(sendMessageSpy).toHaveBeenCalledWith( - "-1001234567890", - expect.any(String), - expect.objectContaining({ message_thread_id: 1 }), - ); + expect(sendMessageSpy).toHaveBeenCalledTimes(1); + const sendParams = sendMessageSpy.mock.calls[0]?.[2] as { message_thread_id?: number }; + expect(sendParams?.message_thread_id).toBeUndefined(); }); it("applies topic skill filters and system prompts", async () => { diff --git a/src/telegram/bot/helpers.test.ts b/src/telegram/bot/helpers.test.ts new file mode 100644 index 00000000000..b84204858b3 --- /dev/null +++ b/src/telegram/bot/helpers.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from "vitest"; +import { buildTelegramThreadParams, buildTypingThreadParams } from "./helpers.js"; + +describe("buildTelegramThreadParams", () => { + it("omits General topic thread id for message sends", () => { + expect(buildTelegramThreadParams(1)).toBeUndefined(); + }); + + it("includes non-General topic thread ids", () => { + expect(buildTelegramThreadParams(99)).toEqual({ message_thread_id: 99 }); + }); + + it("normalizes thread ids to integers", () => { + expect(buildTelegramThreadParams(42.9)).toEqual({ message_thread_id: 42 }); + }); +}); + +describe("buildTypingThreadParams", () => { + it("returns undefined when no thread id is provided", () => { + expect(buildTypingThreadParams(undefined)).toBeUndefined(); + }); + + it("includes General topic thread id for typing indicators", () => { + expect(buildTypingThreadParams(1)).toEqual({ message_thread_id: 1 }); + }); + + it("normalizes thread ids to integers", () => { + expect(buildTypingThreadParams(42.9)).toEqual({ message_thread_id: 42 }); + }); +}); diff --git a/src/telegram/bot/helpers.ts b/src/telegram/bot/helpers.ts index 8d6a5980c68..41eace902da 100644 --- a/src/telegram/bot/helpers.ts +++ b/src/telegram/bot/helpers.ts @@ -21,25 +21,29 @@ export function resolveTelegramForumThreadId(params: { /** * Build thread params for Telegram API calls (messages, media). - * Excludes General topic (id=1) as Telegram rejects explicit message_thread_id=1 - * for sendMessage calls in forum supergroups ("message thread not found" error). + * General forum topic (id=1) must be treated like a regular supergroup send: + * Telegram rejects sendMessage/sendMedia with message_thread_id=1 ("thread not found"). */ export function buildTelegramThreadParams(messageThreadId?: number) { - if (messageThreadId == null || messageThreadId === TELEGRAM_GENERAL_TOPIC_ID) { + if (messageThreadId == null) { return undefined; } - return { message_thread_id: messageThreadId }; + const normalized = Math.trunc(messageThreadId); + if (normalized === TELEGRAM_GENERAL_TOPIC_ID) { + return undefined; + } + return { message_thread_id: normalized }; } /** * Build thread params for typing indicators (sendChatAction). - * Unlike sendMessage, sendChatAction accepts message_thread_id=1 for General topic. + * Empirically, General topic (id=1) needs message_thread_id for typing to appear. */ export function buildTypingThreadParams(messageThreadId?: number) { if (messageThreadId == null) { return undefined; } - return { message_thread_id: messageThreadId }; + return { message_thread_id: Math.trunc(messageThreadId) }; } export function resolveTelegramStreamMode(