From 38d8a669b40b96f1a41c3ba5feaf30b72508f5a0 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 1 Jan 2026 23:58:35 +0100 Subject: [PATCH] fix: add discord mention context history --- CHANGELOG.md | 1 + docs/configuration.md | 3 ++- docs/discord.md | 5 +++- src/config/config.ts | 3 +++ src/discord/monitor.ts | 61 +++++++++++++++++++++++++++++++++++++++--- src/gateway/server.ts | 1 + 6 files changed, 69 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f070f0a8..af8d5c4df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ ### Fixes - Gateway CLI: read `CLAWDIS_GATEWAY_PASSWORD` from environment in `callGateway()` — allows `doctor`/`health` commands to auth without explicit `--password` flag. - Auto-reply: suppress stray `HEARTBEAT_OK` acks so they never get delivered as messages. +- Discord: include recent guild context when replying to mentions and add `discord.historyLimit` to tune how many messages are captured. - Skills: switch imsg installer to brew tap formula. - Skills: gate macOS-only skills by OS and surface block reasons in the Skills UI. - Onboarding: show skill descriptions in the macOS setup flow and surface clearer Gateway/skills error messages. diff --git a/docs/configuration.md b/docs/configuration.md index c0dd79d59..4d03b907b 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -178,7 +178,8 @@ Configure the Discord bot by setting the bot token and optional gating: users: ["987654321098765432"] // optional user allowlist (ids) }, requireMention: true, // require @bot mentions in guilds - mediaMaxMb: 8 // clamp inbound media size + mediaMaxMb: 8, // clamp inbound media size + historyLimit: 20 // include last N guild messages as context } } ``` diff --git a/docs/discord.md b/docs/discord.md index 197961d82..5fcb5445f 100644 --- a/docs/discord.md +++ b/docs/discord.md @@ -23,6 +23,7 @@ Status: ready for DM and guild text channels via the official Discord bot gatewa 6. Guild channels: use `channel:` for delivery. Mentions are required by default; disable with `discord.requireMention = false`. 7. Optional DM allowlist: reuse `discord.allowFrom` with user ids (`1234567890` or `discord:1234567890`). Use `"*"` to allow all DMs. 8. Optional guild allowlist: set `discord.guildAllowFrom` with `guilds` and/or `users` to gate who can invoke the bot in servers. +9. Optional guild context history: set `discord.historyLimit` (default 20) to include the last N guild messages as context when replying to a mention. Set `0` to disable. Note: Discord does not provide a simple username → id lookup without extra guild context, so prefer ids or `<@id>` mentions for DM delivery targets. @@ -45,7 +46,8 @@ Note: Discord does not provide a simple username → id lookup without extra gui users: ["987654321098765432"] }, requireMention: true, - mediaMaxMb: 8 + mediaMaxMb: 8, + historyLimit: 20 } } ``` @@ -54,6 +56,7 @@ Note: Discord does not provide a simple username → id lookup without extra gui - `guildAllowFrom`: Optional allowlist for guild messages. Set `guilds` and/or `users` (ids). When both are set, both must match. - `requireMention`: when `true`, messages in guild channels must mention the bot. - `mediaMaxMb`: clamp inbound media saved to disk. +- `historyLimit`: number of recent guild messages to include as context when replying to a mention (default 20, `0` disables). ## Safety & ops - Treat the bot token like a password; prefer the `DISCORD_BOT_TOKEN` env var on supervised hosts or lock down the config file permissions. diff --git a/src/config/config.ts b/src/config/config.ts index a1beabbac..1e9f56353 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -169,6 +169,8 @@ export type DiscordConfig = { }; requireMention?: boolean; mediaMaxMb?: number; + /** Number of recent guild messages to include for context (default: 20). */ + historyLimit?: number; }; export type SignalConfig = { @@ -874,6 +876,7 @@ const ClawdisSchema = z.object({ .optional(), requireMention: z.boolean().optional(), mediaMaxMb: z.number().positive().optional(), + historyLimit: z.number().int().min(0).optional(), }) .optional(), signal: z diff --git a/src/discord/monitor.ts b/src/discord/monitor.ts index e9e34ab34..04e3c9ea5 100644 --- a/src/discord/monitor.ts +++ b/src/discord/monitor.ts @@ -31,6 +31,7 @@ export type MonitorDiscordOpts = { }; requireMention?: boolean; mediaMaxMb?: number; + historyLimit?: number; }; type DiscordMediaInfo = { @@ -39,6 +40,12 @@ type DiscordMediaInfo = { placeholder: string; }; +type DiscordHistoryEntry = { + sender: string; + body: string; + timestamp?: number; +}; + export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) { const cfg = loadConfig(); const token = normalizeDiscordToken( @@ -67,6 +74,10 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) { opts.requireMention ?? cfg.discord?.requireMention ?? true; const mediaMaxBytes = (opts.mediaMaxMb ?? cfg.discord?.mediaMaxMb ?? 8) * 1024 * 1024; + const historyLimit = Math.max( + 0, + opts.historyLimit ?? cfg.discord?.historyLimit ?? 20, + ); const client = new Client({ intents: [ @@ -79,6 +90,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) { }); const logger = getChildLogger({ module: "discord-auto-reply" }); + const guildHistories = new Map(); client.once(Events.ClientReady, () => { runtime.log?.(`discord: logged in as ${client.user?.tag ?? "unknown"}`); @@ -97,6 +109,24 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) { const botId = client.user?.id; const wasMentioned = !isDirectMessage && Boolean(botId && message.mentions.has(botId)); + const attachment = message.attachments.first(); + const baseText = + message.content?.trim() || + (attachment ? inferPlaceholder(attachment) : "") || + message.embeds[0]?.description || + ""; + + if (!isDirectMessage && historyLimit > 0 && baseText) { + const history = guildHistories.get(message.channelId) ?? []; + history.push({ + sender: message.member?.displayName ?? message.author.tag, + body: baseText, + timestamp: message.createdTimestamp, + }); + while (history.length > historyLimit) history.shift(); + guildHistories.set(message.channelId, history); + } + if (!isDirectMessage && requireMention) { if (botId && !wasMentioned) { logger.info( @@ -166,15 +196,37 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) { const fromLabel = isDirectMessage ? buildDirectLabel(message) : buildGuildLabel(message); - const body = formatAgentEnvelope({ + let combinedBody = formatAgentEnvelope({ surface: "Discord", from: fromLabel, timestamp: message.createdTimestamp, body: text, }); + let shouldClearHistory = false; + if (!isDirectMessage) { + const history = + historyLimit > 0 ? (guildHistories.get(message.channelId) ?? []) : []; + const historyWithoutCurrent = + history.length > 0 ? history.slice(0, -1) : []; + if (historyWithoutCurrent.length > 0) { + const historyText = historyWithoutCurrent + .map((entry) => + formatAgentEnvelope({ + surface: "Discord", + from: fromLabel, + timestamp: entry.timestamp, + body: `${entry.sender}: ${entry.body}`, + }), + ) + .join("\n"); + combinedBody = `[Chat messages since your last reply - for context]\n${historyText}\n\n[Current message - respond to this]\n${combinedBody}`; + } + combinedBody = `${combinedBody}\n[from: ${message.member?.displayName ?? message.author.tag}]`; + shouldClearHistory = true; + } const ctxPayload = { - Body: body, + Body: combinedBody, From: isDirectMessage ? `discord:${message.author.id}` : `group:${message.channelId}`, @@ -209,7 +261,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) { } if (isVerbose()) { - const preview = body.slice(0, 200).replace(/\n/g, "\\n"); + const preview = combinedBody.slice(0, 200).replace(/\n/g, "\\n"); logVerbose( `discord inbound: channel=${message.channelId} from=${ctxPayload.From} preview="${preview}"`, ); @@ -235,6 +287,9 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) { token, runtime, }); + if (!isDirectMessage && shouldClearHistory && historyLimit > 0) { + guildHistories.set(message.channelId, []); + } } catch (err) { runtime.error?.(danger(`Discord handler failed: ${String(err)}`)); } diff --git a/src/gateway/server.ts b/src/gateway/server.ts index 8867cbd09..9fd781f16 100644 --- a/src/gateway/server.ts +++ b/src/gateway/server.ts @@ -2098,6 +2098,7 @@ export async function startGatewayServer( guildAllowFrom: cfg.discord?.guildAllowFrom, requireMention: cfg.discord?.requireMention, mediaMaxMb: cfg.discord?.mediaMaxMb, + historyLimit: cfg.discord?.historyLimit, }) .catch((err) => { discordRuntime = {