From 8ef9a77e6378b60880547a7ccc1955b8027903e8 Mon Sep 17 00:00:00 2001 From: KimGLee <05_bolster_inkling@icloud.com> Date: Tue, 3 Mar 2026 00:16:34 +0800 Subject: [PATCH] fix(followup): scope session dedupe fingerprints to single-target runs --- src/auto-reply/reply/followup-runner.test.ts | 34 ++++++++++++++++++++ src/auto-reply/reply/followup-runner.ts | 11 +++++-- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/src/auto-reply/reply/followup-runner.test.ts b/src/auto-reply/reply/followup-runner.test.ts index db4a086923c..82f95ee5551 100644 --- a/src/auto-reply/reply/followup-runner.test.ts +++ b/src/auto-reply/reply/followup-runner.test.ts @@ -506,6 +506,40 @@ describe("createFollowupRunner messaging tool dedupe", () => { expect(onBlockReply).toHaveBeenCalled(); }); + it("does not reuse session-level text dedupe when prior run had multiple messaging targets", async () => { + const onBlockReply = vi.fn(async () => {}); + const sessionEntry: SessionEntry = { + sessionId: "session", + updatedAt: Date.now(), + lastMessagingToolSessionId: "session", + lastMessagingToolSentAt: Date.now(), + lastMessagingToolSentTexts: ["hello world!"], + lastMessagingToolSentTargets: [ + { tool: "message", provider: "telegram", to: "123" }, + { tool: "message", provider: "telegram", to: "999" }, + ], + }; + const sessionStore: Record = { main: sessionEntry }; + + runEmbeddedPiAgentMock.mockResolvedValueOnce({ + payloads: [{ text: "hello world!" }], + meta: {}, + }); + + const runner = createMessagingDedupeRunner(onBlockReply, { + sessionEntry, + sessionStore, + sessionKey: "main", + }); + + await runner({ + ...baseQueuedRun("telegram"), + originatingTo: "123", + }); + + expect(onBlockReply).toHaveBeenCalled(); + }); + it("drops media URL from payload when messaging tool already sent it", async () => { const onBlockReply = vi.fn(async () => {}); runEmbeddedPiAgentMock.mockResolvedValueOnce({ diff --git a/src/auto-reply/reply/followup-runner.ts b/src/auto-reply/reply/followup-runner.ts index 2793f588c94..9344c1e78ed 100644 --- a/src/auto-reply/reply/followup-runner.ts +++ b/src/auto-reply/reply/followup-runner.ts @@ -335,22 +335,27 @@ export function createFollowupRunner(params: { typeof sessionEntry?.lastMessagingToolSentAt === "number" && sessionEntry?.lastMessagingToolSessionId === queued.run.sessionId && now - sessionEntry.lastMessagingToolSentAt <= RECENT_MESSAGING_TOOL_DEDUPE_WINDOW_MS; + const previousSentTargets = sessionEntry?.lastMessagingToolSentTargets ?? []; const recentTargetMatch = recentWindowActive && shouldSuppressMessagingToolReplies({ messageProvider, - messagingToolSentTargets: sessionEntry?.lastMessagingToolSentTargets, + messagingToolSentTargets: previousSentTargets, originatingTo, accountId: originAccountId, }); + const canReuseSessionDedupeFingerprints = + recentTargetMatch && previousSentTargets.length <= 1; const sentTexts = [ ...(runResult.messagingToolSentTexts ?? []), - ...(recentTargetMatch ? (sessionEntry?.lastMessagingToolSentTexts ?? []) : []), + ...(canReuseSessionDedupeFingerprints ? (sessionEntry?.lastMessagingToolSentTexts ?? []) : []), ]; const sentMediaUrls = [ ...(runResult.messagingToolSentMediaUrls ?? []), - ...(recentTargetMatch ? (sessionEntry?.lastMessagingToolSentMediaUrls ?? []) : []), + ...(canReuseSessionDedupeFingerprints + ? (sessionEntry?.lastMessagingToolSentMediaUrls ?? []) + : []), ]; // Keep target-based suppression scoped to the current run only. // Session-level dedupe state is used for text/media duplicate filtering when target matches.