diff --git a/CHANGELOG.md b/CHANGELOG.md index a82e9d3de97..c3b9bc17a5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Docs: https://docs.openclaw.ai - Discord/Streaming: add stream preview mode for live draft replies with partial/block options and configurable chunking. Thanks @thewilloftheshadow. Inspiration @neoagentic-ship-it. - Discord/Telegram: add configurable lifecycle status reactions for queued/thinking/tool/done/error phases with a shared controller and emoji/timing overrides. Thanks @wolly-tundracube and @thewilloftheshadow. - Discord/Voice: add voice channel join/leave/status via `/vc`, plus auto-join configuration for realtime voice conversations. Thanks @thewilloftheshadow. +- Discord: include channel topics in trusted inbound metadata on new sessions. Thanks @thewilloftheshadow. - Docs/Discord: document forum channel thread creation flows and component limits. Thanks @thewilloftheshadow. ### Fixes diff --git a/docs/channels/discord.md b/docs/channels/discord.md index 28c4c8e14ee..aadf357a813 100644 --- a/docs/channels/discord.md +++ b/docs/channels/discord.md @@ -614,7 +614,7 @@ See [Slash commands](/tools/slash-commands) for command catalog and behavior. - parent thread metadata can be used for parent-session linkage - thread config inherits parent channel config unless a thread-specific entry exists - Channel topics are injected as **untrusted** context (not as system prompt). + Channel topics are injected as untrusted context and also included in trusted inbound metadata on new sessions. diff --git a/src/auto-reply/reply/inbound-meta.test.ts b/src/auto-reply/reply/inbound-meta.test.ts index 915c4800e68..ed92feb2d28 100644 --- a/src/auto-reply/reply/inbound-meta.test.ts +++ b/src/auto-reply/reply/inbound-meta.test.ts @@ -71,6 +71,36 @@ describe("buildInboundMetaSystemPrompt", () => { const payload = parseInboundMetaPayload(prompt); expect(payload["sender_id"]).toBeUndefined(); }); + + it("includes discord channel topics only for new sessions", () => { + const prompt = buildInboundMetaSystemPrompt({ + OriginatingTo: "discord:channel:123", + OriginatingChannel: "discord", + Provider: "discord", + Surface: "discord", + ChatType: "group", + ChannelTopic: " Shipping updates ", + IsNewSession: "true", + } as TemplateContext); + + const payload = parseInboundMetaPayload(prompt); + expect(payload["channel_topic"]).toBe("Shipping updates"); + }); + + it("omits discord channel topics for existing sessions", () => { + const prompt = buildInboundMetaSystemPrompt({ + OriginatingTo: "discord:channel:123", + OriginatingChannel: "discord", + Provider: "discord", + Surface: "discord", + ChatType: "group", + ChannelTopic: "Shipping updates", + IsNewSession: "false", + } as TemplateContext); + + const payload = parseInboundMetaPayload(prompt); + expect(payload["channel_topic"]).toBeUndefined(); + }); }); describe("buildInboundUserContextPrefix", () => { diff --git a/src/auto-reply/reply/inbound-meta.ts b/src/auto-reply/reply/inbound-meta.ts index bbcbc5dabac..ca817fda57c 100644 --- a/src/auto-reply/reply/inbound-meta.ts +++ b/src/auto-reply/reply/inbound-meta.ts @@ -13,8 +13,15 @@ function safeTrim(value: unknown): string | undefined { export function buildInboundMetaSystemPrompt(ctx: TemplateContext): string { const chatType = normalizeChatType(ctx.ChatType); const isDirect = !chatType || chatType === "direct"; + const isNewSession = ctx.IsNewSession === "true"; + const originatingChannel = safeTrim(ctx.OriginatingChannel); + const surface = safeTrim(ctx.Surface); + const provider = safeTrim(ctx.Provider); + const isDiscord = + provider === "discord" || surface === "discord" || originatingChannel === "discord"; - // Keep system metadata strictly free of attacker-controlled strings (sender names, group subjects, etc.). + // Keep system metadata strictly free of attacker-controlled strings (sender names, group subjects, etc.) + // unless explicitly opted into for new-session context (e.g. Discord channel topics). // Those belong in the user-role "untrusted context" blocks. // Per-message identifiers (message_id, reply_to_id, sender_id) are also excluded here: they change // on every turn and would bust prefix-based prompt caches on local model providers. They are @@ -23,25 +30,27 @@ export function buildInboundMetaSystemPrompt(ctx: TemplateContext): string { // Resolve channel identity: prefer explicit channel, then surface, then provider. // For webchat/Hub Chat sessions (when Surface is 'webchat' or undefined with no real channel), // omit the channel field entirely rather than falling back to an unrelated provider. - let channelValue = safeTrim(ctx.OriginatingChannel) ?? safeTrim(ctx.Surface); + let channelValue = originatingChannel ?? surface; if (!channelValue) { // Only fall back to Provider if it represents a real messaging channel. // For webchat/internal sessions, ctx.Provider may be unrelated (e.g., the user's configured // default channel), so skip it to avoid incorrect runtime labels like "channel=whatsapp". - const provider = safeTrim(ctx.Provider); // Check if provider is "webchat" or if we're in an internal/webchat context - if (provider !== "webchat" && ctx.Surface !== "webchat") { + if (provider !== "webchat" && surface !== "webchat") { channelValue = provider; } // Otherwise leave channelValue undefined (no channel label) } + const channelTopic = isNewSession && isDiscord ? safeTrim(ctx.ChannelTopic) : undefined; + const payload = { schema: "openclaw.inbound_meta.v1", chat_id: safeTrim(ctx.OriginatingTo), channel: channelValue, - provider: safeTrim(ctx.Provider), - surface: safeTrim(ctx.Surface), + channel_topic: channelTopic, + provider, + surface, chat_type: chatType ?? (isDirect ? "direct" : undefined), flags: { is_group_chat: !isDirect ? true : undefined, diff --git a/src/auto-reply/templating.ts b/src/auto-reply/templating.ts index 4bc9b517549..c90adbde2b3 100644 --- a/src/auto-reply/templating.ts +++ b/src/auto-reply/templating.ts @@ -98,6 +98,8 @@ export type MsgContext = { GroupSubject?: string; /** Human label for channel-like group conversations (e.g. #general, #support). */ GroupChannel?: string; + /** Channel topic/description (trusted metadata for new session context). */ + ChannelTopic?: string; GroupSpace?: string; GroupMembers?: string; GroupSystemPrompt?: string; diff --git a/src/discord/monitor/message-handler.process.ts b/src/discord/monitor/message-handler.process.ts index 0badfe48369..cf0c0de900e 100644 --- a/src/discord/monitor/message-handler.process.ts +++ b/src/discord/monitor/message-handler.process.ts @@ -173,6 +173,7 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext) const forumContextLine = isForumStarter ? `[Forum parent: #${forumParentSlug}]` : null; const groupChannel = isGuildMessage && displayChannelSlug ? `#${displayChannelSlug}` : undefined; const groupSubject = isDirectMessage ? undefined : groupChannel; + const channelTopic = isGuildMessage ? channelInfo?.topic : undefined; const untrustedChannelMetadata = isGuildMessage ? buildUntrustedChannelMetadata({ source: "discord", @@ -334,6 +335,7 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext) SenderTag: senderTag, GroupSubject: groupSubject, GroupChannel: groupChannel, + ChannelTopic: channelTopic, UntrustedContext: untrustedChannelMetadata ? [untrustedChannelMetadata] : undefined, GroupSystemPrompt: isGuildMessage ? groupSystemPrompt : undefined, GroupSpace: isGuildMessage ? (guildInfo?.id ?? guildSlug) || undefined : undefined,