From b0d4c9b7219896c470c2959f1bdcef8a567b02f1 Mon Sep 17 00:00:00 2001 From: Shakker Date: Tue, 17 Feb 2026 13:47:16 +0000 Subject: [PATCH] fix(discord): preserve DM lastRoute user target --- .../monitor/message-handler.process.test.ts | 83 +++++++++++++++++++ .../monitor/message-handler.process.ts | 9 +- 2 files changed, 89 insertions(+), 3 deletions(-) diff --git a/src/discord/monitor/message-handler.process.test.ts b/src/discord/monitor/message-handler.process.test.ts index 75cf28e5175..6b92fc597f8 100644 --- a/src/discord/monitor/message-handler.process.test.ts +++ b/src/discord/monitor/message-handler.process.test.ts @@ -71,6 +71,22 @@ beforeEach(() => { resolveStorePath.mockReturnValue("/tmp/openclaw-discord-process-test-sessions.json"); }); +function getLastRouteUpdate(): + | { sessionKey?: string; channel?: string; to?: string; accountId?: string } + | undefined { + const params = recordInboundSession.mock.calls.at(-1)?.[0] as + | { + updateLastRoute?: { + sessionKey?: string; + channel?: string; + to?: string; + accountId?: string; + }; + } + | undefined; + return params?.updateLastRoute; +} + describe("processDiscordMessage ack reactions", () => { it("skips ack reactions for group-mentions when mentions are not required", async () => { const ctx = await createBaseContext({ @@ -170,3 +186,70 @@ describe("processDiscordMessage ack reactions", () => { expect(emojis).toContain("✅"); }); }); + +describe("processDiscordMessage session routing", () => { + it("stores DM lastRoute with user target for direct-session continuity", async () => { + const ctx = await createBaseContext({ + data: { guild: null }, + channelInfo: null, + channelName: undefined, + isGuildMessage: false, + isDirectMessage: true, + isGroupDm: false, + shouldRequireMention: false, + canDetectMention: false, + effectiveWasMentioned: false, + displayChannelSlug: "", + guildInfo: null, + guildSlug: "", + message: { + id: "m1", + channelId: "dm1", + timestamp: new Date().toISOString(), + attachments: [], + }, + messageChannelId: "dm1", + baseSessionKey: "agent:main:discord:direct:u1", + route: { + agentId: "main", + channel: "discord", + accountId: "default", + sessionKey: "agent:main:discord:direct:u1", + mainSessionKey: "agent:main:main", + }, + }); + + // oxlint-disable-next-line typescript/no-explicit-any + await processDiscordMessage(ctx as any); + + expect(getLastRouteUpdate()).toEqual({ + sessionKey: "agent:main:discord:direct:u1", + channel: "discord", + to: "user:U1", + accountId: "default", + }); + }); + + it("stores group lastRoute with channel target", async () => { + const ctx = await createBaseContext({ + baseSessionKey: "agent:main:discord:channel:c1", + route: { + agentId: "main", + channel: "discord", + accountId: "default", + sessionKey: "agent:main:discord:channel:c1", + mainSessionKey: "agent:main:main", + }, + }); + + // oxlint-disable-next-line typescript/no-explicit-any + await processDiscordMessage(ctx as any); + + expect(getLastRouteUpdate()).toEqual({ + sessionKey: "agent:main:discord:channel:c1", + channel: "discord", + to: "channel:c1", + accountId: "default", + }); + }); +}); diff --git a/src/discord/monitor/message-handler.process.ts b/src/discord/monitor/message-handler.process.ts index e307219d947..c627604a4b4 100644 --- a/src/discord/monitor/message-handler.process.ts +++ b/src/discord/monitor/message-handler.process.ts @@ -503,6 +503,8 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext) runtime.error?.(danger("discord: missing reply target")); return; } + // Keep DM routes user-addressed so follow-up sends resolve direct session keys. + const lastRouteTo = isDirectMessage ? `user:${author.id}` : effectiveTo; const inboundHistory = shouldIncludeChannelHistory && historyLimit > 0 @@ -553,15 +555,16 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext) OriginatingChannel: "discord" as const, OriginatingTo: autoThreadContext?.OriginatingTo ?? replyTarget, }); + const persistedSessionKey = ctxPayload.SessionKey ?? route.sessionKey; await recordInboundSession({ storePath, - sessionKey: ctxPayload.SessionKey ?? route.sessionKey, + sessionKey: persistedSessionKey, ctx: ctxPayload, updateLastRoute: { - sessionKey: ctxPayload.SessionKey ?? route.sessionKey, + sessionKey: persistedSessionKey, channel: "discord", - to: effectiveTo, + to: lastRouteTo, accountId: route.accountId, }, onRecordError: (err) => {