fix: add discord mention context history
This commit is contained in:
@@ -32,6 +32,7 @@
|
|||||||
### Fixes
|
### Fixes
|
||||||
- Gateway CLI: read `CLAWDIS_GATEWAY_PASSWORD` from environment in `callGateway()` — allows `doctor`/`health` commands to auth without explicit `--password` flag.
|
- 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.
|
- 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: switch imsg installer to brew tap formula.
|
||||||
- Skills: gate macOS-only skills by OS and surface block reasons in the Skills UI.
|
- 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.
|
- Onboarding: show skill descriptions in the macOS setup flow and surface clearer Gateway/skills error messages.
|
||||||
|
|||||||
@@ -178,7 +178,8 @@ Configure the Discord bot by setting the bot token and optional gating:
|
|||||||
users: ["987654321098765432"] // optional user allowlist (ids)
|
users: ["987654321098765432"] // optional user allowlist (ids)
|
||||||
},
|
},
|
||||||
requireMention: true, // require @bot mentions in guilds
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ Status: ready for DM and guild text channels via the official Discord bot gatewa
|
|||||||
6. Guild channels: use `channel:<channelId>` for delivery. Mentions are required by default; disable with `discord.requireMention = false`.
|
6. Guild channels: use `channel:<channelId>` 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.
|
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.
|
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.
|
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"]
|
users: ["987654321098765432"]
|
||||||
},
|
},
|
||||||
requireMention: true,
|
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.
|
- `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.
|
- `requireMention`: when `true`, messages in guild channels must mention the bot.
|
||||||
- `mediaMaxMb`: clamp inbound media saved to disk.
|
- `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
|
## 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.
|
- Treat the bot token like a password; prefer the `DISCORD_BOT_TOKEN` env var on supervised hosts or lock down the config file permissions.
|
||||||
|
|||||||
@@ -169,6 +169,8 @@ export type DiscordConfig = {
|
|||||||
};
|
};
|
||||||
requireMention?: boolean;
|
requireMention?: boolean;
|
||||||
mediaMaxMb?: number;
|
mediaMaxMb?: number;
|
||||||
|
/** Number of recent guild messages to include for context (default: 20). */
|
||||||
|
historyLimit?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SignalConfig = {
|
export type SignalConfig = {
|
||||||
@@ -874,6 +876,7 @@ const ClawdisSchema = z.object({
|
|||||||
.optional(),
|
.optional(),
|
||||||
requireMention: z.boolean().optional(),
|
requireMention: z.boolean().optional(),
|
||||||
mediaMaxMb: z.number().positive().optional(),
|
mediaMaxMb: z.number().positive().optional(),
|
||||||
|
historyLimit: z.number().int().min(0).optional(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
signal: z
|
signal: z
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export type MonitorDiscordOpts = {
|
|||||||
};
|
};
|
||||||
requireMention?: boolean;
|
requireMention?: boolean;
|
||||||
mediaMaxMb?: number;
|
mediaMaxMb?: number;
|
||||||
|
historyLimit?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type DiscordMediaInfo = {
|
type DiscordMediaInfo = {
|
||||||
@@ -39,6 +40,12 @@ type DiscordMediaInfo = {
|
|||||||
placeholder: string;
|
placeholder: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type DiscordHistoryEntry = {
|
||||||
|
sender: string;
|
||||||
|
body: string;
|
||||||
|
timestamp?: number;
|
||||||
|
};
|
||||||
|
|
||||||
export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
||||||
const cfg = loadConfig();
|
const cfg = loadConfig();
|
||||||
const token = normalizeDiscordToken(
|
const token = normalizeDiscordToken(
|
||||||
@@ -67,6 +74,10 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
|||||||
opts.requireMention ?? cfg.discord?.requireMention ?? true;
|
opts.requireMention ?? cfg.discord?.requireMention ?? true;
|
||||||
const mediaMaxBytes =
|
const mediaMaxBytes =
|
||||||
(opts.mediaMaxMb ?? cfg.discord?.mediaMaxMb ?? 8) * 1024 * 1024;
|
(opts.mediaMaxMb ?? cfg.discord?.mediaMaxMb ?? 8) * 1024 * 1024;
|
||||||
|
const historyLimit = Math.max(
|
||||||
|
0,
|
||||||
|
opts.historyLimit ?? cfg.discord?.historyLimit ?? 20,
|
||||||
|
);
|
||||||
|
|
||||||
const client = new Client({
|
const client = new Client({
|
||||||
intents: [
|
intents: [
|
||||||
@@ -79,6 +90,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const logger = getChildLogger({ module: "discord-auto-reply" });
|
const logger = getChildLogger({ module: "discord-auto-reply" });
|
||||||
|
const guildHistories = new Map<string, DiscordHistoryEntry[]>();
|
||||||
|
|
||||||
client.once(Events.ClientReady, () => {
|
client.once(Events.ClientReady, () => {
|
||||||
runtime.log?.(`discord: logged in as ${client.user?.tag ?? "unknown"}`);
|
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 botId = client.user?.id;
|
||||||
const wasMentioned =
|
const wasMentioned =
|
||||||
!isDirectMessage && Boolean(botId && message.mentions.has(botId));
|
!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 (!isDirectMessage && requireMention) {
|
||||||
if (botId && !wasMentioned) {
|
if (botId && !wasMentioned) {
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -166,15 +196,37 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
|||||||
const fromLabel = isDirectMessage
|
const fromLabel = isDirectMessage
|
||||||
? buildDirectLabel(message)
|
? buildDirectLabel(message)
|
||||||
: buildGuildLabel(message);
|
: buildGuildLabel(message);
|
||||||
const body = formatAgentEnvelope({
|
let combinedBody = formatAgentEnvelope({
|
||||||
surface: "Discord",
|
surface: "Discord",
|
||||||
from: fromLabel,
|
from: fromLabel,
|
||||||
timestamp: message.createdTimestamp,
|
timestamp: message.createdTimestamp,
|
||||||
body: text,
|
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 = {
|
const ctxPayload = {
|
||||||
Body: body,
|
Body: combinedBody,
|
||||||
From: isDirectMessage
|
From: isDirectMessage
|
||||||
? `discord:${message.author.id}`
|
? `discord:${message.author.id}`
|
||||||
: `group:${message.channelId}`,
|
: `group:${message.channelId}`,
|
||||||
@@ -209,7 +261,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isVerbose()) {
|
if (isVerbose()) {
|
||||||
const preview = body.slice(0, 200).replace(/\n/g, "\\n");
|
const preview = combinedBody.slice(0, 200).replace(/\n/g, "\\n");
|
||||||
logVerbose(
|
logVerbose(
|
||||||
`discord inbound: channel=${message.channelId} from=${ctxPayload.From} preview="${preview}"`,
|
`discord inbound: channel=${message.channelId} from=${ctxPayload.From} preview="${preview}"`,
|
||||||
);
|
);
|
||||||
@@ -235,6 +287,9 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
|||||||
token,
|
token,
|
||||||
runtime,
|
runtime,
|
||||||
});
|
});
|
||||||
|
if (!isDirectMessage && shouldClearHistory && historyLimit > 0) {
|
||||||
|
guildHistories.set(message.channelId, []);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
runtime.error?.(danger(`Discord handler failed: ${String(err)}`));
|
runtime.error?.(danger(`Discord handler failed: ${String(err)}`));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2098,6 +2098,7 @@ export async function startGatewayServer(
|
|||||||
guildAllowFrom: cfg.discord?.guildAllowFrom,
|
guildAllowFrom: cfg.discord?.guildAllowFrom,
|
||||||
requireMention: cfg.discord?.requireMention,
|
requireMention: cfg.discord?.requireMention,
|
||||||
mediaMaxMb: cfg.discord?.mediaMaxMb,
|
mediaMaxMb: cfg.discord?.mediaMaxMb,
|
||||||
|
historyLimit: cfg.discord?.historyLimit,
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
discordRuntime = {
|
discordRuntime = {
|
||||||
|
|||||||
Reference in New Issue
Block a user