fix(agents): wake active parents for subagent completions

This commit is contained in:
Vincent Koc
2026-06-23 02:52:34 +02:00
parent d716dfd532
commit 7fc4bbc0bc
2 changed files with 70 additions and 1 deletions

View File

@@ -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: {

View File

@@ -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();