diff --git a/CHANGELOG.md b/CHANGELOG.md index f40baa04cfc..0fbd6cd8064 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Mattermost: suppress reasoning-only payloads even when they arrive as blockquoted `> Reasoning:` text, preventing `/reasoning on` from leaking thinking into channel posts. (#69927) Thanks @lawrence3699. +- Discord: read `channel.parentId` through a safe accessor in the slash-command, reaction, and model-picker paths so partial `GuildThreadChannel` prototype getters no longer throw `Cannot access rawData on partial Channel` when commands like `/new` run from inside a thread. Fixes #69861. (#69908) Thanks @neeravmakwana. - Browser/Chrome MCP: reset cached existing-session control sessions when a `navigate_page` call times out, so one stuck navigation no longer poisons the browser profile until a gateway restart. (#69733) Thanks @ayeshakhalid192007-dev. - Browser/Chrome MCP: propagate click timeouts and abort signals to existing-session actions so a stuck click fails fast and reconnects instead of poisoning the browser tool until gateway restart. (#63524) Thanks @dongseok0. - OpenCode Go: canonicalize stale bundled `opencode-go` base URLs from `/go` or `/go/v1` to `/zen/go` or `/zen/go/v1`, so older generated model metadata stops hitting the 404 HTML endpoint. (#69898) diff --git a/extensions/discord/src/monitor/channel-access.ts b/extensions/discord/src/monitor/channel-access.ts index 4ac98a715ec..0d1a2ba27ed 100644 --- a/extensions/discord/src/monitor/channel-access.ts +++ b/extensions/discord/src/monitor/channel-access.ts @@ -42,6 +42,10 @@ export function resolveDiscordChannelTopicSafe(channel: unknown): string | undef return resolveDiscordChannelStringPropertySafe(channel, "topic"); } +export function resolveDiscordChannelParentIdSafe(channel: unknown): string | undefined { + return resolveDiscordChannelStringPropertySafe(channel, "parentId"); +} + export function resolveDiscordChannelInfoSafe(channel: unknown): DiscordChannelInfoSafe { const parent = readDiscordChannelPropertySafe(channel, "parent"); return { diff --git a/extensions/discord/src/monitor/listeners.ts b/extensions/discord/src/monitor/listeners.ts index f6688d9c764..8caea16a745 100644 --- a/extensions/discord/src/monitor/listeners.ts +++ b/extensions/discord/src/monitor/listeners.ts @@ -32,7 +32,10 @@ import { resolveDiscordGuildEntry, shouldEmitDiscordReactionNotification, } from "./allow-list.js"; -import { resolveDiscordChannelInfoSafe } from "./channel-access.js"; +import { + resolveDiscordChannelInfoSafe, + resolveDiscordChannelParentIdSafe, +} from "./channel-access.js"; import { formatDiscordReactionEmoji, formatDiscordUserTag } from "./format.js"; import { resolveDiscordChannelInfo } from "./message-utils.js"; import { setPresence } from "./presence-cache.js"; @@ -487,7 +490,7 @@ async function handleDiscordReactionEvent( return; } } - let parentId = "parentId" in channel ? (channel.parentId ?? undefined) : undefined; + let parentId = resolveDiscordChannelParentIdSafe(channel); let parentName: string | undefined; let parentSlug = ""; let reactionBase: { baseText: string; contextKey: string } | null = null; diff --git a/extensions/discord/src/monitor/native-command-ui.ts b/extensions/discord/src/monitor/native-command-ui.ts index a969c7c1eaa..4d1b0599a83 100644 --- a/extensions/discord/src/monitor/native-command-ui.ts +++ b/extensions/discord/src/monitor/native-command-ui.ts @@ -34,7 +34,10 @@ import { normalizeOptionalString, withTimeout, } from "openclaw/plugin-sdk/text-runtime"; -import { resolveDiscordChannelNameSafe } from "./channel-access.js"; +import { + resolveDiscordChannelNameSafe, + resolveDiscordChannelParentIdSafe, +} from "./channel-access.js"; import { resolveDiscordSlashCommandConfig } from "./commands.js"; import { resolveDiscordChannelInfo } from "./message-utils.js"; import { @@ -258,7 +261,7 @@ async function resolveDiscordModelPickerRouteState(params: { threadChannel: { id: rawChannelId, name: resolveDiscordChannelNameSafe(channel), - parentId: "parentId" in channel ? (channel.parentId ?? undefined) : undefined, + parentId: resolveDiscordChannelParentIdSafe(channel), parent: undefined, }, channelInfo, diff --git a/extensions/discord/src/monitor/native-command.commands-allowfrom.test.ts b/extensions/discord/src/monitor/native-command.commands-allowfrom.test.ts index 99d342a0d84..c289ec66eda 100644 --- a/extensions/discord/src/monitor/native-command.commands-allowfrom.test.ts +++ b/extensions/discord/src/monitor/native-command.commands-allowfrom.test.ts @@ -192,6 +192,29 @@ describe("Discord native slash commands with commands.allowFrom", () => { expectNotUnauthorizedReply(interaction); }); + it("tolerates partial guild thread channels whose parentId getter throws", async () => { + const { dispatchSpy, interaction } = await runGuildSlashCommand({ + mutateInteraction: (currentInteraction) => { + currentInteraction.channel = { + type: ChannelType.PublicThread, + id: currentInteraction.channel.id, + } as MockCommandInteraction["channel"]; + Object.defineProperty(currentInteraction.channel, "parentId", { + configurable: true, + enumerable: true, + get() { + throw new Error( + "Cannot access rawData on partial Channel. Use fetch() to populate data.", + ); + }, + }); + }, + }); + expect(interaction.defer).toHaveBeenCalledTimes(1); + expect(dispatchSpy).toHaveBeenCalledTimes(1); + expectNotUnauthorizedReply(interaction); + }); + it("authorizes guild slash commands from an allowlisted channel when commands.allowFrom is not configured", async () => { const { dispatchSpy, interaction } = await runGuildSlashCommand({ mutateConfig: (cfg) => { diff --git a/extensions/discord/src/monitor/native-command.ts b/extensions/discord/src/monitor/native-command.ts index c81f5f59a63..297b20a65f2 100644 --- a/extensions/discord/src/monitor/native-command.ts +++ b/extensions/discord/src/monitor/native-command.ts @@ -65,7 +65,11 @@ import { resolveDiscordOwnerAccess, resolveGroupDmAllow, } from "./allow-list.js"; -import { resolveDiscordChannelNameSafe, resolveDiscordChannelTopicSafe } from "./channel-access.js"; +import { + resolveDiscordChannelNameSafe, + resolveDiscordChannelParentIdSafe, + resolveDiscordChannelTopicSafe, +} from "./channel-access.js"; import { resolveDiscordDmCommandAccess } from "./dm-command-auth.js"; import { handleDiscordDmCommandDecision } from "./dm-command-decision.js"; import { resolveDiscordChannelInfo } from "./message-utils.js"; @@ -466,7 +470,7 @@ async function resolveDiscordNativeAutocompleteAuthorized(params: { threadChannel: { id: rawChannelId, name: channelName, - parentId: "parentId" in channel ? (channel.parentId ?? undefined) : undefined, + parentId: resolveDiscordChannelParentIdSafe(channel), parent: undefined, }, channelInfo, @@ -859,7 +863,7 @@ async function dispatchDiscordCommandInteraction(params: { threadChannel: { id: rawChannelId, name: channelName, - parentId: "parentId" in channel ? (channel.parentId ?? undefined) : undefined, + parentId: resolveDiscordChannelParentIdSafe(channel), parent: undefined, }, channelInfo, @@ -1072,7 +1076,9 @@ async function dispatchDiscordCommandInteraction(params: { interaction.channel?.type === ChannelType.AnnouncementThread; const messageThreadId = !isDirectMessage && isThreadChannel ? channelId : undefined; const threadParentId = - !isDirectMessage && isThreadChannel ? (interaction.channel.parentId ?? undefined) : undefined; + !isDirectMessage && isThreadChannel + ? resolveDiscordChannelParentIdSafe(interaction.channel) + : undefined; const { effectiveRoute } = await getNativeRouteState(); const pluginReply = await executePluginCommandImpl({ command: pluginMatch.command,