diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a508fdfbd6..4bd305e417c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -102,6 +102,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Discord/gateway: measure heartbeat ACK timeouts from the actual heartbeat send, preventing late initial heartbeats from triggering false reconnect loops while the channel is still awaiting readiness. Fixes #77668. (#78087) Thanks @bryce-d-greybeard and @NikolaFC. +- Discord/guilds: route plain text control commands such as `/steer` through the normal authorization and mention gate instead of silently dropping them before an agent session can see them. Fixes #78080. Thanks @ramitrkar-hash. - Control UI/Sessions: make the compaction count a compact `N Checkpoint(s)` disclosure and show expanded session-level details with modern checkpoint history cards across responsive table layouts. Thanks @BunsDev. - Control UI/performance: keep chat and channel tabs responsive while history payloads and channel probes are slow, label partial channel status, and record slow chat/config render timings in the event log. Thanks @BunsDev. - Control UI/sessions: fire the documented `/new` command and lifecycle hooks only for explicit Control UI session creation, restoring session-memory and custom hook capture without changing SDK parent-session creates. Fixes #76957. Thanks @BunsDev. diff --git a/extensions/discord/src/monitor/message-handler.preflight.test-helpers.ts b/extensions/discord/src/monitor/message-handler.preflight.test-helpers.ts index b1d3c433e96..ac010821813 100644 --- a/extensions/discord/src/monitor/message-handler.preflight.test-helpers.ts +++ b/extensions/discord/src/monitor/message-handler.preflight.test-helpers.ts @@ -65,9 +65,11 @@ export function createDiscordMessage(params: { mentionedEveryone?: boolean; attachments?: Array>; webhookId?: string; + type?: import("../internal/discord.js").MessageType; }): import("../internal/discord.js").Message { return { id: params.id, + type: params.type, content: params.content, timestamp: new Date().toISOString(), channelId: params.channelId, diff --git a/extensions/discord/src/monitor/message-handler.preflight.test.ts b/extensions/discord/src/monitor/message-handler.preflight.test.ts index 2b805d61ae5..38254f59827 100644 --- a/extensions/discord/src/monitor/message-handler.preflight.test.ts +++ b/extensions/discord/src/monitor/message-handler.preflight.test.ts @@ -1,5 +1,5 @@ import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import { ChannelType } from "../internal/discord.js"; +import { ChannelType, MessageType } from "../internal/discord.js"; import { createPartialDiscordChannelWithThrowingGetters } from "../test-support/partial-channel.js"; const transcribeFirstAudioMock = vi.hoisted(() => vi.fn()); @@ -966,6 +966,95 @@ describe("preflightDiscordMessage", () => { expect(result).not.toBeNull(); }); + it("routes ordinary guild text control commands through authorization instead of dropping them", async () => { + const channelId = "channel-text-control-command"; + const guildId = "guild-text-control-command"; + const message = createDiscordMessage({ + id: "m-text-control-command", + channelId, + content: "/steer keep digging", + author: { + id: "user-1", + bot: false, + username: "Alice", + }, + }); + + const result = await preflightDiscordMessage({ + ...createPreflightArgs({ + cfg: DEFAULT_PREFLIGHT_CFG, + discordConfig: {} as DiscordConfig, + data: createGuildEvent({ + channelId, + guildId, + author: message.author, + message, + }), + client: createGuildTextClient(channelId), + }), + allowFrom: ["discord:user-1"], + guildEntries: { + [guildId]: { + channels: { + [channelId]: { + enabled: true, + requireMention: true, + }, + }, + }, + }, + }); + + expect(result).not.toBeNull(); + expect(result?.baseText).toBe("/steer keep digging"); + expect(result?.commandAuthorized).toBe(true); + expect(result?.shouldRequireMention).toBe(true); + expect(result?.shouldBypassMention).toBe(true); + }); + + it("still drops Discord native command echo messages", async () => { + const channelId = "channel-native-command-echo"; + const guildId = "guild-native-command-echo"; + const message = createDiscordMessage({ + id: "m-native-command-echo", + channelId, + content: "/steer keep digging", + type: MessageType.ChatInputCommand, + author: { + id: "user-1", + bot: false, + username: "Alice", + }, + }); + + const result = await preflightDiscordMessage({ + ...createPreflightArgs({ + cfg: DEFAULT_PREFLIGHT_CFG, + discordConfig: {} as DiscordConfig, + data: createGuildEvent({ + channelId, + guildId, + author: message.author, + message, + }), + client: createGuildTextClient(channelId), + }), + allowFrom: ["discord:user-1"], + guildEntries: { + [guildId]: { + channels: { + [channelId]: { + enabled: true, + requireMention: true, + }, + }, + }, + }, + }); + + expect(result).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"; diff --git a/extensions/discord/src/monitor/message-handler.preflight.ts b/extensions/discord/src/monitor/message-handler.preflight.ts index 1d0d0cad50a..cb21561a807 100644 --- a/extensions/discord/src/monitor/message-handler.preflight.ts +++ b/extensions/discord/src/monitor/message-handler.preflight.ts @@ -230,13 +230,6 @@ export async function preflightDiscordMessage( includeForwarded: false, }); - // Intercept text-only slash commands (e.g. user typing "/reset" instead of using Discord's slash command picker) - // These should not be forwarded to the agent; proper slash command interactions are handled elsewhere - if (!isDirectMessage && baseText && hasControlCommand(baseText, params.cfg)) { - logVerbose(`discord: drop text-based slash command ${message.id} (intercepted at gateway)`); - return null; - } - recordChannelActivity({ channel: "discord", accountId: params.accountId,