From 4a79282a2b291f35c288266e42a896cee3ea01dd Mon Sep 17 00:00:00 2001 From: Clawdbot Date: Wed, 28 Jan 2026 20:54:57 +0100 Subject: [PATCH] fix(telegram): restore DM thread delivery after forum-only gate Commit 9154971 ('ignore message_thread_id for non-forum group sessions') changed resolveTelegramForumThreadId to return undefined when is_forum is false. This correctly fixed reply-chain pollution in regular groups, but broke DM thread delivery: Telegram Bot API never sets is_forum=true for private chats, so resolvedThreadId became undefined for DM topics (Telegram Premium feature). The returned context.resolvedThreadId is used by bot-message-dispatch.ts for draft streaming and final reply delivery. With it undefined, replies went to General instead of the originating DM thread. Fix: introduce effectiveThreadId that uses raw messageThreadId for private chats (bypassing the forum gate) while preserving the forum-resolved value for groups. Apply it to: - outbound delivery (resolvedThreadId in returned context) - typing indicator (sendChatAction) - voice recording indicator --- src/telegram/bot-message-context.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/telegram/bot-message-context.ts b/src/telegram/bot-message-context.ts index 832a4413d..629a8dc44 100644 --- a/src/telegram/bot-message-context.ts +++ b/src/telegram/bot-message-context.ts @@ -161,6 +161,9 @@ export const buildTelegramMessageContext = async ({ isForum, messageThreadId, }); + // Effective thread ID for outbound delivery: groups use forum-resolved; DMs use raw messageThreadId + // (private chats never set is_forum=true per Telegram Bot API, so resolvedThreadId is always undefined for DMs) + const effectiveThreadId = isGroup ? resolvedThreadId : messageThreadId; const { groupConfig, topicConfig } = resolveTelegramGroupConfig(chatId, resolvedThreadId); const peerId = isGroup ? buildTelegramGroupPeerId(chatId, resolvedThreadId) : String(chatId); const route = resolveAgentRoute({ @@ -203,7 +206,8 @@ export const buildTelegramMessageContext = async ({ const sendTyping = async () => { await withTelegramApiErrorLogging({ operation: "sendChatAction", - fn: () => bot.api.sendChatAction(chatId, "typing", buildTypingThreadParams(resolvedThreadId)), + fn: () => + bot.api.sendChatAction(chatId, "typing", buildTypingThreadParams(effectiveThreadId)), }); }; @@ -212,7 +216,11 @@ export const buildTelegramMessageContext = async ({ await withTelegramApiErrorLogging({ operation: "sendChatAction", fn: () => - bot.api.sendChatAction(chatId, "record_voice", buildTypingThreadParams(resolvedThreadId)), + bot.api.sendChatAction( + chatId, + "record_voice", + buildTypingThreadParams(effectiveThreadId), + ), }); } catch (err) { logVerbose(`telegram record_voice cue failed for chat ${chatId}: ${String(err)}`); @@ -605,6 +613,8 @@ export const buildTelegramMessageContext = async ({ // For groups: use resolvedThreadId (forum topics only); for DMs: use raw messageThreadId MessageThreadId: isGroup ? resolvedThreadId : messageThreadId, IsForum: isForum, + // DM thread label for display in Sessions tab (e.g., "Sender Name (Thread: 42)") + ThreadLabel: !isGroup && messageThreadId != null ? `Thread: ${messageThreadId}` : undefined, // Originating channel for reply routing. OriginatingChannel: "telegram" as const, OriginatingTo: `telegram:${chatId}`, @@ -655,7 +665,7 @@ export const buildTelegramMessageContext = async ({ msg, chatId, isGroup, - resolvedThreadId, + resolvedThreadId: effectiveThreadId, isForum, historyKey, historyLimit,