From 7fc4bbc0bcbabc2aa99b1fd51e77099f2b26f4e1 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Tue, 23 Jun 2026 02:52:34 +0200 Subject: [PATCH] fix(agents): wake active parents for subagent completions --- src/agents/subagent-announce-delivery.test.ts | 59 ++++++++++++++++++- src/agents/subagent-announce-delivery.ts | 12 ++++ 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/src/agents/subagent-announce-delivery.test.ts b/src/agents/subagent-announce-delivery.test.ts index 57741517689..ac902e4ae5e 100644 --- a/src/agents/subagent-announce-delivery.test.ts +++ b/src/agents/subagent-announce-delivery.test.ts @@ -229,6 +229,8 @@ async function deliverDiscordDirectMessageCompletion(params: { callGateway: typeof runtimeCallGateway; sendMessage?: typeof runtimeSendMessage; internalEvents?: AgentInternalEvent[]; + isActive?: boolean; + queueEmbeddedAgentMessageWithOutcome?: QueueEmbeddedAgentMessageWithOutcome; sourceTool?: string; }) { const origin = { @@ -240,10 +242,13 @@ async function deliverDiscordDirectMessageCompletion(params: { callGateway: params.callGateway, getRequesterSessionActivity: () => ({ sessionId: "requester-session-dm", - isActive: false, + isActive: params.isActive === true, }), getRuntimeConfig: () => ({}) as never, sendMessage: params.sendMessage ?? runtimeSendMessage, + ...(params.queueEmbeddedAgentMessageWithOutcome + ? { queueEmbeddedAgentMessageWithOutcome: params.queueEmbeddedAgentMessageWithOutcome } + : {}), }); return deliverSubagentAnnouncement({ @@ -4786,6 +4791,58 @@ describe("deliverSubagentAnnouncement completion delivery", () => { expect(sendMessage).not.toHaveBeenCalled(); }); + it("retries active direct subagent completion wake without forced message-tool mode", async () => { + const callGateway = createGatewayMock({ + result: { + payloads: [{ text: "The subagent is done: child completion output" }], + didSendViaMessagingTool: true, + }, + }); + const queueEmbeddedAgentMessageWithOutcome = createQueueOutcomeSequenceMock([ + "source_reply_delivery_mode_mismatch", + true, + ]); + + const result = await deliverDiscordDirectMessageCompletion({ + callGateway, + isActive: true, + queueEmbeddedAgentMessageWithOutcome, + sourceTool: "subagent_announce", + internalEvents: [ + { + type: "task_completion", + source: "subagent", + childSessionKey: "agent:worker:subagent:child", + childSessionId: "child-session-id", + announceType: "subagent task", + taskLabel: "direct completion active wake", + status: "ok", + statusLabel: "completed successfully", + result: "child completion output", + replyInstruction: "Summarize the result.", + }, + ], + }); + + expectRecordFields(result, { + delivered: true, + path: "steered", + }); + expect(queueEmbeddedAgentMessageWithOutcome).toHaveBeenCalledTimes(2); + expectRecordFields(mockCallArg(queueEmbeddedAgentMessageWithOutcome, 0, 2), { + sourceReplyDeliveryMode: "message_tool_only", + waitForTranscriptCommit: true, + }); + const retryOptions = mockCallArg(queueEmbeddedAgentMessageWithOutcome, 1, 2); + expectRecordFields(retryOptions, { + waitForTranscriptCommit: true, + }); + expect( + (retryOptions as { sourceReplyDeliveryMode?: unknown }).sourceReplyDeliveryMode, + ).toBeUndefined(); + expect(callGateway).not.toHaveBeenCalled(); + }); + it("falls back to the external requester route when completion origin is internal", async () => { const callGateway = createGatewayMock({ result: { diff --git a/src/agents/subagent-announce-delivery.ts b/src/agents/subagent-announce-delivery.ts index 7af7957cc22..42b4312d489 100644 --- a/src/agents/subagent-announce-delivery.ts +++ b/src/agents/subagent-announce-delivery.ts @@ -285,6 +285,18 @@ async function resolveActiveWakeWithRetries( outcome = await resolveQueueEmbeddedAgentMessageOutcome(sessionId, message, currentOptions); continue; } + if ( + outcome.reason === "source_reply_delivery_mode_mismatch" && + currentOptions.sourceReplyDeliveryMode !== undefined + ) { + // Active requester runs own their final delivery mode. Direct-completion + // policy must not make an already-running automatic parent unreachable. + const activeRunOptions = { ...currentOptions }; + delete activeRunOptions.sourceReplyDeliveryMode; + currentOptions = activeRunOptions; + outcome = await resolveQueueEmbeddedAgentMessageOutcome(sessionId, message, currentOptions); + continue; + } if (outcome.reason === "compacting") { const remainingDeliveryTimeoutMs = compactionDeadlineMs === undefined ? undefined : compactionDeadlineMs - Date.now();