diff --git a/src/auto-reply/reply/agent-runner-payloads.test.ts b/src/auto-reply/reply/agent-runner-payloads.test.ts index 40d0ae72ad3..9b62db984e8 100644 --- a/src/auto-reply/reply/agent-runner-payloads.test.ts +++ b/src/auto-reply/reply/agent-runner-payloads.test.ts @@ -85,4 +85,27 @@ describe("buildReplyPayloads media filter integration", () => { expect(replyPayloads).toHaveLength(0); }); + + it("does not suppress same-target replies when accountId differs", () => { + const { replyPayloads } = buildReplyPayloads({ + ...baseParams, + payloads: [{ text: "hello world!" }], + messageProvider: "heartbeat", + originatingChannel: "telegram", + originatingTo: "268300329", + accountId: "personal", + messagingToolSentTexts: ["different message"], + messagingToolSentTargets: [ + { + tool: "telegram", + provider: "telegram", + to: "268300329", + accountId: "work", + }, + ], + }); + + expect(replyPayloads).toHaveLength(1); + expect(replyPayloads[0]?.text).toBe("hello world!"); + }); }); diff --git a/src/auto-reply/reply/followup-runner.test.ts b/src/auto-reply/reply/followup-runner.test.ts index a77bb0be44e..189267b8d94 100644 --- a/src/auto-reply/reply/followup-runner.test.ts +++ b/src/auto-reply/reply/followup-runner.test.ts @@ -238,6 +238,36 @@ describe("createFollowupRunner messaging tool dedupe", () => { expect(onBlockReply).not.toHaveBeenCalled(); }); + it("does not suppress replies for same target when account differs", async () => { + const onBlockReply = vi.fn(async () => {}); + runEmbeddedPiAgentMock.mockResolvedValueOnce({ + payloads: [{ text: "hello world!" }], + messagingToolSentTexts: ["different message"], + messagingToolSentTargets: [ + { tool: "telegram", provider: "telegram", to: "268300329", accountId: "work" }, + ], + meta: {}, + }); + + const runner = createMessagingDedupeRunner(onBlockReply); + + await runner({ + ...baseQueuedRun("heartbeat"), + originatingChannel: "telegram", + originatingTo: "268300329", + originatingAccountId: "personal", + } as FollowupRun); + + expect(routeReplyMock).toHaveBeenCalledWith( + expect.objectContaining({ + channel: "telegram", + to: "268300329", + accountId: "personal", + }), + ); + expect(onBlockReply).not.toHaveBeenCalled(); + }); + it("drops media URL from payload when messaging tool already sent it", async () => { const onBlockReply = vi.fn(async () => {}); runEmbeddedPiAgentMock.mockResolvedValueOnce({ @@ -335,6 +365,34 @@ describe("createFollowupRunner messaging tool dedupe", () => { expect(routeReplyMock).toHaveBeenCalled(); expect(onBlockReply).not.toHaveBeenCalled(); }); + + it("routes followups with originating account/thread metadata", async () => { + const onBlockReply = vi.fn(async () => {}); + runEmbeddedPiAgentMock.mockResolvedValueOnce({ + payloads: [{ text: "hello world!" }], + meta: {}, + }); + + const runner = createMessagingDedupeRunner(onBlockReply); + + await runner({ + ...baseQueuedRun("webchat"), + originatingChannel: "discord", + originatingTo: "channel:C1", + originatingAccountId: "work", + originatingThreadId: "1739142736.000100", + } as FollowupRun); + + expect(routeReplyMock).toHaveBeenCalledWith( + expect.objectContaining({ + channel: "discord", + to: "channel:C1", + accountId: "work", + threadId: "1739142736.000100", + }), + ); + expect(onBlockReply).not.toHaveBeenCalled(); + }); }); describe("createFollowupRunner agentDir forwarding", () => { diff --git a/src/auto-reply/reply/reply-flow.test.ts b/src/auto-reply/reply/reply-flow.test.ts index 3b91cf52d40..03ff953be7c 100644 --- a/src/auto-reply/reply/reply-flow.test.ts +++ b/src/auto-reply/reply/reply-flow.test.ts @@ -1068,6 +1068,7 @@ describe("followup queue collect routing", () => { prompt: "first", originatingChannel: "discord", originatingTo: "channel:C1", + originatingAccountId: "work", originatingThreadId: "1739142736.000100", }), settings, @@ -1078,6 +1079,7 @@ describe("followup queue collect routing", () => { prompt: "second", originatingChannel: "discord", originatingTo: "channel:C1", + originatingAccountId: "work", originatingThreadId: "1739142736.000100", }), settings, @@ -1088,6 +1090,7 @@ describe("followup queue collect routing", () => { expect(calls[0]?.originatingChannel).toBe("discord"); expect(calls[0]?.originatingTo).toBe("channel:C1"); + expect(calls[0]?.originatingAccountId).toBe("work"); expect(calls[0]?.originatingThreadId).toBe("1739142736.000100"); expect(calls[0]?.prompt).toContain("[Queue overflow] Dropped 1 message due to cap."); }); diff --git a/src/infra/outbound/targets.test.ts b/src/infra/outbound/targets.test.ts index 9d7ec7dde5e..5cc004a4b3a 100644 --- a/src/infra/outbound/targets.test.ts +++ b/src/infra/outbound/targets.test.ts @@ -321,4 +321,39 @@ describe("resolveSessionDeliveryTarget", () => { expect(resolved.to).toBe("user:U123"); expect(resolved.threadId).toBeUndefined(); }); + + it("keeps explicit threadId in heartbeat mode", () => { + const resolved = resolveSessionDeliveryTarget({ + entry: { + sessionId: "sess-heartbeat-explicit-thread", + updatedAt: 1, + lastChannel: "telegram", + lastTo: "-100123", + lastThreadId: 999, + }, + requestedChannel: "last", + mode: "heartbeat", + explicitThreadId: 42, + }); + + expect(resolved.channel).toBe("telegram"); + expect(resolved.to).toBe("-100123"); + expect(resolved.threadId).toBe(42); + expect(resolved.threadIdExplicit).toBe(true); + }); + + it("parses explicit heartbeat topic targets into threadId", () => { + const cfg: OpenClawConfig = {}; + const resolved = resolveHeartbeatDeliveryTarget({ + cfg, + heartbeat: { + target: "telegram", + to: "63448508:topic:1008013", + }, + }); + + expect(resolved.channel).toBe("telegram"); + expect(resolved.to).toBe("63448508"); + expect(resolved.threadId).toBe(1008013); + }); });