From 317075ef3d0d1ee38e4d8a1b7d52c20e5402bd4d Mon Sep 17 00:00:00 2001 From: bmendonca3 Date: Mon, 2 Mar 2026 14:56:26 -0700 Subject: [PATCH] telegram: route dm sessions by sender id --- .../bot-message-context.dm-threads.test.ts | 45 ++++++++++++++++++- src/telegram/bot-message-context.ts | 7 ++- src/telegram/bot/helpers.test.ts | 15 +++++++ src/telegram/bot/helpers.ts | 18 ++++++++ 4 files changed, 82 insertions(+), 3 deletions(-) diff --git a/src/telegram/bot-message-context.dm-threads.test.ts b/src/telegram/bot-message-context.dm-threads.test.ts index 26812b4c891..eba4c19c88c 100644 --- a/src/telegram/bot-message-context.dm-threads.test.ts +++ b/src/telegram/bot-message-context.dm-threads.test.ts @@ -1,4 +1,5 @@ -import { describe, expect, it } from "vitest"; +import { afterEach, describe, expect, it } from "vitest"; +import { clearRuntimeConfigSnapshot, setRuntimeConfigSnapshot } from "../config/config.js"; import { buildTelegramMessageContextForTest } from "./bot-message-context.test-harness.js"; describe("buildTelegramMessageContext dm thread sessions", () => { @@ -104,3 +105,45 @@ describe("buildTelegramMessageContext group sessions without forum", () => { expect(ctx?.ctxPayload?.MessageThreadId).toBe(99); }); }); + +describe("buildTelegramMessageContext direct peer routing", () => { + afterEach(() => { + clearRuntimeConfigSnapshot(); + }); + + it("isolates dm sessions by sender id when chat id differs", async () => { + const runtimeCfg = { + agents: { defaults: { model: "anthropic/claude-opus-4-5", workspace: "/tmp/openclaw" } }, + channels: { telegram: {} }, + messages: { groupChat: { mentionPatterns: [] } }, + session: { dmScope: "per-channel-peer" as const }, + }; + setRuntimeConfigSnapshot(runtimeCfg); + + const baseMessage = { + chat: { id: 777777777, type: "private" as const }, + date: 1700000000, + text: "hello", + }; + + const first = await buildTelegramMessageContextForTest({ + cfg: runtimeCfg, + message: { + ...baseMessage, + message_id: 1, + from: { id: 123456789, first_name: "Alice" }, + }, + }); + const second = await buildTelegramMessageContextForTest({ + cfg: runtimeCfg, + message: { + ...baseMessage, + message_id: 2, + from: { id: 987654321, first_name: "Bob" }, + }, + }); + + expect(first?.ctxPayload?.SessionKey).toBe("agent:main:telegram:direct:123456789"); + expect(second?.ctxPayload?.SessionKey).toBe("agent:main:telegram:direct:987654321"); + }); +}); diff --git a/src/telegram/bot-message-context.ts b/src/telegram/bot-message-context.ts index 9e4205fcc23..bca275ee2cc 100644 --- a/src/telegram/bot-message-context.ts +++ b/src/telegram/bot-message-context.ts @@ -52,6 +52,7 @@ import { buildGroupLabel, buildSenderLabel, buildSenderName, + resolveTelegramDirectPeerId, buildTelegramGroupFrom, buildTelegramGroupPeerId, buildTelegramParentPeer, @@ -174,6 +175,7 @@ export const buildTelegramMessageContext = async ({ const msg = primaryCtx.message; const chatId = msg.chat.id; const isGroup = msg.chat.type === "group" || msg.chat.type === "supergroup"; + const senderId = msg.from?.id ? String(msg.from.id) : ""; const messageThreadId = (msg as { message_thread_id?: number }).message_thread_id; const isForum = (msg.chat as { is_forum?: boolean }).is_forum === true; const threadSpec = resolveTelegramThreadSpec({ @@ -191,7 +193,9 @@ export const buildTelegramMessageContext = async ({ !isGroup && groupConfig && "dmPolicy" in groupConfig ? (groupConfig.dmPolicy ?? dmPolicy) : dmPolicy; - const peerId = isGroup ? buildTelegramGroupPeerId(chatId, resolvedThreadId) : String(chatId); + const peerId = isGroup + ? buildTelegramGroupPeerId(chatId, resolvedThreadId) + : resolveTelegramDirectPeerId({ chatId, senderId }); const parentPeer = buildTelegramParentPeer({ isGroup, resolvedThreadId, chatId }); // Fresh config for bindings lookup; other routing inputs are payload-derived. const route = resolveAgentRoute({ @@ -235,7 +239,6 @@ export const buildTelegramMessageContext = async ({ // Group sender checks are explicit and must not inherit DM pairing-store entries. const effectiveGroupAllow = normalizeAllowFrom(groupAllowOverride ?? groupAllowFrom); const hasGroupAllowOverride = typeof groupAllowOverride !== "undefined"; - const senderId = msg.from?.id ? String(msg.from.id) : ""; const senderUsername = msg.from?.username ?? ""; const baseAccess = evaluateTelegramGroupBaseAccess({ isGroup, diff --git a/src/telegram/bot/helpers.test.ts b/src/telegram/bot/helpers.test.ts index ffbd0c3efff..c83311980b2 100644 --- a/src/telegram/bot/helpers.test.ts +++ b/src/telegram/bot/helpers.test.ts @@ -5,6 +5,7 @@ import { describeReplyTarget, expandTextLinks, normalizeForwardedContext, + resolveTelegramDirectPeerId, resolveTelegramForumThreadId, } from "./helpers.js"; @@ -53,6 +54,20 @@ describe("buildTypingThreadParams", () => { }); }); +describe("resolveTelegramDirectPeerId", () => { + it("prefers sender id when available", () => { + expect(resolveTelegramDirectPeerId({ chatId: 777777777, senderId: 123456789 })).toBe( + "123456789", + ); + }); + + it("falls back to chat id when sender id is missing", () => { + expect(resolveTelegramDirectPeerId({ chatId: 777777777, senderId: undefined })).toBe( + "777777777", + ); + }); +}); + describe("thread id normalization", () => { it.each([ { diff --git a/src/telegram/bot/helpers.ts b/src/telegram/bot/helpers.ts index 24e2ba47e70..1f078c94c35 100644 --- a/src/telegram/bot/helpers.ts +++ b/src/telegram/bot/helpers.ts @@ -175,6 +175,24 @@ export function buildTelegramGroupPeerId(chatId: number | string, messageThreadI return messageThreadId != null ? `${chatId}:topic:${messageThreadId}` : String(chatId); } +/** + * Resolve the direct-message peer identifier for Telegram routing/session keys. + * + * In some Telegram DM deliveries (for example certain business/chat bridge flows), + * `chat.id` can differ from the actual sender user id. Prefer sender id when present + * so per-peer DM scopes isolate users correctly. + */ +export function resolveTelegramDirectPeerId(params: { + chatId: number | string; + senderId?: number | string | null; +}) { + const senderId = params.senderId != null ? String(params.senderId).trim() : ""; + if (senderId) { + return senderId; + } + return String(params.chatId); +} + export function buildTelegramGroupFrom(chatId: number | string, messageThreadId?: number) { return `telegram:group:${buildTelegramGroupPeerId(chatId, messageThreadId)}`; }