diff --git a/CHANGELOG.md b/CHANGELOG.md index c000acdde55..7ff8fe951d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ Docs: https://docs.openclaw.ai - Channels/status reactions: remove stale non-terminal lifecycle reactions when a run reaches done or error, so Discord does not leave a permanent thinking emoji after completion. Fixes #75458. Thanks @davelutztx. - Discord/doctor: migrate unsupported per-channel `agentId` entries under guild channel config into top-level `bindings[]` routes, so `openclaw doctor --fix` preserves the intended agent route instead of stripping it as an unknown key. Fixes #62455. Thanks @lobster-biscuit. - Discord/DMs: set inbound direct-message `ctx.To` to the semantic `user:` target while keeping delivery routed through the DM channel, so mirror and recovery paths do not treat DMs as channel conversations. Fixes #68126. Thanks @illuminate0623. +- Discord/DMs: keep no-guild inbound messages on direct-message routing when Discord channel lookup is temporarily unavailable, preventing degraded DMs from forking into channel sessions. Fixes #59817. Thanks @DooPeePey. - Gateway/config: log config health-state write failures instead of silently hiding config observe-recovery write errors. Thanks @sallyom. - Diagnostics: reset stuck-session timers on reply, tool, status, block, and ACP progress events, and back off repeated `session.stuck` diagnostics while a session remains unchanged. Supersedes #72010. Thanks @rubencu. diff --git a/extensions/discord/src/monitor/message-handler.preflight.test.ts b/extensions/discord/src/monitor/message-handler.preflight.test.ts index 79aeb96db28..1f19cea7ee9 100644 --- a/extensions/discord/src/monitor/message-handler.preflight.test.ts +++ b/extensions/discord/src/monitor/message-handler.preflight.test.ts @@ -117,6 +117,12 @@ function createDmClient(channelId: string): DiscordClient { } as unknown as DiscordClient; } +function createMissingChannelClient(): DiscordClient { + return { + fetchChannel: async () => null, + } as unknown as DiscordClient; +} + async function runThreadBoundPreflight(params: { threadId: string; parentId: string; @@ -203,6 +209,26 @@ async function runDmPreflight(params: { }); } +async function runUnresolvedDmPreflight(params: { + cfg?: import("openclaw/plugin-sdk/config-types").OpenClawConfig; + channelId: string; + message: import("../internal/discord.js").Message; + discordConfig: DiscordConfig; +}) { + return preflightDiscordMessage({ + ...createPreflightArgs({ + cfg: params.cfg ?? DEFAULT_PREFLIGHT_CFG, + discordConfig: params.discordConfig, + data: { + channel_id: params.channelId, + author: params.message.author, + message: params.message, + } as DiscordMessageEvent, + client: createMissingChannelClient(), + }), + }); +} + async function runMentionOnlyBotPreflight(params: { channelId: string; guildId: string; @@ -483,6 +509,38 @@ describe("preflightDiscordMessage", () => { expect(result?.preflightAudioTranscript).toBe("hello openclaw from dm audio"); }); + it("keeps no-guild messages direct when channel lookup is unavailable", async () => { + const result = await runUnresolvedDmPreflight({ + cfg: { + ...DEFAULT_PREFLIGHT_CFG, + session: { + ...DEFAULT_PREFLIGHT_CFG.session, + dmScope: "per-channel-peer", + }, + }, + channelId: "dm-channel-unresolved-1", + message: createDiscordMessage({ + id: "m-dm-unresolved-1", + channelId: "dm-channel-unresolved-1", + content: "hello from a degraded dm", + author: { + id: "user-1", + bot: false, + username: "alice", + }, + }), + discordConfig: { + dmPolicy: "open", + } as DiscordConfig, + }); + + expect(result).not.toBeNull(); + expect(result?.channelInfo).toBeNull(); + expect(result?.isDirectMessage).toBe(true); + expect(result?.isGroupDm).toBe(false); + expect(result?.route.sessionKey).toBe("agent:main:discord:direct:user-1"); + }); + it("falls back to the default discord account for omitted-account dm authorization", async () => { const message = createDiscordMessage({ id: "m-dm-default-account", diff --git a/extensions/discord/src/monitor/message-handler.preflight.ts b/extensions/discord/src/monitor/message-handler.preflight.ts index 56c4d8deb5c..d7a7ada4986 100644 --- a/extensions/discord/src/monitor/message-handler.preflight.ts +++ b/extensions/discord/src/monitor/message-handler.preflight.ts @@ -70,6 +70,17 @@ export { shouldIgnoreBoundThreadWebhookMessage, } from "./message-handler.preflight-helpers.js"; +function resolveDiscordPreflightConversationKind(params: { + isGuildMessage: boolean; + channelType?: ChannelType; +}) { + const isGroupDm = params.channelType === ChannelType.GroupDM; + const isDirectMessage = + params.channelType === ChannelType.DM || + (!params.isGuildMessage && !isGroupDm && params.channelType == null); + return { isDirectMessage, isGroupDm }; +} + export async function preflightDiscordMessage( params: DiscordMessagePreflightParams, ): Promise { @@ -137,8 +148,10 @@ export async function preflightDiscordMessage( if (isPreflightAborted(params.abortSignal)) { return null; } - const isDirectMessage = channelInfo?.type === ChannelType.DM; - const isGroupDm = channelInfo?.type === ChannelType.GroupDM; + const { isDirectMessage, isGroupDm } = resolveDiscordPreflightConversationKind({ + isGuildMessage, + channelType: channelInfo?.type, + }); const messageText = resolveDiscordMessageText(message, { includeForwarded: true, });