Skip to content

Commit ef5a2f8

Browse files
committed
fix(subagents): require completion ack after fallback steer
1 parent 98c9e4f commit ef5a2f8

2 files changed

Lines changed: 65 additions & 0 deletions

File tree

src/agents/subagent-announce-delivery.test.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1987,6 +1987,66 @@ describe("deliverSubagentAnnouncement completion delivery", () => {
19871987
expect(sendMessage).not.toHaveBeenCalled();
19881988
});
19891989

1990+
it("requires requester assistant acknowledgement when completion direct failure fallback-steers", async () => {
1991+
const callGateway = createGatewayMock({
1992+
result: {
1993+
payloads: [{ text: "Tests passed and the PR is ready for review." }],
1994+
deliveryStatus: {
1995+
status: "failed",
1996+
errorMessage: "Slack send failed: channel not found",
1997+
},
1998+
},
1999+
});
2000+
const sendMessage = createSendMessageMock();
2001+
const queueEmbeddedAgentMessageWithOutcome = createQueueOutcomeSequenceMock([
2002+
"no_active_run",
2003+
true,
2004+
]);
2005+
const result = await deliverSlackThreadAnnouncement({
2006+
callGateway,
2007+
sendMessage,
2008+
queueEmbeddedAgentMessageWithOutcome,
2009+
sessionId: "requester-session-4",
2010+
isActive: true,
2011+
expectsCompletionMessage: true,
2012+
directIdempotencyKey: "announce-thread-delivery-fallback-steer-ack",
2013+
internalEvents: [
2014+
{
2015+
type: "task_completion",
2016+
source: "subagent",
2017+
childSessionKey: "agent:worker:subagent:child",
2018+
childSessionId: "child-session-id",
2019+
announceType: "subagent task",
2020+
taskLabel: "thread completion smoke",
2021+
status: "ok",
2022+
statusLabel: "completed successfully",
2023+
result: "child completion output",
2024+
replyInstruction: "Summarize the result.",
2025+
},
2026+
],
2027+
});
2028+
2029+
expectRecordFields(result, {
2030+
delivered: true,
2031+
path: "steered",
2032+
});
2033+
expect(callGateway).toHaveBeenCalledTimes(1);
2034+
expect(queueEmbeddedAgentMessageWithOutcome).toHaveBeenCalledTimes(2);
2035+
expect(queueEmbeddedAgentMessageWithOutcome).toHaveBeenNthCalledWith(
2036+
2,
2037+
"requester-session-4",
2038+
"child done",
2039+
{
2040+
debounceMs: 500,
2041+
deliveryTimeoutMs: 120_000,
2042+
steeringMode: "all",
2043+
waitForTranscriptCommit: true,
2044+
waitForAssistantResponseAfterTranscriptCommit: true,
2045+
},
2046+
);
2047+
expect(sendMessage).not.toHaveBeenCalled();
2048+
});
2049+
19902050
it("does not raw-send grouped child results when requester-agent output is empty", async () => {
19912051
const callGateway = createGatewayMock({
19922052
result: {

src/agents/subagent-announce-delivery.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -600,6 +600,7 @@ async function maybeSteerSubagentAnnounce(params: {
600600
deliveryTimeoutMs?: number;
601601
requesterSessionKey: string;
602602
steerMessage: string;
603+
expectsCompletionMessage: boolean;
603604
signal?: AbortSignal;
604605
}): Promise<
605606
{ status: "steered"; deliveredAt?: number; enqueuedAt?: number } | { status: "none" | "dropped" }
@@ -630,6 +631,9 @@ async function maybeSteerSubagentAnnounce(params: {
630631
steeringMode: "all",
631632
...(queueSettings.debounceMs !== undefined ? { debounceMs: queueSettings.debounceMs } : {}),
632633
waitForTranscriptCommit: true,
634+
...(params.expectsCompletionMessage
635+
? { waitForAssistantResponseAfterTranscriptCommit: true }
636+
: {}),
633637
};
634638
const queueOutcome = await resolveActiveWakeWithRetries(
635639
sessionId,
@@ -1688,6 +1692,7 @@ export async function deliverSubagentAnnouncement(params: {
16881692
),
16891693
requesterSessionKey: params.requesterSessionKey,
16901694
steerMessage: params.steerMessage,
1695+
expectsCompletionMessage: params.expectsCompletionMessage,
16911696
signal: params.signal,
16921697
}),
16931698
direct: async () =>

0 commit comments

Comments
 (0)