Skip to content

Commit 97dae1f

Browse files
committed
fix(agents): dedupe messaging tool replies by route
1 parent 050f0f5 commit 97dae1f

7 files changed

Lines changed: 133 additions & 35 deletions

src/auto-reply/reply/agent-runner-payloads.ts

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -188,26 +188,23 @@ export async function buildReplyPayloads(params: {
188188
const dedupeRuntime = shouldCheckMessagingToolDedupe
189189
? await loadReplyPayloadsDedupeRuntime()
190190
: null;
191-
const suppressMessagingToolReplies =
192-
dedupeRuntime?.shouldSuppressMessagingToolReplies({
193-
messageProvider: resolveOriginMessageProvider({
194-
originatingChannel: params.originatingChannel,
195-
provider: params.messageProvider,
196-
}),
197-
messagingToolSentTargets,
198-
originatingTo: resolveOriginMessageTo({
199-
originatingTo: params.originatingTo,
200-
}),
201-
accountId: resolveOriginAccountId({
202-
originatingAccountId: params.accountId,
203-
}),
204-
}) ?? false;
205-
// Only dedupe against messaging tool sends for the same origin target.
206-
// Cross-target sends (for example posting to another channel) must not
207-
// suppress the current conversation's final reply.
208-
// If target metadata is unavailable, keep legacy dedupe behavior.
209-
const dedupeMessagingToolPayloads =
210-
suppressMessagingToolReplies || messagingToolSentTargets.length === 0;
191+
const messagingToolPayloadDedupe = dedupeRuntime?.resolveMessagingToolPayloadDedupe({
192+
messageProvider: resolveOriginMessageProvider({
193+
originatingChannel: params.originatingChannel,
194+
provider: params.messageProvider,
195+
}),
196+
messagingToolSentTargets,
197+
originatingTo: resolveOriginMessageTo({
198+
originatingTo: params.originatingTo,
199+
}),
200+
accountId: resolveOriginAccountId({
201+
originatingAccountId: params.accountId,
202+
}),
203+
}) ?? {
204+
shouldDedupePayloads: shouldCheckMessagingToolDedupe && messagingToolSentTargets.length === 0,
205+
suppressReplies: false,
206+
};
207+
const dedupeMessagingToolPayloads = messagingToolPayloadDedupe.shouldDedupePayloads;
211208
const messagingToolSentMediaUrls = dedupeMessagingToolPayloads
212209
? await normalizeSentMediaUrlsForDedupe({
213210
sentMediaUrls: params.messagingToolSentMediaUrls ?? [],
@@ -284,7 +281,7 @@ export async function buildReplyPayloads(params: {
284281
sentMediaUrls: blockSentMediaUrls,
285282
})
286283
: contentSuppressedPayloads;
287-
const replyPayloads = suppressMessagingToolReplies
284+
const replyPayloads = messagingToolPayloadDedupe.suppressReplies
288285
? []
289286
: filteredPayloads.filter(isRenderablePayload);
290287

src/auto-reply/reply/followup-delivery.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,32 @@ describe("resolveFollowupDeliveryPayloads", () => {
4343
).toEqual([{ mediaUrl: undefined, mediaUrls: undefined }]);
4444
});
4545

46+
it("does not dedupe text sent via messaging tool to another target", () => {
47+
expect(
48+
resolveFollowupDeliveryPayloads({
49+
cfg: baseConfig,
50+
payloads: [{ text: "hello world!" }],
51+
messageProvider: "telegram",
52+
originatingTo: "telegram:123",
53+
sentTexts: ["hello world!"],
54+
sentTargets: [{ tool: "discord", provider: "discord", to: "channel:C1" }],
55+
}),
56+
).toEqual([{ text: "hello world!" }]);
57+
});
58+
59+
it("does not dedupe media sent via messaging tool to another target", () => {
60+
expect(
61+
resolveFollowupDeliveryPayloads({
62+
cfg: baseConfig,
63+
payloads: [{ text: "photo", mediaUrl: "file:///tmp/photo.jpg" }],
64+
messageProvider: "telegram",
65+
originatingTo: "telegram:123",
66+
sentMediaUrls: ["file:///tmp/photo.jpg"],
67+
sentTargets: [{ tool: "slack", provider: "slack", to: "channel:C1" }],
68+
}),
69+
).toEqual([{ text: "photo", mediaUrl: "file:///tmp/photo.jpg" }]);
70+
});
71+
4672
it("suppresses replies when a messaging tool already sent to the same provider and target", () => {
4773
expect(
4874
resolveFollowupDeliveryPayloads({

src/auto-reply/reply/followup-delivery.ts

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
applyReplyThreading,
1313
filterMessagingToolDuplicates,
1414
filterMessagingToolMediaDuplicates,
15-
shouldSuppressMessagingToolReplies,
15+
resolveMessagingToolPayloadDedupe,
1616
} from "./reply-payloads.js";
1717
import { resolveReplyToMode } from "./reply-threading.js";
1818

@@ -35,10 +35,11 @@ export function resolveFollowupDeliveryPayloads(params: {
3535
sentTargets?: MessagingToolSend[];
3636
sentTexts?: string[];
3737
}): ReplyPayload[] {
38-
const replyToChannel = resolveOriginMessageProvider({
38+
const replyMessageProvider = resolveOriginMessageProvider({
3939
originatingChannel: params.originatingChannel,
4040
provider: params.messageProvider,
41-
}) as OriginatingChannelType | undefined;
41+
});
42+
const replyToChannel = replyMessageProvider as OriginatingChannelType | undefined;
4243
const replyToMode = resolveReplyToMode(
4344
params.cfg,
4445
replyToChannel,
@@ -62,16 +63,8 @@ export function resolveFollowupDeliveryPayloads(params: {
6263
replyToMode,
6364
replyToChannel,
6465
});
65-
const dedupedPayloads = filterMessagingToolDuplicates({
66-
payloads: replyTaggedPayloads,
67-
sentTexts: params.sentTexts ?? [],
68-
});
69-
const mediaFilteredPayloads = filterMessagingToolMediaDuplicates({
70-
payloads: dedupedPayloads,
71-
sentMediaUrls: params.sentMediaUrls ?? [],
72-
});
73-
const suppressMessagingToolReplies = shouldSuppressMessagingToolReplies({
74-
messageProvider: replyToChannel,
66+
const messagingToolPayloadDedupe = resolveMessagingToolPayloadDedupe({
67+
messageProvider: replyMessageProvider,
7568
messagingToolSentTargets: params.sentTargets,
7669
originatingTo: resolveOriginMessageTo({
7770
originatingTo: params.originatingTo,
@@ -80,5 +73,17 @@ export function resolveFollowupDeliveryPayloads(params: {
8073
originatingAccountId: params.originatingAccountId,
8174
}),
8275
});
83-
return suppressMessagingToolReplies ? [] : mediaFilteredPayloads;
76+
const mediaFilteredPayloads = messagingToolPayloadDedupe.shouldDedupePayloads
77+
? filterMessagingToolMediaDuplicates({
78+
payloads: replyTaggedPayloads,
79+
sentMediaUrls: params.sentMediaUrls ?? [],
80+
})
81+
: replyTaggedPayloads;
82+
const dedupedPayloads = messagingToolPayloadDedupe.shouldDedupePayloads
83+
? filterMessagingToolDuplicates({
84+
payloads: mediaFilteredPayloads,
85+
sentTexts: params.sentTexts ?? [],
86+
})
87+
: mediaFilteredPayloads;
88+
return messagingToolPayloadDedupe.suppressReplies ? [] : dedupedPayloads;
8489
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
export {
22
filterMessagingToolDuplicates,
33
filterMessagingToolMediaDuplicates,
4+
resolveMessagingToolPayloadDedupe,
45
shouldSuppressMessagingToolReplies,
6+
type MessagingToolPayloadDedupeDecision,
57
} from "./reply-payloads-dedupe.js";

src/auto-reply/reply/reply-payloads-dedupe.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,3 +204,28 @@ export function shouldSuppressMessagingToolReplies(params: {
204204
});
205205
});
206206
}
207+
208+
export type MessagingToolPayloadDedupeDecision = {
209+
shouldDedupePayloads: boolean;
210+
suppressReplies: boolean;
211+
};
212+
213+
export function resolveMessagingToolPayloadDedupe(params: {
214+
messageProvider?: string;
215+
messagingToolSentTargets?: MessagingToolSend[];
216+
originatingTo?: string;
217+
accountId?: string;
218+
}): MessagingToolPayloadDedupeDecision {
219+
const sentTargets = params.messagingToolSentTargets ?? [];
220+
const suppressReplies = shouldSuppressMessagingToolReplies({
221+
messageProvider: params.messageProvider,
222+
messagingToolSentTargets: sentTargets,
223+
originatingTo: params.originatingTo,
224+
accountId: params.accountId,
225+
});
226+
227+
return {
228+
shouldDedupePayloads: suppressReplies || sentTargets.length === 0,
229+
suppressReplies,
230+
};
231+
}

src/auto-reply/reply/reply-payloads.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "../../p
33
import { createOutboundTestPlugin, createTestRegistry } from "../../test-utils/channel-plugins.js";
44
import {
55
filterMessagingToolMediaDuplicates,
6+
resolveMessagingToolPayloadDedupe,
67
shouldSuppressMessagingToolReplies,
78
} from "./reply-payloads.js";
89

@@ -244,3 +245,43 @@ describe("shouldSuppressMessagingToolReplies", () => {
244245
).toBe(true);
245246
});
246247
});
248+
249+
describe("resolveMessagingToolPayloadDedupe", () => {
250+
it("dedupes by content when messaging tool target metadata is unavailable", () => {
251+
expect(
252+
resolveMessagingToolPayloadDedupe({
253+
messageProvider: "telegram",
254+
originatingTo: "123",
255+
}),
256+
).toEqual({
257+
shouldDedupePayloads: true,
258+
suppressReplies: false,
259+
});
260+
});
261+
262+
it("suppresses final replies when a messaging tool sent to the same route", () => {
263+
expect(
264+
resolveMessagingToolPayloadDedupe({
265+
messageProvider: "telegram",
266+
originatingTo: "123",
267+
messagingToolSentTargets: [{ tool: "message", provider: "telegram", to: "123" }],
268+
}),
269+
).toEqual({
270+
shouldDedupePayloads: true,
271+
suppressReplies: true,
272+
});
273+
});
274+
275+
it("keeps final payloads intact when a messaging tool sent to another route", () => {
276+
expect(
277+
resolveMessagingToolPayloadDedupe({
278+
messageProvider: "telegram",
279+
originatingTo: "123",
280+
messagingToolSentTargets: [{ tool: "slack", provider: "slack", to: "channel:C1" }],
281+
}),
282+
).toEqual({
283+
shouldDedupePayloads: false,
284+
suppressReplies: false,
285+
});
286+
});
287+
});

src/auto-reply/reply/reply-payloads.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,7 @@ export {
88
export {
99
filterMessagingToolDuplicates,
1010
filterMessagingToolMediaDuplicates,
11+
resolveMessagingToolPayloadDedupe,
1112
shouldSuppressMessagingToolReplies,
13+
type MessagingToolPayloadDedupeDecision,
1214
} from "./reply-payloads-dedupe.js";

0 commit comments

Comments
 (0)