diff --git a/CHANGELOG.md b/CHANGELOG.md index 68a3d120d..e09aa25b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Docs: https://docs.clawd.bot - Nodes: expose node PATH in status/describe and bootstrap PATH for node-host execution. - CLI: flatten node service commands under `clawdbot node` and remove `service node` docs. - CLI: move gateway service commands under `clawdbot gateway` and add `gateway probe` for reachability. +- Queue: allow per-channel debounce overrides and plugin defaults. (#1190) Thanks @cheeeee. ### Fixes - Embedded runner: persist injected history images so attachments aren’t reloaded each turn. (#1374) Thanks @Nicell. diff --git a/src/auto-reply/inbound-debounce.ts b/src/auto-reply/inbound-debounce.ts index 47a29687d..9d43fa8f7 100644 --- a/src/auto-reply/inbound-debounce.ts +++ b/src/auto-reply/inbound-debounce.ts @@ -11,8 +11,7 @@ const resolveChannelOverride = (params: { channel: string; }): number | undefined => { if (!params.byChannel) return undefined; - const channelKey = params.channel as keyof InboundDebounceByProvider; - return resolveMs(params.byChannel[channelKey]); + return resolveMs(params.byChannel[params.channel]); }; export function resolveInboundDebounceMs(params: { diff --git a/src/auto-reply/reply/queue/settings.ts b/src/auto-reply/reply/queue/settings.ts index 5fd9797dc..9591a8bc4 100644 --- a/src/auto-reply/reply/queue/settings.ts +++ b/src/auto-reply/reply/queue/settings.ts @@ -1,3 +1,5 @@ +import { getChannelPlugin } from "../../../channels/plugins/index.js"; +import type { InboundDebounceByProvider } from "../../../config/types.messages.js"; import { normalizeQueueDropPolicy, normalizeQueueMode } from "./normalize.js"; import { DEFAULT_QUEUE_CAP, DEFAULT_QUEUE_DEBOUNCE_MS, DEFAULT_QUEUE_DROP } from "./state.js"; import type { QueueMode, QueueSettings, ResolveQueueSettingsParams } from "./types.js"; @@ -6,6 +8,23 @@ function defaultQueueModeForChannel(_channel?: string): QueueMode { return "collect"; } +/** Resolve per-channel debounce override from debounceMsByChannel map. */ +function resolveChannelDebounce( + byChannel: InboundDebounceByProvider | undefined, + channelKey: string | undefined, +): number | undefined { + if (!channelKey || !byChannel) return undefined; + const value = byChannel[channelKey]; + return typeof value === "number" && Number.isFinite(value) ? Math.max(0, value) : undefined; +} + +function resolvePluginDebounce(channelKey: string | undefined): number | undefined { + if (!channelKey) return undefined; + const plugin = getChannelPlugin(channelKey); + const value = plugin?.defaults?.queue?.debounceMs; + return typeof value === "number" && Number.isFinite(value) ? Math.max(0, value) : undefined; +} + export function resolveQueueSettings(params: ResolveQueueSettingsParams): QueueSettings { const channelKey = params.channel?.trim().toLowerCase(); const queueCfg = params.cfg.messages?.queue; @@ -22,6 +41,8 @@ export function resolveQueueSettings(params: ResolveQueueSettingsParams): QueueS const debounceRaw = params.inlineOptions?.debounceMs ?? params.sessionEntry?.queueDebounceMs ?? + resolveChannelDebounce(queueCfg?.debounceMsByChannel, channelKey) ?? + resolvePluginDebounce(channelKey) ?? queueCfg?.debounceMs ?? DEFAULT_QUEUE_DEBOUNCE_MS; const capRaw = diff --git a/src/channels/plugins/types.plugin.ts b/src/channels/plugins/types.plugin.ts index 38ed40666..5aeab17d3 100644 --- a/src/channels/plugins/types.plugin.ts +++ b/src/channels/plugins/types.plugin.ts @@ -48,6 +48,11 @@ export type ChannelPlugin = { id: ChannelId; meta: ChannelMeta; capabilities: ChannelCapabilities; + defaults?: { + queue?: { + debounceMs?: number; + }; + }; reload?: { configPrefixes: string[]; noopPrefixes?: string[] }; // CLI onboarding wizard hooks for this channel. onboarding?: ChannelOnboardingAdapter; diff --git a/src/config/types.messages.ts b/src/config/types.messages.ts index 691ca617a..7499f79a0 100644 --- a/src/config/types.messages.ts +++ b/src/config/types.messages.ts @@ -13,20 +13,13 @@ export type QueueConfig = { mode?: QueueMode; byChannel?: QueueModeByProvider; debounceMs?: number; + /** Per-channel debounce overrides (ms). */ + debounceMsByChannel?: InboundDebounceByProvider; cap?: number; drop?: QueueDropPolicy; }; -export type InboundDebounceByProvider = { - whatsapp?: number; - telegram?: number; - discord?: number; - slack?: number; - signal?: number; - imessage?: number; - msteams?: number; - webchat?: number; -}; +export type InboundDebounceByProvider = Record; export type InboundDebounceConfig = { debounceMs?: number; diff --git a/src/config/zod-schema.core.ts b/src/config/zod-schema.core.ts index 6e7b34b0d..48066963f 100644 --- a/src/config/zod-schema.core.ts +++ b/src/config/zod-schema.core.ts @@ -217,17 +217,7 @@ export const QueueModeBySurfaceSchema = z .optional(); export const DebounceMsBySurfaceSchema = z - .object({ - whatsapp: z.number().int().nonnegative().optional(), - telegram: z.number().int().nonnegative().optional(), - discord: z.number().int().nonnegative().optional(), - slack: z.number().int().nonnegative().optional(), - signal: z.number().int().nonnegative().optional(), - imessage: z.number().int().nonnegative().optional(), - msteams: z.number().int().nonnegative().optional(), - webchat: z.number().int().nonnegative().optional(), - }) - .strict() + .record(z.string(), z.number().int().nonnegative()) .optional(); export const QueueSchema = z @@ -235,6 +225,7 @@ export const QueueSchema = z mode: QueueModeSchema.optional(), byChannel: QueueModeBySurfaceSchema, debounceMs: z.number().int().nonnegative().optional(), + debounceMsByChannel: DebounceMsBySurfaceSchema, cap: z.number().int().positive().optional(), drop: QueueDropSchema.optional(), })