Skip to content

Commit f64d25b

Browse files
authored
fix(telegram): scope DM topic thread keys by chat id (#31064)
* fix(telegram): scope DM topic thread keys by chat id * test(telegram): update dm topic session-key expectation * fix(telegram): parse scoped dm thread ids in outbound recovery * chore(telegram): format accounts config merge block * test(nodes): simplify mocked exports for ts tuple spreads
1 parent bbab94c commit f64d25b

File tree

11 files changed

+74
-19
lines changed

11 files changed

+74
-19
lines changed

src/agents/tools/nodes-tool.test.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,20 @@ const screenMocks = vi.hoisted(() => ({
2525
}));
2626

2727
vi.mock("./gateway.js", () => ({
28-
callGatewayTool: (...args: unknown[]) => gatewayMocks.callGatewayTool(...args),
29-
readGatewayCallOptions: (...args: unknown[]) => gatewayMocks.readGatewayCallOptions(...args),
28+
callGatewayTool: gatewayMocks.callGatewayTool,
29+
readGatewayCallOptions: gatewayMocks.readGatewayCallOptions,
3030
}));
3131

3232
vi.mock("./nodes-utils.js", () => ({
33-
resolveNodeId: (...args: unknown[]) => nodeUtilsMocks.resolveNodeId(...args),
34-
listNodes: (...args: unknown[]) => nodeUtilsMocks.listNodes(...args),
35-
resolveNodeIdFromList: (...args: unknown[]) => nodeUtilsMocks.resolveNodeIdFromList(...args),
33+
resolveNodeId: nodeUtilsMocks.resolveNodeId,
34+
listNodes: nodeUtilsMocks.listNodes,
35+
resolveNodeIdFromList: nodeUtilsMocks.resolveNodeIdFromList,
3636
}));
3737

3838
vi.mock("../../cli/nodes-screen.js", () => ({
39-
parseScreenRecordPayload: (...args: unknown[]) => screenMocks.parseScreenRecordPayload(...args),
40-
screenRecordTempPath: (...args: unknown[]) => screenMocks.screenRecordTempPath(...args),
41-
writeScreenRecordToFile: (...args: unknown[]) => screenMocks.writeScreenRecordToFile(...args),
39+
parseScreenRecordPayload: screenMocks.parseScreenRecordPayload,
40+
screenRecordTempPath: screenMocks.screenRecordTempPath,
41+
writeScreenRecordToFile: screenMocks.writeScreenRecordToFile,
4242
}));
4343

4444
import { createNodesTool } from "./nodes-tool.js";

src/channels/plugins/outbound/telegram.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,32 @@ describe("telegramOutbound", () => {
3232
expect(result).toEqual({ channel: "telegram", messageId: "tg-text-1", chatId: "123" });
3333
});
3434

35+
it("parses scoped DM thread ids for sendText", async () => {
36+
const sendTelegram = vi.fn().mockResolvedValue({ messageId: "tg-text-2", chatId: "12345" });
37+
const sendText = telegramOutbound.sendText;
38+
expect(sendText).toBeDefined();
39+
40+
await sendText!({
41+
cfg: {},
42+
to: "12345",
43+
text: "<b>hello</b>",
44+
accountId: "work",
45+
threadId: "12345:99",
46+
deps: { sendTelegram },
47+
});
48+
49+
expect(sendTelegram).toHaveBeenCalledWith(
50+
"12345",
51+
"<b>hello</b>",
52+
expect.objectContaining({
53+
textMode: "html",
54+
verbose: false,
55+
accountId: "work",
56+
messageThreadId: 99,
57+
}),
58+
);
59+
});
60+
3561
it("passes media options for sendMedia", async () => {
3662
const sendTelegram = vi.fn().mockResolvedValue({ messageId: "tg-media-1", chatId: "123" });
3763
const sendMedia = telegramOutbound.sendMedia;

src/infra/outbound/outbound-session.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { normalizeAllowListLower } from "../../slack/monitor/allow-list.js";
2020
import { parseSlackTarget } from "../../slack/targets.js";
2121
import { buildTelegramGroupPeerId } from "../../telegram/bot/helpers.js";
2222
import { resolveTelegramTargetChatType } from "../../telegram/inline-buttons.js";
23+
import { parseTelegramThreadId } from "../../telegram/outbound-params.js";
2324
import { parseTelegramTarget } from "../../telegram/targets.js";
2425
import { isWhatsAppGroupJid, normalizeWhatsAppTarget } from "../../whatsapp/normalize.js";
2526
import type { ResolvedMessagingTarget } from "./target-resolver.js";
@@ -283,8 +284,7 @@ function resolveTelegramSession(
283284
}
284285
const parsedThreadId = parsed.messageThreadId;
285286
const fallbackThreadId = normalizeThreadId(params.threadId);
286-
const resolvedThreadId =
287-
parsedThreadId ?? (fallbackThreadId ? Number.parseInt(fallbackThreadId, 10) : undefined);
287+
const resolvedThreadId = parsedThreadId ?? parseTelegramThreadId(fallbackThreadId);
288288
// Telegram topics are encoded in the peer id (chatId:topic:<id>).
289289
const chatType = resolveTelegramTargetChatType(params.target);
290290
// If the target is a username and we lack a resolvedTarget, default to DM to avoid group keys.

src/infra/outbound/outbound.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -891,6 +891,7 @@ describe("resolveOutboundSessionRoute", () => {
891891
channel: string;
892892
target: string;
893893
replyToId?: string;
894+
threadId?: string;
894895
expected: {
895896
sessionKey: string;
896897
from?: string;
@@ -934,6 +935,20 @@ describe("resolveOutboundSessionRoute", () => {
934935
chatType: "direct",
935936
},
936937
},
938+
{
939+
name: "Telegram DM scoped threadId fallback",
940+
cfg: perChannelPeerCfg,
941+
channel: "telegram",
942+
target: "12345",
943+
threadId: "12345:99",
944+
expected: {
945+
sessionKey: "agent:main:telegram:direct:12345",
946+
from: "telegram:12345",
947+
to: "telegram:12345",
948+
threadId: 99,
949+
chatType: "direct",
950+
},
951+
},
937952
{
938953
name: "identity-links per-peer",
939954
cfg: identityLinksCfg,
@@ -1018,6 +1033,7 @@ describe("resolveOutboundSessionRoute", () => {
10181033
agentId: "main",
10191034
target: testCase.target,
10201035
replyToId: testCase.replyToId,
1036+
threadId: testCase.threadId,
10211037
});
10221038
expect(route?.sessionKey, testCase.name).toBe(testCase.expected.sessionKey);
10231039
if (testCase.expected.from !== undefined) {

src/telegram/accounts.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,11 @@ function resolveAccountConfig(
8484
}
8585

8686
function mergeTelegramAccountConfig(cfg: OpenClawConfig, accountId: string): TelegramAccountConfig {
87-
const { accounts: _ignored, groups: channelGroups, ...base } = (cfg.channels?.telegram ??
88-
{}) as TelegramAccountConfig & { accounts?: unknown };
87+
const {
88+
accounts: _ignored,
89+
groups: channelGroups,
90+
...base
91+
} = (cfg.channels?.telegram ?? {}) as TelegramAccountConfig & { accounts?: unknown };
8992
const account = resolveAccountConfig(cfg, accountId) ?? {};
9093

9194
// In multi-account setups, channel-level `groups` must NOT be inherited by

src/telegram/bot-handlers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,7 @@ export const registerTelegramHandlers = ({
290290
const dmThreadId = !params.isGroup ? params.messageThreadId : undefined;
291291
const threadKeys =
292292
dmThreadId != null
293-
? resolveThreadSessionKeys({ baseSessionKey, threadId: String(dmThreadId) })
293+
? resolveThreadSessionKeys({ baseSessionKey, threadId: `${params.chatId}:${dmThreadId}` })
294294
: null;
295295
const sessionKey = threadKeys?.sessionKey ?? baseSessionKey;
296296
const storePath = resolveStorePath(cfg.session?.store, { agentId: route.agentId });

src/telegram/bot-message-context.dm-threads.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ describe("buildTelegramMessageContext dm thread sessions", () => {
1919

2020
expect(ctx).not.toBeNull();
2121
expect(ctx?.ctxPayload?.MessageThreadId).toBe(42);
22-
expect(ctx?.ctxPayload?.SessionKey).toBe("agent:main:main:thread:42");
22+
expect(ctx?.ctxPayload?.SessionKey).toBe("agent:main:main:thread:1234:42");
2323
});
2424

2525
it("keeps legacy dm session key when no thread id", async () => {

src/telegram/bot-message-context.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ export const buildTelegramMessageContext = async ({
204204
const dmThreadId = threadSpec.scope === "dm" ? threadSpec.id : undefined;
205205
const threadKeys =
206206
dmThreadId != null
207-
? resolveThreadSessionKeys({ baseSessionKey, threadId: String(dmThreadId) })
207+
? resolveThreadSessionKeys({ baseSessionKey, threadId: `${chatId}:${dmThreadId}` })
208208
: null;
209209
const sessionKey = threadKeys?.sessionKey ?? baseSessionKey;
210210
const mentionRegexes = buildMentionRegexes(cfg, route.agentId);

src/telegram/bot-native-commands.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -551,7 +551,7 @@ export const registerTelegramNativeCommands = ({
551551
dmThreadId != null
552552
? resolveThreadSessionKeys({
553553
baseSessionKey,
554-
threadId: String(dmThreadId),
554+
threadId: `${chatId}:${dmThreadId}`,
555555
})
556556
: null;
557557
const sessionKey = threadKeys?.sessionKey ?? baseSessionKey;

src/telegram/bot.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -928,7 +928,7 @@ describe("createTelegramBot", () => {
928928

929929
expect(replySpy).toHaveBeenCalledTimes(1);
930930
const payload = replySpy.mock.calls[0][0];
931-
expect(payload.CommandTargetSessionKey).toBe("agent:main:main:thread:99");
931+
expect(payload.CommandTargetSessionKey).toBe("agent:main:main:thread:12345:99");
932932
});
933933

934934
it("allows native DM commands for paired users", async () => {

0 commit comments

Comments
 (0)