diff --git a/CHANGELOG.md b/CHANGELOG.md index c34663e41..bddc24771 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -234,6 +234,7 @@ Docs: https://docs.openclaw.ai - Telegram: unify message-like inbound handling so `message` and `channel_post` share the same dedupe/access/media pipeline and remain behaviorally consistent. (#20591) Thanks @obviyus. - Telegram/Agents: gate exec/bash tool-failure warnings behind verbose mode so default Telegram replies stay clean while verbose sessions still surface diagnostics. (#20560) Thanks @obviyus. - Telegram/Cron/Heartbeat: honor explicit Telegram topic targets in cron and heartbeat delivery (`:topic:`) so scheduled sends land in the configured topic instead of the last active thread. (#19367) Thanks @Lukavyi. +- Telegram/DM routing: prevent DM inbound origin metadata from leaking into main-session `lastRoute` updates and normalize DM `lastRoute.to` to provider-prefixed `telegram:`. (#19491) thanks @guirguispierre. - Gateway/Daemon: forward `TMPDIR` into installed service environments so macOS LaunchAgent gateway runs can open SQLite temp/journal files reliably instead of failing with `SQLITE_CANTOPEN`. (#20512) Thanks @Clawborn. - Agents/Billing: include the active model that produced a billing error in user-facing billing messages (for example, `OpenAI (gpt-5.3)`) across payload, failover, and lifecycle error paths, so users can identify exactly which key needs credits. (#20510) Thanks @echoVic. - Gateway/TUI: honor `agents.defaults.blockStreamingDefault` for `chat.send` by removing the hardcoded block-streaming disable override, so replies can use configured block-mode delivery. (#19693) Thanks @neipor. diff --git a/src/channels/session.test.ts b/src/channels/session.test.ts new file mode 100644 index 000000000..0be177f85 --- /dev/null +++ b/src/channels/session.test.ts @@ -0,0 +1,78 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { MsgContext } from "../auto-reply/templating.js"; + +const recordSessionMetaFromInboundMock = vi.fn((_args?: unknown) => Promise.resolve(undefined)); +const updateLastRouteMock = vi.fn((_args?: unknown) => Promise.resolve(undefined)); + +vi.mock("../config/sessions.js", () => ({ + recordSessionMetaFromInbound: (args: unknown) => recordSessionMetaFromInboundMock(args), + updateLastRoute: (args: unknown) => updateLastRouteMock(args), +})); + +describe("recordInboundSession", () => { + const ctx: MsgContext = { + Provider: "telegram", + From: "telegram:1234", + SessionKey: "agent:main:telegram:1234:thread:42", + OriginatingTo: "telegram:1234", + }; + + beforeEach(() => { + recordSessionMetaFromInboundMock.mockClear(); + updateLastRouteMock.mockClear(); + }); + + it("does not pass ctx when updating a different session key", async () => { + const { recordInboundSession } = await import("./session.js"); + + await recordInboundSession({ + storePath: "/tmp/openclaw-session-store.json", + sessionKey: "agent:main:telegram:1234:thread:42", + ctx, + updateLastRoute: { + sessionKey: "agent:main:main", + channel: "telegram", + to: "telegram:1234", + }, + onRecordError: vi.fn(), + }); + + expect(updateLastRouteMock).toHaveBeenCalledWith( + expect.objectContaining({ + sessionKey: "agent:main:main", + ctx: undefined, + deliveryContext: expect.objectContaining({ + channel: "telegram", + to: "telegram:1234", + }), + }), + ); + }); + + it("passes ctx when updating the same session key", async () => { + const { recordInboundSession } = await import("./session.js"); + + await recordInboundSession({ + storePath: "/tmp/openclaw-session-store.json", + sessionKey: "agent:main:telegram:1234:thread:42", + ctx, + updateLastRoute: { + sessionKey: "agent:main:telegram:1234:thread:42", + channel: "telegram", + to: "telegram:1234", + }, + onRecordError: vi.fn(), + }); + + expect(updateLastRouteMock).toHaveBeenCalledWith( + expect.objectContaining({ + sessionKey: "agent:main:telegram:1234:thread:42", + ctx, + deliveryContext: expect.objectContaining({ + channel: "telegram", + to: "telegram:1234", + }), + }), + ); + }); +}); diff --git a/src/channels/session.ts b/src/channels/session.ts index 8aeb371db..c2f2433be 100644 --- a/src/channels/session.ts +++ b/src/channels/session.ts @@ -45,7 +45,8 @@ export async function recordInboundSession(params: { accountId: update.accountId, threadId: update.threadId, }, - ctx, + // Avoid leaking inbound origin metadata into a different target session. + ctx: update.sessionKey === sessionKey ? ctx : undefined, groupResolution, }); } diff --git a/src/telegram/bot-message-context.dm-topic-threadid.test.ts b/src/telegram/bot-message-context.dm-topic-threadid.test.ts index 54d962141..ba566898d 100644 --- a/src/telegram/bot-message-context.dm-topic-threadid.test.ts +++ b/src/telegram/bot-message-context.dm-topic-threadid.test.ts @@ -41,8 +41,9 @@ describe("buildTelegramMessageContext DM topic threadId in deliveryContext (#889 expect(recordInboundSessionMock).toHaveBeenCalled(); // Check that updateLastRoute includes threadId - const updateLastRoute = getUpdateLastRoute() as { threadId?: string } | undefined; + const updateLastRoute = getUpdateLastRoute() as { threadId?: string; to?: string } | undefined; expect(updateLastRoute).toBeDefined(); + expect(updateLastRoute?.to).toBe("telegram:1234"); expect(updateLastRoute?.threadId).toBe("42"); }); @@ -57,8 +58,9 @@ describe("buildTelegramMessageContext DM topic threadId in deliveryContext (#889 expect(recordInboundSessionMock).toHaveBeenCalled(); // Check that updateLastRoute does NOT include threadId - const updateLastRoute = getUpdateLastRoute() as { threadId?: string } | undefined; + const updateLastRoute = getUpdateLastRoute() as { threadId?: string; to?: string } | undefined; expect(updateLastRoute).toBeDefined(); + expect(updateLastRoute?.to).toBe("telegram:1234"); expect(updateLastRoute?.threadId).toBeUndefined(); }); diff --git a/src/telegram/bot-message-context.ts b/src/telegram/bot-message-context.ts index 312f12f8e..ea32380b1 100644 --- a/src/telegram/bot-message-context.ts +++ b/src/telegram/bot-message-context.ts @@ -765,7 +765,7 @@ export const buildTelegramMessageContext = async ({ ? { sessionKey: route.mainSessionKey, channel: "telegram", - to: String(chatId), + to: `telegram:${chatId}`, accountId: route.accountId, // Preserve DM topic threadId for replies (fixes #8891) threadId: dmThreadId != null ? String(dmThreadId) : undefined,