From cc0bfa0f3959fb147052e805c52a920be48fbaa2 Mon Sep 17 00:00:00 2001 From: Garnet Liu <12513503+garnetlyx@users.noreply.github.com> Date: Mon, 16 Feb 2026 00:21:18 +0800 Subject: [PATCH] fix(telegram): restore thread_id=1 handling for DMs (regression from 19b8416a8) (openclaw#10942) thanks @garnetlyx Verified: - pnpm install --frozen-lockfile - pnpm build - pnpm test:macmini Co-authored-by: garnetlyx <12513503+garnetlyx@users.noreply.github.com> Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com> --- CHANGELOG.md | 1 + src/telegram/bot/delivery.test.ts | 4 ++-- src/telegram/bot/helpers.test.ts | 21 ++++++++++++++++++--- src/telegram/bot/helpers.ts | 21 +++++++++++++++++++-- src/telegram/draft-stream.test.ts | 6 ++---- 5 files changed, 42 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ec5a51b207..b59b0bf8150 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Telegram: omit `message_thread_id` for DM sends/draft previews and keep forum-topic handling (`id=1` general omitted, non-general kept), preventing DM failures with `400 Bad Request: message thread not found`. (#10942) Thanks @garnetlyx. - Subagents/Models: preserve `agents.defaults.model.fallbacks` when subagent sessions carry a model override, so subagent runs fail over to configured fallback models instead of retrying only the overridden primary model. - Config/Gateway: make sensitive-key whitelist suffix matching case-insensitive while preserving `passwordFile` path exemptions, preventing accidental redaction of non-secret config values like `maxTokens` and IRC password-file paths. (#16042) Thanks @akramcodez. - Group chats: always inject group chat context (name, participants, reply guidance) into the system prompt on every turn, not just the first. Prevents the model from losing awareness of which group it's in and incorrectly using the message tool to send to the same group. (#14447) Thanks @tyler6204. diff --git a/src/telegram/bot/delivery.test.ts b/src/telegram/bot/delivery.test.ts index 7d61f05284c..c1a008b1133 100644 --- a/src/telegram/bot/delivery.test.ts +++ b/src/telegram/bot/delivery.test.ts @@ -169,7 +169,7 @@ describe("deliverReplies", () => { ); }); - it("keeps message_thread_id=1 when allowed", async () => { + it("does not include message_thread_id for DMs (threads don't exist in private chats)", async () => { const runtime = { error: vi.fn(), log: vi.fn() }; const sendMessage = vi.fn().mockResolvedValue({ message_id: 4, @@ -191,7 +191,7 @@ describe("deliverReplies", () => { expect(sendMessage).toHaveBeenCalledWith( "123", expect.any(String), - expect.objectContaining({ + expect.not.objectContaining({ message_thread_id: 1, }), ); diff --git a/src/telegram/bot/helpers.test.ts b/src/telegram/bot/helpers.test.ts index 526d2ec3aad..d0f91345da9 100644 --- a/src/telegram/bot/helpers.test.ts +++ b/src/telegram/bot/helpers.test.ts @@ -43,9 +43,24 @@ describe("buildTelegramThreadParams", () => { }); }); - it("keeps thread id=1 for dm threads", () => { - expect(buildTelegramThreadParams({ id: 1, scope: "dm" })).toEqual({ - message_thread_id: 1, + it("skips thread id for dm threads (DMs don't have threads)", () => { + expect(buildTelegramThreadParams({ id: 1, scope: "dm" })).toBeUndefined(); + expect(buildTelegramThreadParams({ id: 2, scope: "dm" })).toBeUndefined(); + }); + + it("normalizes and skips thread id for dm threads even with edge values", () => { + expect(buildTelegramThreadParams({ id: 0, scope: "dm" })).toBeUndefined(); + expect(buildTelegramThreadParams({ id: -1, scope: "dm" })).toBeUndefined(); + expect(buildTelegramThreadParams({ id: 1.9, scope: "dm" })).toBeUndefined(); + }); + + it("handles thread id 0 for non-dm scopes", () => { + // id=0 should be included for forum and none scopes (not falsy) + expect(buildTelegramThreadParams({ id: 0, scope: "forum" })).toEqual({ + message_thread_id: 0, + }); + expect(buildTelegramThreadParams({ id: 0, scope: "none" })).toEqual({ + message_thread_id: 0, }); }); diff --git a/src/telegram/bot/helpers.ts b/src/telegram/bot/helpers.ts index b9f0706b63d..809a3aa8255 100644 --- a/src/telegram/bot/helpers.ts +++ b/src/telegram/bot/helpers.ts @@ -56,17 +56,34 @@ export function resolveTelegramThreadSpec(params: { /** * Build thread params for Telegram API calls (messages, media). + * + * IMPORTANT: Thread IDs behave differently based on chat type: + * - DMs (private chats): Never send thread_id (threads don't exist) + * - Forum topics: Skip thread_id=1 (General topic), include others + * - Regular groups: Thread IDs are ignored by Telegram + * * 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"). + * + * @param thread - Thread specification with ID and scope + * @returns API params object or undefined if thread_id should be omitted */ export function buildTelegramThreadParams(thread?: TelegramThreadSpec | null) { - if (!thread?.id) { + if (thread?.id == null) { return undefined; } const normalized = Math.trunc(thread.id); - if (normalized === TELEGRAM_GENERAL_TOPIC_ID && thread.scope === "forum") { + + // 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 }; } diff --git a/src/telegram/draft-stream.test.ts b/src/telegram/draft-stream.test.ts index 0c4cc74ad4f..c8ad4ed7ce6 100644 --- a/src/telegram/draft-stream.test.ts +++ b/src/telegram/draft-stream.test.ts @@ -94,7 +94,7 @@ describe("createTelegramDraftStream", () => { await vi.waitFor(() => expect(api.sendMessage).toHaveBeenCalledWith(123, "Hello", undefined)); }); - it("keeps message_thread_id for dm threads and clears preview on cleanup", async () => { + it("omits message_thread_id for dm threads and clears preview on cleanup", async () => { const api = { sendMessage: vi.fn().mockResolvedValue({ message_id: 17 }), editMessageText: vi.fn().mockResolvedValue(true), @@ -108,9 +108,7 @@ describe("createTelegramDraftStream", () => { }); stream.update("Hello"); - await vi.waitFor(() => - expect(api.sendMessage).toHaveBeenCalledWith(123, "Hello", { message_thread_id: 1 }), - ); + await vi.waitFor(() => expect(api.sendMessage).toHaveBeenCalledWith(123, "Hello", undefined)); await stream.clear(); expect(api.deleteMessage).toHaveBeenCalledWith(123, 17);