feat(whatsapp): add debounceMs for batching rapid messages (#971)

* feat(whatsapp): add debounceMs for batching rapid messages

Add a `debounceMs` configuration option to WhatsApp channel settings
that batches rapid consecutive messages from the same sender into a
single response. This prevents triggering separate agent runs for
each message when a user sends multiple short messages in quick
succession (e.g., "Hey!", "how are you?", "I was wondering...").

Changes:
- Add `debounceMs` config to WhatsAppConfig and WhatsAppAccountConfig
- Implement message buffering in `monitorWebInbox` with:
  - Map-based buffer keyed by sender (DM) or chat ID (groups)
  - Debounce timer that resets on each new message
  - Message combination with newline separator
  - Single message optimization (no modification if only one message)
- Wire `debounceMs` through account resolution and monitor tuning
- Add UI hints and schema documentation

Usage example:
{
  "channels": {
    "whatsapp": {
      "debounceMs": 5000  // 5 second window
    }
  }
}

Default behavior: `debounceMs: 0` (disabled by default)

Verified: All existing tests pass (3204 tests), TypeScript compilation
succeeds with no errors.

Implemented with assistance from AI coding tools.

Closes #967

* chore: wip inbound debounce

* fix: debounce inbound messages across channels (#971) (thanks @juanpablodlc)

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
juanpablodlc
2026-01-15 15:07:19 -08:00
committed by GitHub
parent 7dea403302
commit 4a99b9b651
26 changed files with 927 additions and 212 deletions

View File

@@ -1,5 +1,7 @@
import { DEFAULT_GROUP_HISTORY_LIMIT } from "../../auto-reply/reply/history.js";
import { getReplyFromConfig } from "../../auto-reply/reply.js";
import { hasControlCommand } from "../../auto-reply/command-detection.js";
import { resolveInboundDebounceMs } from "../../auto-reply/inbound-debounce.js";
import { waitForever } from "../../cli/wait.js";
import { loadConfig } from "../../config/config.js";
import { logVerbose } from "../../globals.js";
@@ -169,12 +171,22 @@ export async function monitorWebChannel(
account,
});
const inboundDebounceMs = resolveInboundDebounceMs({ cfg, channel: "whatsapp" });
const shouldDebounce = (msg: WebInboundMsg) => {
if (msg.mediaPath || msg.mediaType) return false;
if (msg.location) return false;
if (msg.replyToId || msg.replyToBody) return false;
return !hasControlCommand(msg.body, cfg);
};
const listener = await (listenerFactory ?? monitorWebInbox)({
verbose,
accountId: account.accountId,
authDir: account.authDir,
mediaMaxMb: account.mediaMaxMb,
sendReadReceipts: account.sendReadReceipts,
debounceMs: inboundDebounceMs,
shouldDebounce,
onMessage: async (msg: WebInboundMsg) => {
handledMessages += 1;
lastMessageAt = Date.now();

View File

@@ -30,4 +30,6 @@ export type WebMonitorTuning = {
statusSink?: (status: WebChannelStatus) => void;
/** WhatsApp account id. Default: "default". */
accountId?: string;
/** Debounce window (ms) for batching rapid consecutive messages from the same sender. */
debounceMs?: number;
};