From c5267396a444305f40c35e7f09a8973bb82424f7 Mon Sep 17 00:00:00 2001 From: Clawdbot Date: Tue, 27 Jan 2026 14:55:50 +0100 Subject: [PATCH] fix(telegram): inject toolContext threadId into message tool params When using the message tool in a Telegram DM with topics enabled, media and text were sent to the General topic instead of the current session topic. This happened because toolContext.currentThreadTs was not mapped to params.threadId before the Telegram plugin action handler read it. Now runMessageAction injects toolContext.currentThreadTs into params.threadId when not explicitly provided, ensuring messages land in the correct topic. Fixes #2777 --- .../message-action-runner.threading.test.ts | 109 ++++++++++++++++++ src/infra/outbound/message-action-runner.ts | 7 ++ 2 files changed, 116 insertions(+) diff --git a/src/infra/outbound/message-action-runner.threading.test.ts b/src/infra/outbound/message-action-runner.threading.test.ts index 68a719fc5..1fc6897c1 100644 --- a/src/infra/outbound/message-action-runner.threading.test.ts +++ b/src/infra/outbound/message-action-runner.threading.test.ts @@ -4,6 +4,7 @@ import type { MoltbotConfig } from "../../config/config.js"; import { setActivePluginRegistry } from "../../plugins/runtime.js"; import { createTestRegistry } from "../../test-utils/channel-plugins.js"; import { slackPlugin } from "../../../extensions/slack/src/channel.js"; +import { telegramPlugin } from "../../../extensions/telegram/src/channel.js"; const mocks = vi.hoisted(() => ({ executeSendAction: vi.fn(), @@ -116,3 +117,111 @@ describe("runMessageAction Slack threading", () => { expect(call?.ctx?.mirror?.sessionKey).toBe("agent:main:slack:channel:c123:thread:333.444"); }); }); + +const telegramConfig = { + channels: { + telegram: { + enabled: true, + botToken: "test:token", + }, + }, +} as MoltbotConfig; + +describe("runMessageAction thread id injection from toolContext", () => { + beforeEach(async () => { + const { createPluginRuntime } = await import("../../plugins/runtime/index.js"); + const { setTelegramRuntime } = await import("../../../extensions/telegram/src/runtime.js"); + const runtime = createPluginRuntime(); + setTelegramRuntime(runtime); + setActivePluginRegistry( + createTestRegistry([ + { + pluginId: "telegram", + source: "test", + plugin: telegramPlugin, + }, + ]), + ); + }); + + afterEach(() => { + setActivePluginRegistry(createTestRegistry([])); + mocks.executeSendAction.mockReset(); + mocks.recordSessionMetaFromInbound.mockReset(); + }); + + it("injects toolContext.currentThreadTs into params.threadId when not explicitly set", async () => { + mocks.executeSendAction.mockResolvedValue({ + handledBy: "plugin", + payload: { ok: true, messageId: "123", chatId: "63448508" }, + }); + + await runMessageAction({ + cfg: telegramConfig, + action: "send", + params: { + channel: "telegram", + target: "63448508", + message: "hello from topic", + }, + toolContext: { + currentChannelId: "63448508", + currentChannelProvider: "telegram", + currentThreadTs: "994409", + }, + }); + + const call = mocks.executeSendAction.mock.calls[0]?.[0]; + expect(call?.ctx?.params?.threadId).toBe("994409"); + }); + + it("does not override explicit threadId with toolContext", async () => { + mocks.executeSendAction.mockResolvedValue({ + handledBy: "plugin", + payload: { ok: true, messageId: "124", chatId: "63448508" }, + }); + + await runMessageAction({ + cfg: telegramConfig, + action: "send", + params: { + channel: "telegram", + target: "63448508", + message: "explicit thread", + threadId: "12345", + }, + toolContext: { + currentChannelId: "63448508", + currentChannelProvider: "telegram", + currentThreadTs: "994409", + }, + }); + + const call = mocks.executeSendAction.mock.calls[0]?.[0]; + expect(call?.ctx?.params?.threadId).toBe("12345"); + }); + + it("does not inject threadId when toolContext has no currentThreadTs", async () => { + mocks.executeSendAction.mockResolvedValue({ + handledBy: "plugin", + payload: { ok: true, messageId: "125", chatId: "63448508" }, + }); + + await runMessageAction({ + cfg: telegramConfig, + action: "send", + params: { + channel: "telegram", + target: "63448508", + message: "no thread context", + }, + toolContext: { + currentChannelId: "63448508", + currentChannelProvider: "telegram", + }, + }); + + const call = mocks.executeSendAction.mock.calls[0]?.[0]; + expect(call?.ctx?.params?.threadId).toBeUndefined(); + }); +}); diff --git a/src/infra/outbound/message-action-runner.ts b/src/infra/outbound/message-action-runner.ts index 8f99ad791..40bdf6278 100644 --- a/src/infra/outbound/message-action-runner.ts +++ b/src/infra/outbound/message-action-runner.ts @@ -900,6 +900,13 @@ export async function runMessageAction( } } + // Inject thread id from session context when not explicitly provided. + // This ensures the message tool sends to the correct Telegram DM topic + // (or forum thread) instead of the General/root chat. + if (!params.threadId && input.toolContext?.currentThreadTs) { + params.threadId = input.toolContext.currentThreadTs; + } + applyTargetToParams({ action, args: params }); if (actionRequiresTarget(action)) { if (!actionHasTarget(action, params)) {