diff --git a/src/config/types.discord.ts b/src/config/types.discord.ts index 044207101..a54d7d1d0 100644 --- a/src/config/types.discord.ts +++ b/src/config/types.discord.ts @@ -38,6 +38,8 @@ export type DiscordGuildChannelConfig = { users?: Array; /** Optional system prompt snippet for this channel. */ systemPrompt?: string; + /** If false, omit thread starter context for this channel (default: true). */ + includeThreadStarter?: boolean; }; export type DiscordReactionNotificationMode = "off" | "own" | "all" | "allowlist"; diff --git a/src/config/zod-schema.providers-core.ts b/src/config/zod-schema.providers-core.ts index 427baa1ee..c0ffc4858 100644 --- a/src/config/zod-schema.providers-core.ts +++ b/src/config/zod-schema.providers-core.ts @@ -234,6 +234,7 @@ export const DiscordGuildChannelSchema = z enabled: z.boolean().optional(), users: z.array(z.union([z.string(), z.number()])).optional(), systemPrompt: z.string().optional(), + includeThreadStarter: z.boolean().optional(), autoThread: z.boolean().optional(), }) .strict(); diff --git a/src/discord/monitor.tool-result.accepts-guild-messages-mentionpatterns-match.test.ts b/src/discord/monitor.tool-result.accepts-guild-messages-mentionpatterns-match.test.ts index 3f4ed1017..a514bf390 100644 --- a/src/discord/monitor.tool-result.accepts-guild-messages-mentionpatterns-match.test.ts +++ b/src/discord/monitor.tool-result.accepts-guild-messages-mentionpatterns-match.test.ts @@ -438,6 +438,115 @@ describe("discord tool result dispatch", () => { expect(capturedCtx?.ThreadLabel).toContain("Discord thread #general"); }); + it("skips thread starter context when disabled", async () => { + const { createDiscordMessageHandler } = await import("./monitor.js"); + let capturedCtx: + | { + ThreadStarterBody?: string; + } + | undefined; + dispatchMock.mockImplementationOnce(async ({ ctx, dispatcher }) => { + capturedCtx = ctx; + dispatcher.sendFinalReply({ text: "hi" }); + return { queuedFinal: true, counts: { final: 1 } }; + }); + + const cfg = { + agents: { + defaults: { + model: "anthropic/claude-opus-4-5", + workspace: "/tmp/openclaw", + }, + }, + session: { store: "/tmp/openclaw-sessions.json" }, + channels: { + discord: { + dm: { enabled: true, policy: "open" }, + groupPolicy: "open", + guilds: { + "*": { + requireMention: false, + channels: { + "*": { includeThreadStarter: false }, + }, + }, + }, + }, + }, + } as ReturnType; + + const handler = createDiscordMessageHandler({ + cfg, + discordConfig: cfg.channels.discord, + accountId: "default", + token: "token", + runtime: { + log: vi.fn(), + error: vi.fn(), + exit: (code: number): never => { + throw new Error(`exit ${code}`); + }, + }, + botUserId: "bot-id", + guildHistories: new Map(), + historyLimit: 0, + mediaMaxBytes: 10_000, + textLimit: 2000, + replyToMode: "off", + dmEnabled: true, + groupDmEnabled: false, + guildEntries: cfg.channels.discord.guilds, + }); + + const threadChannel = { + type: ChannelType.GuildText, + name: "thread-name", + parentId: "p1", + parent: { id: "p1", name: "general" }, + isThread: () => true, + }; + + const client = { + fetchChannel: vi.fn().mockResolvedValue({ + type: ChannelType.GuildText, + name: "thread-name", + }), + rest: { + get: vi.fn().mockResolvedValue({ + content: "starter message", + author: { id: "u1", username: "Alice", discriminator: "0001" }, + timestamp: new Date().toISOString(), + }), + }, + } as unknown as Client; + + await handler( + { + message: { + id: "m7", + content: "thread reply", + channelId: "t1", + channel: threadChannel, + timestamp: new Date().toISOString(), + type: MessageType.Default, + attachments: [], + embeds: [], + mentionedEveryone: false, + mentionedUsers: [], + mentionedRoles: [], + author: { id: "u2", bot: false, username: "Bob", tag: "Bob#2" }, + }, + author: { id: "u2", bot: false, username: "Bob", tag: "Bob#2" }, + member: { displayName: "Bob" }, + guild: { id: "g1", name: "Guild" }, + guild_id: "g1", + }, + client, + ); + + expect(capturedCtx?.ThreadStarterBody).toBeUndefined(); + }); + it("treats forum threads as distinct sessions without channel payloads", async () => { const { createDiscordMessageHandler } = await import("./monitor.js"); let capturedCtx: diff --git a/src/discord/monitor/allow-list.ts b/src/discord/monitor/allow-list.ts index a9e53a971..0254c21a0 100644 --- a/src/discord/monitor/allow-list.ts +++ b/src/discord/monitor/allow-list.ts @@ -31,6 +31,7 @@ export type DiscordGuildEntryResolved = { enabled?: boolean; users?: Array; systemPrompt?: string; + includeThreadStarter?: boolean; autoThread?: boolean; } >; @@ -43,6 +44,7 @@ export type DiscordChannelConfigResolved = { enabled?: boolean; users?: Array; systemPrompt?: string; + includeThreadStarter?: boolean; autoThread?: boolean; matchKey?: string; matchSource?: ChannelMatchSource; @@ -241,6 +243,7 @@ function resolveDiscordChannelConfigEntry( enabled: entry.enabled, users: entry.users, systemPrompt: entry.systemPrompt, + includeThreadStarter: entry.includeThreadStarter, autoThread: entry.autoThread, }; return resolved; diff --git a/src/discord/monitor/message-handler.process.ts b/src/discord/monitor/message-handler.process.ts index 08ff191ec..927e9621a 100644 --- a/src/discord/monitor/message-handler.process.ts +++ b/src/discord/monitor/message-handler.process.ts @@ -209,22 +209,25 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext) let threadLabel: string | undefined; let parentSessionKey: string | undefined; if (threadChannel) { - const starter = await resolveDiscordThreadStarter({ - channel: threadChannel, - client, - parentId: threadParentId, - parentType: threadParentType, - resolveTimestampMs, - }); - if (starter?.text) { - const starterEnvelope = formatThreadStarterEnvelope({ - channel: "Discord", - author: starter.author, - timestamp: starter.timestamp, - body: starter.text, - envelope: envelopeOptions, + const includeThreadStarter = channelConfig?.includeThreadStarter !== false; + if (includeThreadStarter) { + const starter = await resolveDiscordThreadStarter({ + channel: threadChannel, + client, + parentId: threadParentId, + parentType: threadParentType, + resolveTimestampMs, }); - threadStarterBody = starterEnvelope; + if (starter?.text) { + const starterEnvelope = formatThreadStarterEnvelope({ + channel: "Discord", + author: starter.author, + timestamp: starter.timestamp, + body: starter.text, + envelope: envelopeOptions, + }); + threadStarterBody = starterEnvelope; + } } const parentName = threadParentName ?? "parent"; threadLabel = threadName