fix(reply): isolate cross-run dedupe by session id and clear on reset

This commit is contained in:
Kim 2026-02-27 14:44:30 +08:00 committed by KimGLee
parent 91ae4468f5
commit b8d8e59b89
4 changed files with 43 additions and 0 deletions

View File

@ -294,6 +294,11 @@ export async function runReplyAgent(params: {
fallbackNoticeSelectedModel: undefined,
fallbackNoticeActiveModel: undefined,
fallbackNoticeReason: undefined,
lastMessagingToolSessionId: undefined,
lastMessagingToolSentAt: undefined,
lastMessagingToolSentTexts: undefined,
lastMessagingToolSentMediaUrls: undefined,
lastMessagingToolSentTargets: undefined,
};
const agentId = resolveAgentIdFromSessionKey(sessionKey);
const nextSessionFile = resolveSessionTranscriptPath(
@ -423,6 +428,7 @@ export async function runReplyAgent(params: {
if (sessionDedupeEntry) {
const now = Date.now();
if (sentTexts.length || sentMediaUrls.length || sentTargets.length) {
sessionDedupeEntry.lastMessagingToolSessionId = followupRun.run.sessionId;
sessionDedupeEntry.lastMessagingToolSentAt = now;
sessionDedupeEntry.lastMessagingToolSentTexts = sentTexts;
sessionDedupeEntry.lastMessagingToolSentMediaUrls = sentMediaUrls;
@ -431,6 +437,7 @@ export async function runReplyAgent(params: {
typeof sessionDedupeEntry.lastMessagingToolSentAt === "number" &&
now - sessionDedupeEntry.lastMessagingToolSentAt > RECENT_MESSAGING_TOOL_DEDUPE_WINDOW_MS
) {
delete sessionDedupeEntry.lastMessagingToolSessionId;
delete sessionDedupeEntry.lastMessagingToolSentAt;
delete sessionDedupeEntry.lastMessagingToolSentTexts;
delete sessionDedupeEntry.lastMessagingToolSentMediaUrls;

View File

@ -421,6 +421,7 @@ describe("createFollowupRunner messaging tool dedupe", () => {
const sessionEntry: SessionEntry = {
sessionId: "session",
updatedAt: Date.now(),
lastMessagingToolSessionId: "session",
lastMessagingToolSentAt: Date.now(),
lastMessagingToolSentTexts: ["hello world!"],
lastMessagingToolSentTargets: [{ tool: "message", provider: "telegram", to: "123" }],
@ -446,6 +447,38 @@ describe("createFollowupRunner messaging tool dedupe", () => {
expect(onBlockReply).not.toHaveBeenCalled();
});
it("does not use session-level dedupe from a previous session id", async () => {
const onBlockReply = vi.fn(async () => {});
const sessionEntry: SessionEntry = {
sessionId: "current-session",
updatedAt: Date.now(),
lastMessagingToolSessionId: "old-session",
lastMessagingToolSentAt: Date.now(),
lastMessagingToolSentTexts: ["hello world!"],
lastMessagingToolSentTargets: [{ tool: "message", provider: "telegram", to: "123" }],
};
const sessionStore: Record<string, SessionEntry> = { main: sessionEntry };
runEmbeddedPiAgentMock.mockResolvedValueOnce({
payloads: [{ text: "hello world!" }],
meta: {},
});
const runner = createMessagingDedupeRunner(onBlockReply, {
sessionEntry,
sessionStore,
sessionKey: "main",
});
await runner({
...baseQueuedRun("telegram"),
originatingTo: "123",
run: { ...baseQueuedRun("telegram").run, sessionId: "current-session" },
});
expect(onBlockReply).toHaveBeenCalled();
});
it("does not use session-level text dedupe when recent target does not match", async () => {
const onBlockReply = vi.fn(async () => {});
const sessionEntry: SessionEntry = {

View File

@ -333,6 +333,7 @@ export function createFollowupRunner(params: {
const now = Date.now();
const recentWindowActive =
typeof sessionEntry?.lastMessagingToolSentAt === "number" &&
sessionEntry?.lastMessagingToolSessionId === queued.run.sessionId &&
now - sessionEntry.lastMessagingToolSentAt <= RECENT_MESSAGING_TOOL_DEDUPE_WINDOW_MS;
const recentTargetMatch =
recentWindowActive &&

View File

@ -76,6 +76,8 @@ export type SessionEntry = {
lastHeartbeatSentAt?: number;
/** Timestamp (ms) for the most recent message-tool send fingerprint (cross-run dedupe). */
lastMessagingToolSentAt?: number;
/** Session id that produced the most recent message-tool dedupe fingerprint. */
lastMessagingToolSessionId?: string;
/** Recently sent message-tool text payloads for short-window cross-run dedupe. */
lastMessagingToolSentTexts?: string[];
/** Recently sent message-tool media urls for short-window cross-run dedupe. */