diff --git a/src/infra/outbound/message-action-runner.ts b/src/infra/outbound/message-action-runner.ts index e60a01e87..f75f94246 100644 --- a/src/infra/outbound/message-action-runner.ts +++ b/src/infra/outbound/message-action-runner.ts @@ -20,6 +20,7 @@ import { parseReplyDirectives } from "../../auto-reply/reply/reply-directives.js import { dispatchChannelMessageAction } from "../../channels/plugins/message-actions.js"; import { extensionForMime } from "../../media/mime.js"; import { parseSlackTarget } from "../../slack/targets.js"; +import { parseTelegramTarget } from "../../telegram/targets.js"; import { isDeliverableMessageChannel, normalizeMessageChannel, @@ -244,6 +245,32 @@ function resolveSlackAutoThreadId(params: { return context.currentThreadTs; } +/** + * Auto-inject Telegram forum topic thread ID when the message tool targets + * the same chat the session originated from. Mirrors the Slack auto-threading + * pattern so media, buttons, and other tool-sent messages land in the correct + * topic instead of the General Topic. + */ +function resolveTelegramAutoThreadId(params: { + to: string; + toolContext?: ChannelThreadingToolContext; +}): string | undefined { + const context = params.toolContext; + if (!context?.currentThreadTs || !context.currentChannelId) { + return undefined; + } + // Parse both targets to extract base chat IDs, ignoring topic suffixes and + // internal prefixes (e.g. "telegram:group:123:topic:456" → "123"). + // This mirrors Slack's parseSlackTarget approach — compare canonical chat IDs + // so auto-threading applies even when representations differ. + const parsedTo = parseTelegramTarget(params.to); + const parsedChannel = parseTelegramTarget(context.currentChannelId); + if (parsedTo.chatId.toLowerCase() !== parsedChannel.chatId.toLowerCase()) { + return undefined; + } + return context.currentThreadTs; +} + function resolveAttachmentMaxBytes(params: { cfg: OpenClawConfig; channel: ChannelId; @@ -792,6 +819,16 @@ async function handleSendAction(ctx: ResolvedActionContext): Promise