fix(discord): keep degraded DMs on direct routes

This commit is contained in:
Peter Steinberger
2026-05-02 02:29:52 +01:00
parent 6922500382
commit 3451ea9761
3 changed files with 74 additions and 2 deletions

View File

@@ -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:<id>` 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.

View File

@@ -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",

View File

@@ -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<DiscordMessagePreflightContext | null> {
@@ -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,
});