diff --git a/CHANGELOG.md b/CHANGELOG.md index 7edbb7a10..8635852b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - Model config schema changes (auth profiles + model lists); doctor auto-migrates and the gateway rewrites legacy configs on startup. - Commands: gate all slash commands to authorized senders; add `/compact` to manually compact session context. - Groups: `whatsapp.groups`, `telegram.groups`, and `imessage.groups` now act as allowlists when set. Add `"*"` to keep allow-all behavior. +- Auto-reply: removed `autoReply` from Discord/Slack/Telegram channel configs; use `requireMention` instead (Telegram topics now support `requireMention` overrides). ### Fixes - Pairing: generate DM pairing codes with CSPRNG, expire pending codes after 1 hour, and avoid re-sending codes for already pending requests. diff --git a/docs/gateway/configuration.md b/docs/gateway/configuration.md index c8aa219a4..5ad3e51e9 100644 --- a/docs/gateway/configuration.md +++ b/docs/gateway/configuration.md @@ -468,12 +468,16 @@ Set `telegram.enabled: false` to disable automatic startup. dmPolicy: "pairing", // pairing | allowlist | open | disabled allowFrom: ["tg:123456789"], // optional; "open" requires ["*"] groups: { - "*": { requireMention: true, autoReply: false }, + "*": { requireMention: true }, "-1001234567890": { allowFrom: ["@admin"], systemPrompt: "Keep answers brief.", topics: { - "99": { skills: ["search"], systemPrompt: "Stay on topic." } + "99": { + requireMention: false, + skills: ["search"], + systemPrompt: "Stay on topic." + } } } }, @@ -580,7 +584,7 @@ Slack runs in Socket Mode and requires both a bot token and app token: C123: { allow: true, requireMention: true }, "#general": { allow: true, - autoReply: false, + requireMention: true, users: ["U123"], skills: ["docs"], systemPrompt: "Short answers only." diff --git a/docs/providers/discord.md b/docs/providers/discord.md index f5ef24f6f..b4bfaf878 100644 --- a/docs/providers/discord.md +++ b/docs/providers/discord.md @@ -202,8 +202,7 @@ Notes: requireMention: true, users: ["987654321098765432"], skills: ["search", "docs"], - systemPrompt: "Keep answers short.", - autoReply: false + systemPrompt: "Keep answers short." } } } @@ -227,7 +226,6 @@ Ack reactions are controlled globally via `messages.ackReaction` + - `guilds..users`: optional per-guild user allowlist (ids or names). - `guilds..channels..allow`: allow/deny the channel when `groupPolicy="allowlist"`. - `guilds..channels..requireMention`: mention gating for the channel. -- `guilds..channels..autoReply`: if `true`, reply to all messages (overrides `requireMention`). - `guilds..channels..users`: optional per-channel user allowlist. - `guilds..channels..skills`: skill filter (omit = all skills, empty = none). - `guilds..channels..systemPrompt`: extra system prompt for the channel (combined with channel topic). diff --git a/docs/providers/slack.md b/docs/providers/slack.md index f21c4f044..6fd40e5e2 100644 --- a/docs/providers/slack.md +++ b/docs/providers/slack.md @@ -159,7 +159,7 @@ Slack uses Socket Mode only (no HTTP webhook server). Provide both tokens: "C123": { "allow": true, "requireMention": true }, "#general": { "allow": true, - "autoReply": false, + "requireMention": true, "users": ["U123"], "skills": ["search", "docs"], "systemPrompt": "Keep answers short." @@ -212,7 +212,6 @@ Ack reactions are controlled globally via `messages.ackReaction` + Channel options (`slack.channels.` or `slack.channels.`): - `allow`: allow/deny the channel when `groupPolicy="allowlist"`. - `requireMention`: mention gating for the channel. -- `autoReply`: if `true`, reply to every message (overrides `requireMention`). - `users`: optional per-channel user allowlist. - `skills`: skill filter (omit = all skills, empty = none). - `systemPrompt`: extra system prompt for the channel (combined with topic/purpose). diff --git a/docs/providers/telegram.md b/docs/providers/telegram.md index 99ad6448d..ef8b7bac8 100644 --- a/docs/providers/telegram.md +++ b/docs/providers/telegram.md @@ -117,12 +117,12 @@ Provider options: - `telegram.groupAllowFrom`: group sender allowlist (ids/usernames). - `telegram.groups`: per-group defaults + allowlist (use `"*"` for global defaults). - `telegram.groups..requireMention`: mention gating default. - - `telegram.groups..autoReply`: reply to every message (overrides `requireMention`). - `telegram.groups..skills`: skill filter (omit = all skills, empty = none). - `telegram.groups..allowFrom`: per-group sender allowlist override. - `telegram.groups..systemPrompt`: extra system prompt for the group. - `telegram.groups..enabled`: disable the group when `false`. - `telegram.groups..topics..*`: per-topic overrides (same fields as group). + - `telegram.groups..topics..requireMention`: per-topic mention gating override. - `telegram.replyToMode`: `off | first | all`. - `telegram.textChunkLimit`: outbound chunk size (chars). - `telegram.streamMode`: `off | partial | block` (draft streaming). diff --git a/src/auto-reply/reply/groups.ts b/src/auto-reply/reply/groups.ts index b28481d6d..292a77712 100644 --- a/src/auto-reply/reply/groups.ts +++ b/src/auto-reply/reply/groups.ts @@ -44,11 +44,7 @@ function parseTelegramGroupId(value?: string | null) { return { chatId: raw, topicId: undefined }; } -function hasOwn(obj: unknown, key: string): boolean { - return Boolean(obj && typeof obj === "object" && Object.hasOwn(obj, key)); -} - -function resolveTelegramAutoReply(params: { +function resolveTelegramRequireMention(params: { cfg: ClawdbotConfig; chatId?: string; topicId?: string; @@ -61,17 +57,17 @@ function resolveTelegramAutoReply(params: { topicId && groupConfig?.topics ? groupConfig.topics[topicId] : undefined; const defaultTopicConfig = topicId && groupDefault?.topics ? groupDefault.topics[topicId] : undefined; - if (hasOwn(topicConfig, "autoReply")) { - return (topicConfig as { autoReply?: boolean }).autoReply; + if (typeof topicConfig?.requireMention === "boolean") { + return topicConfig.requireMention; } - if (hasOwn(defaultTopicConfig, "autoReply")) { - return (defaultTopicConfig as { autoReply?: boolean }).autoReply; + if (typeof defaultTopicConfig?.requireMention === "boolean") { + return defaultTopicConfig.requireMention; } - if (hasOwn(groupConfig, "autoReply")) { - return (groupConfig as { autoReply?: boolean }).autoReply; + if (typeof groupConfig?.requireMention === "boolean") { + return groupConfig.requireMention; } - if (hasOwn(groupDefault, "autoReply")) { - return (groupDefault as { autoReply?: boolean }).autoReply; + if (typeof groupDefault?.requireMention === "boolean") { + return groupDefault.requireMention; } return undefined; } @@ -107,8 +103,12 @@ export function resolveGroupRequireMention(params: { const groupSpace = ctx.GroupSpace?.trim(); if (provider === "telegram") { const { chatId, topicId } = parseTelegramGroupId(groupId); - const autoReply = resolveTelegramAutoReply({ cfg, chatId, topicId }); - if (typeof autoReply === "boolean") return !autoReply; + const requireMention = resolveTelegramRequireMention({ + cfg, + chatId, + topicId, + }); + if (typeof requireMention === "boolean") return requireMention; return resolveProviderGroupRequireMention({ cfg, provider, @@ -138,9 +138,6 @@ export function resolveGroupRequireMention(params: { (groupRoom ? channelEntries[normalizeDiscordSlug(groupRoom)] : undefined); - if (entry && typeof entry.autoReply === "boolean") { - return !entry.autoReply; - } if (entry && typeof entry.requireMention === "boolean") { return entry.requireMention; } @@ -163,7 +160,7 @@ export function resolveGroupRequireMention(params: { channelName ?? "", normalizedName, ].filter(Boolean); - let matched: { requireMention?: boolean; autoReply?: boolean } | undefined; + let matched: { requireMention?: boolean } | undefined; for (const candidate of candidates) { if (candidate && channels[candidate]) { matched = channels[candidate]; @@ -172,9 +169,6 @@ export function resolveGroupRequireMention(params: { } const fallback = channels["*"]; const resolved = matched ?? fallback; - if (typeof resolved?.autoReply === "boolean") { - return !resolved.autoReply; - } if (typeof resolved?.requireMention === "boolean") { return resolved.requireMention; } diff --git a/src/config/types.ts b/src/config/types.ts index caae79a9b..e5a23ad01 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -237,12 +237,11 @@ export type TelegramActionConfig = { }; export type TelegramTopicConfig = { + requireMention?: boolean; /** If specified, only load these skills for this topic. Omit = all skills; empty = no skills. */ skills?: string[]; /** If false, disable the bot for this topic. */ enabled?: boolean; - /** If true, reply to every message (no mention required). */ - autoReply?: boolean; /** Optional allowlist for topic senders (ids or usernames). */ allowFrom?: Array; /** Optional system prompt snippet for this topic. */ @@ -257,8 +256,6 @@ export type TelegramGroupConfig = { topics?: Record; /** If false, disable the bot for this group (and its topics). */ enabled?: boolean; - /** If true, reply to every message (no mention required). */ - autoReply?: boolean; /** Optional allowlist for group senders (ids or usernames). */ allowFrom?: Array; /** Optional system prompt snippet for this group. */ @@ -325,8 +322,6 @@ export type DiscordGuildChannelConfig = { skills?: string[]; /** If false, disable the bot for this channel. */ enabled?: boolean; - /** If true, reply to every message (no mention required). */ - autoReply?: boolean; /** Optional allowlist for channel senders (ids or names). */ users?: Array; /** Optional system prompt snippet for this channel. */ @@ -412,8 +407,6 @@ export type SlackChannelConfig = { allow?: boolean; /** Require mentioning the bot to trigger replies. */ requireMention?: boolean; - /** Reply to all messages without needing a mention. */ - autoReply?: boolean; /** Allowlist of users that can invoke the bot in this channel. */ users?: Array; /** Optional skill filter for this channel. */ diff --git a/src/config/zod-schema.ts b/src/config/zod-schema.ts index fc701fe5d..28eeb4d1b 100644 --- a/src/config/zod-schema.ts +++ b/src/config/zod-schema.ts @@ -787,7 +787,6 @@ export const ClawdbotSchema = z.object({ requireMention: z.boolean().optional(), skills: z.array(z.string()).optional(), enabled: z.boolean().optional(), - autoReply: z.boolean().optional(), allowFrom: z.array(z.union([z.string(), z.number()])).optional(), systemPrompt: z.string().optional(), topics: z @@ -795,9 +794,9 @@ export const ClawdbotSchema = z.object({ z.string(), z .object({ + requireMention: z.boolean().optional(), skills: z.array(z.string()).optional(), enabled: z.boolean().optional(), - autoReply: z.boolean().optional(), allowFrom: z .array(z.union([z.string(), z.number()])) .optional(), @@ -913,7 +912,6 @@ export const ClawdbotSchema = z.object({ requireMention: z.boolean().optional(), skills: z.array(z.string()).optional(), enabled: z.boolean().optional(), - autoReply: z.boolean().optional(), users: z .array(z.union([z.string(), z.number()])) .optional(), @@ -990,7 +988,6 @@ export const ClawdbotSchema = z.object({ enabled: z.boolean().optional(), allow: z.boolean().optional(), requireMention: z.boolean().optional(), - autoReply: z.boolean().optional(), users: z.array(z.union([z.string(), z.number()])).optional(), skills: z.array(z.string()).optional(), systemPrompt: z.string().optional(), diff --git a/src/discord/monitor.test.ts b/src/discord/monitor.test.ts index 8afe6d33c..59555ae33 100644 --- a/src/discord/monitor.test.ts +++ b/src/discord/monitor.test.ts @@ -101,7 +101,6 @@ describe("discord guild/channel resolution", () => { requireMention: true, skills: ["search"], enabled: false, - autoReply: true, users: ["123"], systemPrompt: "Use short answers.", }, @@ -126,7 +125,6 @@ describe("discord guild/channel resolution", () => { expect(help?.requireMention).toBe(true); expect(help?.skills).toEqual(["search"]); expect(help?.enabled).toBe(false); - expect(help?.autoReply).toBe(true); expect(help?.users).toEqual(["123"]); expect(help?.systemPrompt).toBe("Use short answers."); }); diff --git a/src/discord/monitor.ts b/src/discord/monitor.ts index aa3cbc2a2..473d18eab 100644 --- a/src/discord/monitor.ts +++ b/src/discord/monitor.ts @@ -101,7 +101,6 @@ export type DiscordGuildEntryResolved = { requireMention?: boolean; skills?: string[]; enabled?: boolean; - autoReply?: boolean; users?: Array; systemPrompt?: string; } @@ -113,7 +112,6 @@ export type DiscordChannelConfigResolved = { requireMention?: boolean; skills?: string[]; enabled?: boolean; - autoReply?: boolean; users?: Array; systemPrompt?: string; }; @@ -601,14 +599,8 @@ export function createDiscordMessageHandler(params: { guildHistories.set(message.channelId, history); } - const baseRequireMention = - channelConfig?.requireMention ?? guildInfo?.requireMention ?? true; const shouldRequireMention = - channelConfig?.autoReply === true - ? false - : channelConfig?.autoReply === false - ? true - : baseRequireMention; + channelConfig?.requireMention ?? guildInfo?.requireMention ?? true; const hasAnyMention = Boolean( !isDirectMessage && (message.mentionedEveryone || @@ -1810,7 +1802,6 @@ export function resolveDiscordChannelConfig(params: { requireMention: byId.requireMention, skills: byId.skills, enabled: byId.enabled, - autoReply: byId.autoReply, users: byId.users, systemPrompt: byId.systemPrompt, }; @@ -1821,7 +1812,6 @@ export function resolveDiscordChannelConfig(params: { requireMention: entry.requireMention, skills: entry.skills, enabled: entry.enabled, - autoReply: entry.autoReply, users: entry.users, systemPrompt: entry.systemPrompt, }; @@ -1833,7 +1823,6 @@ export function resolveDiscordChannelConfig(params: { requireMention: entry.requireMention, skills: entry.skills, enabled: entry.enabled, - autoReply: entry.autoReply, users: entry.users, systemPrompt: entry.systemPrompt, }; diff --git a/src/slack/monitor.ts b/src/slack/monitor.ts index ff6229656..91586f2ba 100644 --- a/src/slack/monitor.ts +++ b/src/slack/monitor.ts @@ -159,7 +159,6 @@ type SlackThreadBroadcastEvent = { type SlackChannelConfigResolved = { allowed: boolean; requireMention: boolean; - autoReply?: boolean; users?: Array; skills?: string[]; systemPrompt?: string; @@ -284,7 +283,6 @@ function resolveSlackChannelConfig(params: { enabled?: boolean; allow?: boolean; requireMention?: boolean; - autoReply?: boolean; users?: Array; skills?: string[]; systemPrompt?: string; @@ -308,7 +306,6 @@ function resolveSlackChannelConfig(params: { enabled?: boolean; allow?: boolean; requireMention?: boolean; - autoReply?: boolean; users?: Array; skills?: string[]; systemPrompt?: string; @@ -341,14 +338,13 @@ function resolveSlackChannelConfig(params: { const requireMention = firstDefined(resolved.requireMention, fallback?.requireMention, true) ?? true; - const autoReply = firstDefined(resolved.autoReply, fallback?.autoReply); const users = firstDefined(resolved.users, fallback?.users); const skills = firstDefined(resolved.skills, fallback?.skills); const systemPrompt = firstDefined( resolved.systemPrompt, fallback?.systemPrompt, ); - return { allowed, requireMention, autoReply, users, skills, systemPrompt }; + return { allowed, requireMention, users, skills, systemPrompt }; } async function resolveSlackMedia(params: { @@ -810,11 +806,7 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) { surface: "slack", }); const shouldRequireMention = isRoom - ? channelConfig?.autoReply === true - ? false - : channelConfig?.autoReply === false - ? true - : (channelConfig?.requireMention ?? true) + ? (channelConfig?.requireMention ?? true) : false; const shouldBypassMention = allowTextCommands && diff --git a/src/telegram/bot.test.ts b/src/telegram/bot.test.ts index 48073b216..f43faf89a 100644 --- a/src/telegram/bot.test.ts +++ b/src/telegram/bot.test.ts @@ -704,6 +704,50 @@ describe("createTelegramBot", () => { expect(replySpy).toHaveBeenCalledTimes(1); }); + it("allows per-topic requireMention override", async () => { + onSpy.mockReset(); + const replySpy = replyModule.__replySpy as unknown as ReturnType< + typeof vi.fn + >; + replySpy.mockReset(); + loadConfig.mockReturnValue({ + telegram: { + groups: { + "*": { requireMention: true }, + "-1001234567890": { + requireMention: true, + topics: { + "99": { requireMention: false }, + }, + }, + }, + }, + }); + + createTelegramBot({ token: "tok" }); + const handler = onSpy.mock.calls[0][1] as ( + ctx: Record, + ) => Promise; + + await handler({ + message: { + chat: { + id: -1001234567890, + type: "supergroup", + title: "Forum Group", + is_forum: true, + }, + text: "hello", + date: 1736380800, + message_thread_id: 99, + }, + me: { username: "clawdbot_bot" }, + getFile: async () => ({ download: async () => new Uint8Array() }), + }); + + expect(replySpy).toHaveBeenCalledTimes(1); + }); + it("honors groups default when no explicit group override exists", async () => { onSpy.mockReset(); const replySpy = replyModule.__replySpy as unknown as ReturnType< diff --git a/src/telegram/bot.ts b/src/telegram/bot.ts index 7bfc48e43..034ce8059 100644 --- a/src/telegram/bot.ts +++ b/src/telegram/bot.ts @@ -381,16 +381,11 @@ export function createTelegramBot(opts: TelegramBotOptions) { (ent) => ent.type === "mention", ); const baseRequireMention = resolveGroupRequireMention(chatId); - const autoReplySetting = firstDefined( - topicConfig?.autoReply, - groupConfig?.autoReply, + const requireMention = firstDefined( + topicConfig?.requireMention, + groupConfig?.requireMention, + baseRequireMention, ); - const requireMention = - autoReplySetting === true - ? false - : autoReplySetting === false - ? true - : baseRequireMention; const shouldBypassMention = isGroup && requireMention &&