fix(queue): deduplicate followup queue entries to prevent duplicate responses

## Problem

When messages arrived while the agent was busy processing a previous message,
the same message could be enqueued multiple times into the followup queue.
This happened because Discord's event system can emit the same message multiple
times (e.g., during reconnects or due to slow listener processing), and the
followup queue had no deduplication logic.

This caused the bot to respond to the same user message 2-4+ times.

## Solution

Add simple exact-match deduplication in `enqueueFollowupRun()`: if a prompt
is already in the queue, skip adding it again. Extracted into a small
`isPromptAlreadyQueued()` helper for clarity.

## Testing

- Added test cases for deduplication (same prompt rejected, different accepted)
- Manually verified on Discord: single response per message even when multiple
  events fire during slow agent processing
This commit is contained in:
Samrat Jha
2026-01-09 13:35:55 -05:00
committed by Peter Steinberger
parent 85fab7afe3
commit 9185fdc896
2 changed files with 121 additions and 0 deletions

View File

@@ -29,6 +29,111 @@ function createRun(params: {
};
}
describe("followup queue deduplication", () => {
it("deduplicates messages with same Discord message_id", async () => {
const key = `test-dedup-message-id-${Date.now()}`;
const calls: FollowupRun[] = [];
const runFollowup = async (run: FollowupRun) => {
calls.push(run);
};
const settings: QueueSettings = {
mode: "collect",
debounceMs: 0,
cap: 50,
dropPolicy: "summarize",
};
// First enqueue should succeed
const first = enqueueFollowupRun(
key,
createRun({
prompt: "[Discord Guild #test channel id:123] Hello",
originatingChannel: "discord",
originatingTo: "channel:123",
}),
settings,
);
expect(first).toBe(true);
// Second enqueue with same prompt should be deduplicated
const second = enqueueFollowupRun(
key,
createRun({
prompt: "[Discord Guild #test channel id:123] Hello",
originatingChannel: "discord",
originatingTo: "channel:123",
}),
settings,
);
expect(second).toBe(false);
// Third enqueue with different prompt should succeed
const third = enqueueFollowupRun(
key,
createRun({
prompt: "[Discord Guild #test channel id:123] World",
originatingChannel: "discord",
originatingTo: "channel:123",
}),
settings,
);
expect(third).toBe(true);
scheduleFollowupDrain(key, runFollowup);
await expect.poll(() => calls.length).toBe(1);
// Should collect both unique messages
expect(calls[0]?.prompt).toContain(
"[Queued messages while agent was busy]",
);
});
it("deduplicates across different providers using exact prompt match", async () => {
const key = `test-dedup-whatsapp-${Date.now()}`;
const settings: QueueSettings = {
mode: "collect",
debounceMs: 0,
cap: 50,
dropPolicy: "summarize",
};
// First enqueue should succeed
const first = enqueueFollowupRun(
key,
createRun({
prompt: "Hello world",
originatingChannel: "whatsapp",
originatingTo: "+1234567890",
}),
settings,
);
expect(first).toBe(true);
// Second enqueue with same prompt should be deduplicated
const second = enqueueFollowupRun(
key,
createRun({
prompt: "Hello world",
originatingChannel: "whatsapp",
originatingTo: "+1234567890",
}),
settings,
);
expect(second).toBe(false);
// Third enqueue with different prompt should succeed
const third = enqueueFollowupRun(
key,
createRun({
prompt: "Hello world 2",
originatingChannel: "whatsapp",
originatingTo: "+1234567890",
}),
settings,
);
expect(third).toBe(true);
});
});
describe("followup queue collect routing", () => {
it("does not collect when destinations differ", async () => {
const key = `test-collect-diff-to-${Date.now()}`;

View File

@@ -337,6 +337,16 @@ function getFollowupQueue(
FOLLOWUP_QUEUES.set(key, created);
return created;
}
/**
* Check if a prompt is already queued using exact match.
*/
function isPromptAlreadyQueued(
prompt: string,
queue: FollowupQueueState,
): boolean {
return queue.items.some((item) => item.prompt === prompt);
}
export function enqueueFollowupRun(
key: string,
run: FollowupRun,
@@ -345,6 +355,12 @@ export function enqueueFollowupRun(
const queue = getFollowupQueue(key, settings);
queue.lastEnqueuedAt = Date.now();
queue.lastRun = run.run;
// Deduplicate: skip if the same prompt is already queued.
if (isPromptAlreadyQueued(run.prompt, queue)) {
return false;
}
const cap = queue.cap;
if (cap > 0 && queue.items.length >= cap) {
if (queue.dropPolicy === "new") {