From 01db1dde1ad77392c96ff0e3d9be72d5146a9e91 Mon Sep 17 00:00:00 2001 From: Ayaan Zaidi Date: Fri, 6 Feb 2026 00:22:40 +0530 Subject: [PATCH] =?UTF-8?q?fix:=20telegram=20topic=20auto-threading=20?= =?UTF-8?q?=E2=80=94=20use=20parseTelegramTarget,=20add=20tests=20(#7235)?= =?UTF-8?q?=20(thanks=20@Lukavyi)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + ...est.ts => sessions-spawn-threadid.test.ts} | 0 .../message-action-runner.threading.test.ts | 62 ++++++++++++++++++- src/infra/outbound/message-action-runner.ts | 29 +++++---- 4 files changed, 78 insertions(+), 14 deletions(-) rename src/agents/{openclaw-tools.subagents.sessions-spawn-captures-threadid.test.ts => sessions-spawn-threadid.test.ts} (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17a5ee3bee..89c5d7c407 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai ### Changes +- Telegram: auto-inject forum topic `threadId` in message tool and subagent announce so media, buttons, and subagent results land in the correct topic instead of General. (#7235) Thanks @Lukavyi. - CLI: sort `openclaw --help` commands (and options) alphabetically. (#8068) Thanks @deepsoumya617. - Telegram: remove last `@ts-nocheck` from `bot-handlers.ts`, use Grammy types directly, deduplicate `StickerMetadata`. Zero `@ts-nocheck` remaining in `src/telegram/`. (#9206) - Telegram: remove `@ts-nocheck` from `bot-message.ts`, type deps via `Omit`, widen `allMedia` to `TelegramMediaRef[]`. (#9180) diff --git a/src/agents/openclaw-tools.subagents.sessions-spawn-captures-threadid.test.ts b/src/agents/sessions-spawn-threadid.test.ts similarity index 100% rename from src/agents/openclaw-tools.subagents.sessions-spawn-captures-threadid.test.ts rename to src/agents/sessions-spawn-threadid.test.ts diff --git a/src/infra/outbound/message-action-runner.threading.test.ts b/src/infra/outbound/message-action-runner.threading.test.ts index 4ff3ac3f7f..946f0db961 100644 --- a/src/infra/outbound/message-action-runner.threading.test.ts +++ b/src/infra/outbound/message-action-runner.threading.test.ts @@ -152,7 +152,63 @@ describe("runMessageAction threading auto-injection", () => { agentId: "main", }); - const call = mocks.executeSendAction.mock.calls[0]?.[0] as { ctx?: { params?: any } }; + const call = mocks.executeSendAction.mock.calls[0]?.[0] as { + ctx?: { params?: Record }; + }; + expect(call?.ctx?.params?.threadId).toBe("42"); + }); + + it("skips telegram auto-threading when target chat differs", async () => { + mocks.executeSendAction.mockResolvedValue({ + handledBy: "plugin", + payload: {}, + }); + + await runMessageAction({ + cfg: telegramConfig, + action: "send", + params: { + channel: "telegram", + target: "telegram:999", + message: "hi", + }, + toolContext: { + currentChannelId: "telegram:123", + currentThreadTs: "42", + }, + agentId: "main", + }); + + const call = mocks.executeSendAction.mock.calls[0]?.[0] as { + ctx?: { params?: Record }; + }; + expect(call?.ctx?.params?.threadId).toBeUndefined(); + }); + + it("matches telegram target with internal prefix variations", async () => { + mocks.executeSendAction.mockResolvedValue({ + handledBy: "plugin", + payload: {}, + }); + + await runMessageAction({ + cfg: telegramConfig, + action: "send", + params: { + channel: "telegram", + target: "telegram:group:123", + message: "hi", + }, + toolContext: { + currentChannelId: "telegram:123", + currentThreadTs: "42", + }, + agentId: "main", + }); + + const call = mocks.executeSendAction.mock.calls[0]?.[0] as { + ctx?: { params?: Record }; + }; expect(call?.ctx?.params?.threadId).toBe("42"); }); @@ -178,7 +234,9 @@ describe("runMessageAction threading auto-injection", () => { agentId: "main", }); - const call = mocks.executeSendAction.mock.calls[0]?.[0] as { ctx?: { params?: any } }; + const call = mocks.executeSendAction.mock.calls[0]?.[0] as { + ctx?: { params?: Record }; + }; expect(call?.ctx?.params?.threadId).toBe("999"); }); }); diff --git a/src/infra/outbound/message-action-runner.ts b/src/infra/outbound/message-action-runner.ts index c9487415c1..d032d60b49 100644 --- a/src/infra/outbound/message-action-runner.ts +++ b/src/infra/outbound/message-action-runner.ts @@ -20,7 +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"; -// parseTelegramTarget no longer used (telegram auto-threading uses string matching) +import { parseTelegramTarget } from "../../telegram/targets.js"; import { isDeliverableMessageChannel, normalizeMessageChannel, @@ -250,6 +250,10 @@ function resolveSlackAutoThreadId(params: { * 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. + * + * Unlike Slack, we do not gate on `replyToMode` here: Telegram forum topics + * are persistent sub-channels (not ephemeral reply threads), so auto-injection + * should always apply when the target chat matches. */ function resolveTelegramAutoThreadId(params: { to: string; @@ -259,12 +263,12 @@ function resolveTelegramAutoThreadId(params: { if (!context?.currentThreadTs || !context.currentChannelId) { return undefined; } - // Only apply when the target matches the originating chat. - // Note: Telegram topic routing is carried via threadId/message_thread_id; - // `currentChannelId` (and most agent targets) are typically the base chat id. - const normalizedTo = params.to.trim().toLowerCase(); - const normalizedChannel = context.currentChannelId.trim().toLowerCase(); - if (normalizedTo !== normalizedChannel) { + // Use parseTelegramTarget to extract canonical chatId from both sides, + // mirroring how Slack uses parseSlackTarget. This handles format variations + // like `telegram:group:123:topic:456` vs `telegram:123`. + const parsedTo = parseTelegramTarget(params.to); + const parsedChannel = parseTelegramTarget(context.currentChannelId); + if (parsedTo.chatId.toLowerCase() !== parsedChannel.chatId.toLowerCase()) { return undefined; } return context.currentThreadTs; @@ -823,10 +827,11 @@ async function handleSendAction(ctx: ResolvedActionContext): Promise