diff --git a/src/agents/subagent-announce-delivery.test.ts b/src/agents/subagent-announce-delivery.test.ts index 09f12ac55bc..54dfca750a0 100644 --- a/src/agents/subagent-announce-delivery.test.ts +++ b/src/agents/subagent-announce-delivery.test.ts @@ -76,6 +76,43 @@ async function deliverSlackThreadAnnouncement(params: { }); } +async function deliverDiscordDirectMessageCompletion(params: { + callGateway: typeof runtimeCallGateway; + sendMessage?: typeof runtimeSendMessage; + internalEvents?: AgentInternalEvent[]; +}) { + const origin = { + channel: "discord", + to: "dm:U123", + accountId: "acct-1", + }; + __testing.setDepsForTest({ + callGateway: params.callGateway, + getRequesterSessionActivity: () => ({ + sessionId: "requester-session-dm", + isActive: false, + }), + loadConfig: () => ({}) as never, + ...(params.sendMessage ? { sendMessage: params.sendMessage } : {}), + }); + + return deliverSubagentAnnouncement({ + requesterSessionKey: "agent:main:discord:dm:U123", + targetRequesterSessionKey: "agent:main:discord:dm:U123", + triggerMessage: "child done", + steerMessage: "child done", + requesterOrigin: origin, + requesterSessionOrigin: origin, + completionDirectOrigin: origin, + directOrigin: origin, + requesterIsSubagent: false, + expectsCompletionMessage: true, + bestEffortDeliver: true, + directIdempotencyKey: "announce-dm-fallback-empty", + internalEvents: params.internalEvents, + }); +} + describe("resolveAnnounceOrigin threaded route targets", () => { it("preserves stored thread ids when requester origin omits one for the same chat", () => { expect( @@ -332,6 +369,65 @@ describe("deliverSubagentAnnouncement completion delivery", () => { ); }); + it("uses direct fallback for completion DMs without a thread id when announce-agent returns no visible output", async () => { + const callGateway = createGatewayMock({ + result: { + payloads: [], + }, + }); + const sendMessage = createSendMessageMock(); + const result = await deliverDiscordDirectMessageCompletion({ + callGateway, + sendMessage, + internalEvents: [ + { + type: "task_completion", + source: "music_generation", + childSessionKey: "music_generate:task-123", + childSessionId: "task-123", + announceType: "music generation task", + taskLabel: "night-drive synthwave", + status: "ok", + statusLabel: "completed successfully", + result: "Generated 1 track.\nMEDIA:/tmp/generated-night-drive.mp3", + mediaUrls: ["/tmp/generated-night-drive.mp3"], + replyInstruction: "Deliver the generated music.", + }, + ], + }); + + expect(result).toEqual( + expect.objectContaining({ + delivered: true, + path: "direct-thread-fallback", + }), + ); + expect(callGateway).toHaveBeenCalledWith( + expect.objectContaining({ + method: "agent", + params: expect.objectContaining({ + deliver: true, + channel: "discord", + accountId: "acct-1", + to: "dm:U123", + threadId: undefined, + }), + }), + ); + expect(sendMessage).toHaveBeenCalledWith( + expect.objectContaining({ + channel: "discord", + accountId: "acct-1", + to: "dm:U123", + threadId: undefined, + content: "Generated 1 track.\nMEDIA:/tmp/generated-night-drive.mp3", + requesterSessionKey: "agent:main:discord:dm:U123", + bestEffort: true, + idempotencyKey: "announce-dm-fallback-empty", + }), + ); + }); + it("keeps direct external delivery for non-completion announces", async () => { const callGateway = createGatewayMock(); await deliverSlackThreadAnnouncement({ diff --git a/src/agents/subagent-announce-delivery.ts b/src/agents/subagent-announce-delivery.ts index 8df48e7b7cc..f89a2159af3 100644 --- a/src/agents/subagent-announce-delivery.ts +++ b/src/agents/subagent-announce-delivery.ts @@ -563,7 +563,7 @@ async function sendThreadCompletionFallback(params: { const channel = params.channel?.trim(); const to = params.to?.trim(); const content = params.content.trim(); - if (!channel || !to || !params.threadId || !content) { + if (!channel || !to || !content) { return false; } await runAnnounceDeliveryWithRetry({ @@ -674,7 +674,7 @@ async function sendSubagentAnnounceDirectly(params: { }; } const threadCompletionFallbackText = - params.expectsCompletionMessage && deliveryTarget.deliver && deliveryTarget.threadId + params.expectsCompletionMessage && deliveryTarget.deliver ? extractThreadCompletionFallbackText(params.internalEvents) : ""; let directAnnounceResponse: unknown;