From 1eb810a5e3280405c8342af7977972e785ca9bbc Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Tue, 17 Mar 2026 00:41:44 -0700 Subject: [PATCH] Telegram: fix named-account DM topic session keys (#48773) --- extensions/telegram/src/bot-handlers.ts | 13 +++- ...t-message-context.named-account-dm.test.ts | 10 ++- .../telegram/src/bot-message-context.ts | 34 ++++------ .../telegram/src/bot-native-commands.ts | 13 +++- ...onversation-route.base-session-key.test.ts | 64 +++++++++++++++++++ extensions/telegram/src/conversation-route.ts | 32 ++++++++++ 6 files changed, 137 insertions(+), 29 deletions(-) create mode 100644 extensions/telegram/src/conversation-route.base-session-key.test.ts diff --git a/extensions/telegram/src/bot-handlers.ts b/extensions/telegram/src/bot-handlers.ts index 18db7c3405f..92d584b8ea9 100644 --- a/extensions/telegram/src/bot-handlers.ts +++ b/extensions/telegram/src/bot-handlers.ts @@ -64,7 +64,10 @@ import { resolveTelegramGroupAllowFromContext, } from "./bot/helpers.js"; import type { TelegramContext } from "./bot/types.js"; -import { resolveTelegramConversationRoute } from "./conversation-route.js"; +import { + resolveTelegramConversationBaseSessionKey, + resolveTelegramConversationRoute, +} from "./conversation-route.js"; import { enforceTelegramDmAccess } from "./dm-access.js"; import { isTelegramExecApprovalApprover, @@ -331,7 +334,13 @@ export const registerTelegramHandlers = ({ senderId: params.senderId, topicAgentId: topicConfig?.agentId, }); - const baseSessionKey = route.sessionKey; + const baseSessionKey = resolveTelegramConversationBaseSessionKey({ + cfg, + route, + chatId: params.chatId, + isGroup: params.isGroup, + senderId: params.senderId, + }); const threadKeys = dmThreadId != null ? resolveThreadSessionKeys({ baseSessionKey, threadId: `${params.chatId}:${dmThreadId}` }) diff --git a/extensions/telegram/src/bot-message-context.named-account-dm.test.ts b/extensions/telegram/src/bot-message-context.named-account-dm.test.ts index a60904514ba..e51c7920ae7 100644 --- a/extensions/telegram/src/bot-message-context.named-account-dm.test.ts +++ b/extensions/telegram/src/bot-message-context.named-account-dm.test.ts @@ -6,9 +6,13 @@ import { import { buildTelegramMessageContextForTest } from "./bot-message-context.test-harness.js"; const recordInboundSessionMock = vi.fn().mockResolvedValue(undefined); -vi.mock("../../../src/channels/session.js", () => ({ - recordInboundSession: (...args: unknown[]) => recordInboundSessionMock(...args), -})); +vi.mock("openclaw/plugin-sdk/channel-runtime", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + recordInboundSession: (...args: unknown[]) => recordInboundSessionMock(...args), + }; +}); describe("buildTelegramMessageContext named-account DM fallback", () => { const baseCfg = { diff --git a/extensions/telegram/src/bot-message-context.ts b/extensions/telegram/src/bot-message-context.ts index d77fd52f2fc..b569b1aeb1e 100644 --- a/extensions/telegram/src/bot-message-context.ts +++ b/extensions/telegram/src/bot-message-context.ts @@ -9,7 +9,7 @@ import { loadConfig } from "openclaw/plugin-sdk/config-runtime"; import type { TelegramDirectConfig, TelegramGroupConfig } from "openclaw/plugin-sdk/config-runtime"; import { ensureConfiguredAcpRouteReady } from "openclaw/plugin-sdk/conversation-runtime"; import { recordChannelActivity } from "openclaw/plugin-sdk/infra-runtime"; -import { buildAgentSessionKey, deriveLastRoutePolicy } from "openclaw/plugin-sdk/routing"; +import { deriveLastRoutePolicy } from "openclaw/plugin-sdk/routing"; import { DEFAULT_ACCOUNT_ID, resolveThreadSessionKeys } from "openclaw/plugin-sdk/routing"; import { logVerbose } from "openclaw/plugin-sdk/runtime-env"; import { withTelegramApiErrorLogging } from "./api-logging.js"; @@ -17,12 +17,11 @@ import { firstDefined, normalizeAllowFrom, normalizeDmAllowFromWithStore } from import { resolveTelegramInboundBody } from "./bot-message-context.body.js"; import { buildTelegramInboundContextPayload } from "./bot-message-context.session.js"; import type { BuildTelegramMessageContextParams } from "./bot-message-context.types.js"; +import { buildTypingThreadParams, resolveTelegramThreadSpec } from "./bot/helpers.js"; import { - buildTypingThreadParams, - resolveTelegramDirectPeerId, - resolveTelegramThreadSpec, -} from "./bot/helpers.js"; -import { resolveTelegramConversationRoute } from "./conversation-route.js"; + resolveTelegramConversationBaseSessionKey, + resolveTelegramConversationRoute, +} from "./conversation-route.js"; import { enforceTelegramDmAccess } from "./dm-access.js"; import { evaluateTelegramGroupBaseAccess } from "./group-access.js"; import { @@ -224,22 +223,13 @@ export const buildTelegramMessageContext = async ({ return false; }; - const baseSessionKey = isNamedAccountFallback - ? buildAgentSessionKey({ - agentId: route.agentId, - channel: "telegram", - accountId: route.accountId, - peer: { - kind: "direct", - id: resolveTelegramDirectPeerId({ - chatId, - senderId, - }), - }, - dmScope: "per-account-channel-peer", - identityLinks: freshCfg.session?.identityLinks, - }).toLowerCase() - : route.sessionKey; + const baseSessionKey = resolveTelegramConversationBaseSessionKey({ + cfg: freshCfg, + route, + chatId, + isGroup, + senderId, + }); // DMs: use thread suffix for session isolation (works regardless of dmScope) const threadKeys = dmThreadId != null diff --git a/extensions/telegram/src/bot-native-commands.ts b/extensions/telegram/src/bot-native-commands.ts index 740dc1d8c08..c496c1b97f6 100644 --- a/extensions/telegram/src/bot-native-commands.ts +++ b/extensions/telegram/src/bot-native-commands.ts @@ -63,7 +63,10 @@ import { resolveTelegramThreadSpec, } from "./bot/helpers.js"; import type { TelegramContext } from "./bot/types.js"; -import { resolveTelegramConversationRoute } from "./conversation-route.js"; +import { + resolveTelegramConversationBaseSessionKey, + resolveTelegramConversationRoute, +} from "./conversation-route.js"; import { shouldSuppressLocalTelegramExecApprovalPrompt } from "./exec-approvals.js"; import type { TelegramTransport } from "./fetch.js"; import { @@ -650,7 +653,13 @@ export const registerTelegramNativeCommands = ({ }); return; } - const baseSessionKey = route.sessionKey; + const baseSessionKey = resolveTelegramConversationBaseSessionKey({ + cfg, + route, + chatId, + isGroup, + senderId, + }); // DMs: use raw messageThreadId for thread sessions (not resolvedThreadId which is for forums) const dmThreadId = threadSpec.scope === "dm" ? threadSpec.id : undefined; const threadKeys = diff --git a/extensions/telegram/src/conversation-route.base-session-key.test.ts b/extensions/telegram/src/conversation-route.base-session-key.test.ts new file mode 100644 index 00000000000..baebab3470c --- /dev/null +++ b/extensions/telegram/src/conversation-route.base-session-key.test.ts @@ -0,0 +1,64 @@ +import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; +import { resolveThreadSessionKeys } from "openclaw/plugin-sdk/routing"; +import { describe, expect, it } from "vitest"; +import { resolveTelegramConversationBaseSessionKey } from "./conversation-route.js"; + +describe("resolveTelegramConversationBaseSessionKey", () => { + const cfg: OpenClawConfig = {}; + + it("keeps the routed session key for the default account", () => { + expect( + resolveTelegramConversationBaseSessionKey({ + cfg, + route: { + agentId: "main", + accountId: "default", + matchedBy: "default", + sessionKey: "agent:main:main", + }, + chatId: 12345, + isGroup: false, + senderId: 12345, + }), + ).toBe("agent:main:main"); + }); + + it("uses the per-account fallback key for named-account DMs without an explicit binding", () => { + expect( + resolveTelegramConversationBaseSessionKey({ + cfg, + route: { + agentId: "main", + accountId: "personal", + matchedBy: "default", + sessionKey: "agent:main:main", + }, + chatId: 12345, + isGroup: false, + senderId: 12345, + }), + ).toBe("agent:main:telegram:personal:direct:12345"); + }); + + it("keeps DM topic isolation on the named-account fallback key", () => { + const baseSessionKey = resolveTelegramConversationBaseSessionKey({ + cfg, + route: { + agentId: "main", + accountId: "personal", + matchedBy: "default", + sessionKey: "agent:main:main", + }, + chatId: 12345, + isGroup: false, + senderId: 12345, + }); + + expect( + resolveThreadSessionKeys({ + baseSessionKey, + threadId: "12345:99", + }).sessionKey, + ).toBe("agent:main:telegram:personal:direct:12345:thread:12345:99"); + }); +}); diff --git a/extensions/telegram/src/conversation-route.ts b/extensions/telegram/src/conversation-route.ts index fc06221936f..26c3b039312 100644 --- a/extensions/telegram/src/conversation-route.ts +++ b/extensions/telegram/src/conversation-route.ts @@ -9,6 +9,7 @@ import { } from "openclaw/plugin-sdk/routing"; import { buildAgentMainSessionKey, + DEFAULT_ACCOUNT_ID, resolveAgentIdFromSessionKey, sanitizeAgentId, } from "openclaw/plugin-sdk/routing"; @@ -148,3 +149,34 @@ export function resolveTelegramConversationRoute(params: { configuredBindingSessionKey, }; } + +export function resolveTelegramConversationBaseSessionKey(params: { + cfg: OpenClawConfig; + route: Pick< + ReturnType["route"], + "agentId" | "accountId" | "matchedBy" | "sessionKey" + >; + chatId: number | string; + isGroup: boolean; + senderId?: string | number | null; +}): string { + const isNamedAccountFallback = + params.route.accountId !== DEFAULT_ACCOUNT_ID && params.route.matchedBy === "default"; + if (!isNamedAccountFallback || params.isGroup) { + return params.route.sessionKey; + } + return buildAgentSessionKey({ + agentId: params.route.agentId, + channel: "telegram", + accountId: params.route.accountId, + peer: { + kind: "direct", + id: resolveTelegramDirectPeerId({ + chatId: params.chatId, + senderId: params.senderId, + }), + }, + dmScope: "per-account-channel-peer", + identityLinks: params.cfg.session?.identityLinks, + }).toLowerCase(); +}