fix(discord): fail closed when bot identity is unavailable

Fail Discord startup closed when the bot identity cannot be resolved, and keep mention gating active when configured mention patterns can still detect required mentions without a bot id.\n\nFixes #42219. Carries forward source PRs #46856 by @education-01 and #49218 by @BenediktSchackenberg. #46847 was already closed as a duplicate; #42675 was security-routed separately and left out of the replacement source.
This commit is contained in:
openclaw-clownfish[bot]
2026-04-29 01:55:04 -07:00
committed by GitHub
parent c881e0a176
commit 928698d388
5 changed files with 112 additions and 12 deletions

View File

@@ -17,6 +17,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Channels/Discord: fail startup closed when Discord cannot resolve the bot's own identity and keep mention gating active when only configured mention patterns can detect mentions, so the provider no longer continues with a missing bot id. Fixes #42219; carries forward #46856 and #49218. Thanks @education-01 and @BenediktSchackenberg.
- Browser/gateway: ignore Playwright dialog-close races from `Page.handleJavaScriptDialog` so browser automation no longer crashes the Gateway when a dialog disappears before Playwright accepts it. (#40067) Thanks @randyjtw.
- Cron/Gateway: defer missed isolated agent-turn catch-up out of the channel startup window, so overdue cron work cannot starve Discord or Telegram while providers connect after a restart. Thanks @vincentkoc.
- Plugins/runtime-deps: prune stale `openclaw-unknown-*` bundled runtime dependency roots during Gateway startup while keeping recent or locked roots, so old staging debris cannot keep growing across restarts. Thanks @vincentkoc.

View File

@@ -797,6 +797,55 @@ describe("preflightDiscordMessage", () => {
expect(result).not.toBeNull();
});
it("does not mask mention gating when bot id is missing but mention patterns can detect", async () => {
const channelId = "channel-missing-bot-id-mention-gate";
const guildId = "guild-missing-bot-id-mention-gate";
const message = createDiscordMessage({
id: "m-missing-bot-id-mention-gate",
channelId,
content: "general update without the configured mention",
author: {
id: "user-1",
bot: false,
username: "Alice",
},
});
const result = await preflightDiscordMessage({
...createPreflightArgs({
cfg: {
...DEFAULT_PREFLIGHT_CFG,
messages: {
groupChat: {
mentionPatterns: ["openclaw"],
},
},
} as import("openclaw/plugin-sdk/config-types").OpenClawConfig,
discordConfig: {} as DiscordConfig,
data: createGuildEvent({
channelId,
guildId,
author: message.author,
message,
}),
client: createGuildTextClient(channelId),
}),
botUserId: undefined,
guildEntries: {
[guildId]: {
channels: {
[channelId]: {
enabled: true,
requireMention: true,
},
},
},
},
});
expect(result).toBeNull();
});
it("treats @everyone as a mention when requireMention is true", async () => {
const channelId = "channel-everyone-mention";
const guildId = "guild-everyone-mention";

View File

@@ -1009,9 +1009,9 @@ export async function preflightDiscordMessage(
`[discord-preflight] shouldRequireMention=${shouldRequireMention} baseRequireMention=${shouldRequireMentionByConfig} boundThreadSession=${isBoundThreadSession} mentionDecision.shouldSkip=${mentionDecision.shouldSkip} wasMentioned=${wasMentioned}`,
);
if (isGuildMessage && shouldRequireMention) {
if (botId && mentionDecision.shouldSkip) {
if (mentionDecision.shouldSkip) {
logDebug(`[discord-preflight] drop: no-mention`);
logVerbose(`discord: drop guild message (mention required, botId=${botId})`);
logVerbose(`discord: drop guild message (mention required, botId=${botId ?? "<missing>"})`);
logger.info(
{
channelId: messageChannelId,

View File

@@ -219,21 +219,35 @@ export async function fetchDiscordBotIdentity(params: {
logStartupPhase: (phase: string, details?: string) => void;
}) {
params.logStartupPhase("fetch-bot-identity:start");
let botUser: Awaited<ReturnType<typeof params.client.fetchUser>>;
try {
const botUser = await params.client.fetchUser("@me");
const botUserId = botUser?.id;
const botUserName =
normalizeOptionalString(botUser?.username) ?? normalizeOptionalString(botUser?.globalName);
params.logStartupPhase(
"fetch-bot-identity:done",
`botUserId=${botUserId ?? "<missing>"} botUserName=${botUserName ?? "<missing>"}`,
);
return { botUserId, botUserName };
botUser = await params.client.fetchUser("@me");
} catch (err) {
params.runtime.error?.(danger(`discord: failed to fetch bot identity: ${String(err)}`));
params.logStartupPhase("fetch-bot-identity:error", String(err));
return { botUserId: undefined, botUserName: undefined };
throw new Error("Failed to resolve Discord bot identity", { cause: err });
}
const botUserRecord = botUser as
| { id?: unknown; username?: unknown; globalName?: unknown }
| null
| undefined;
const botUserId = normalizeOptionalString(botUserRecord?.id);
const botUserName =
normalizeOptionalString(botUserRecord?.username) ??
normalizeOptionalString(botUserRecord?.globalName);
if (!botUserId) {
const details = 'fetchUser("@me") returned no usable id';
params.runtime.error?.(danger(`discord: failed to fetch bot identity: ${details}`));
params.logStartupPhase("fetch-bot-identity:error", details);
throw new Error("Failed to resolve Discord bot identity");
}
params.logStartupPhase(
"fetch-bot-identity:done",
`botUserId=${botUserId} botUserName=${botUserName ?? "<missing>"}`,
);
return { botUserId, botUserName };
}
export function registerDiscordMonitorListeners(params: {

View File

@@ -307,6 +307,42 @@ describe("monitorDiscordProvider", () => {
);
});
it("fails closed before lifecycle when Discord bot identity fetch rejects", async () => {
const runtime = baseRuntime();
clientFetchUserMock.mockRejectedValueOnce(new Error("identity offline"));
await expect(
monitorDiscordProvider({
config: baseConfig(),
runtime,
}),
).rejects.toThrow("Failed to resolve Discord bot identity");
expect(createDiscordMessageHandlerMock).not.toHaveBeenCalled();
expect(monitorLifecycleMock).not.toHaveBeenCalled();
expect(createdBindingManagers).toHaveLength(1);
expect(createdBindingManagers[0]?.stop).toHaveBeenCalledTimes(1);
expect(runtime.error).toHaveBeenCalledWith(expect.stringContaining("identity offline"));
});
it("fails closed before lifecycle when Discord bot identity has no usable id", async () => {
const runtime = baseRuntime();
clientFetchUserMock.mockResolvedValueOnce({ username: "Molty" } as never);
await expect(
monitorDiscordProvider({
config: baseConfig(),
runtime,
}),
).rejects.toThrow("Failed to resolve Discord bot identity");
expect(createDiscordMessageHandlerMock).not.toHaveBeenCalled();
expect(monitorLifecycleMock).not.toHaveBeenCalled();
expect(createdBindingManagers).toHaveLength(1);
expect(createdBindingManagers[0]?.stop).toHaveBeenCalledTimes(1);
expect(runtime.error).toHaveBeenCalledWith(expect.stringContaining("no usable id"));
});
it("does not double-stop thread bindings when lifecycle performs cleanup", async () => {
await monitorDiscordProvider({
config: baseConfig(),