diff --git a/src/infra/outbound/message-action-runner.threading.test.ts b/src/infra/outbound/message-action-runner.threading.test.ts index 0a6ca44e9..ecbc168ba 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 { OpenClawConfig } 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 98beaa828..11ac4fcc0 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)) {