* 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>
49 lines
1.4 KiB
TypeScript
49 lines
1.4 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
|
|
import { createInboundDebouncer } from "./inbound-debounce.js";
|
|
|
|
describe("createInboundDebouncer", () => {
|
|
it("debounces and combines items", async () => {
|
|
vi.useFakeTimers();
|
|
const calls: Array<string[]> = [];
|
|
|
|
const debouncer = createInboundDebouncer<{ key: string; id: string }>({
|
|
debounceMs: 10,
|
|
buildKey: (item) => item.key,
|
|
onFlush: async (items) => {
|
|
calls.push(items.map((entry) => entry.id));
|
|
},
|
|
});
|
|
|
|
await debouncer.enqueue({ key: "a", id: "1" });
|
|
await debouncer.enqueue({ key: "a", id: "2" });
|
|
|
|
expect(calls).toEqual([]);
|
|
await vi.advanceTimersByTimeAsync(10);
|
|
expect(calls).toEqual([["1", "2"]]);
|
|
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it("flushes buffered items before non-debounced item", async () => {
|
|
vi.useFakeTimers();
|
|
const calls: Array<string[]> = [];
|
|
|
|
const debouncer = createInboundDebouncer<{ key: string; id: string; debounce: boolean }>({
|
|
debounceMs: 50,
|
|
buildKey: (item) => item.key,
|
|
shouldDebounce: (item) => item.debounce,
|
|
onFlush: async (items) => {
|
|
calls.push(items.map((entry) => entry.id));
|
|
},
|
|
});
|
|
|
|
await debouncer.enqueue({ key: "a", id: "1", debounce: true });
|
|
await debouncer.enqueue({ key: "a", id: "2", debounce: false });
|
|
|
|
expect(calls).toEqual([["1"], ["2"]]);
|
|
|
|
vi.useRealTimers();
|
|
});
|
|
});
|