fix(agents): bypass pendingDescendantRuns guard for cron announce delivery (#35185)

* fix(agents): bypass pendingDescendantRuns guard for cron announce delivery

Standalone cron job completions were blocked from direct channel delivery
when the cron run had spawned subagents that were still registered as
pending. The pendingDescendantRuns guard exists for live orchestration
coordination and should not apply to fire-and-forget cron announce sends.

Thread the announceType through the delivery chain and skip both the
child-descendant and requester-descendant pending-run guards when the
announce originates from a cron job.

Closes #34966

* fix: ensure outbound session entry for cron announce with named agents (#32432)

Named agents may not have a session entry for their delivery target,
causing the announce flow to silently fail (delivered=false, no error).

Two fixes:
1. Call ensureOutboundSessionEntry when resolving the cron announce
   session key so downstream delivery can find channel metadata.
2. Fall back to direct outbound delivery when announce delivery fails
   to ensure cron output reaches the target channel.

Closes #32432

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: guard announce direct-delivery fallback against suppression leaks (#32432)

The `!delivered` fallback condition was too broad — it caught intentional
suppressions (active subagents, interim messages, SILENT_REPLY_TOKEN) in
addition to actual announce delivery failures.  Add an
`announceDeliveryWasAttempted` flag so the direct-delivery fallback only
fires when `runSubagentAnnounceFlow` was actually called and failed.

Also remove the redundant `if (route)` guard in
`resolveCronAnnounceSessionKey` since `resolved` being truthy guarantees
`route` is non-null.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(cron): harden announce synthesis follow-ups

---------

Co-authored-by: scoootscooob <zhentongfan@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
Sid
2026-03-05 11:31:33 +08:00
committed by GitHub
parent 68e68bfb57
commit 8b8167d547
6 changed files with 222 additions and 16 deletions

View File

@@ -469,6 +469,53 @@ describe("subagent announce formatting", () => {
expect(agentSpy).not.toHaveBeenCalled();
});
it("keeps cron completion direct delivery even when sibling runs are still active", async () => {
sessionStore = {
"agent:main:subagent:test": {
sessionId: "child-session-cron-direct",
},
"agent:main:main": {
sessionId: "requester-session-cron-direct",
},
};
readLatestAssistantReplyMock.mockResolvedValue("");
chatHistoryMock.mockResolvedValueOnce({
messages: [{ role: "assistant", content: [{ type: "text", text: "final answer: cron" }] }],
});
subagentRegistryMock.countActiveDescendantRuns.mockImplementation((sessionKey: string) =>
sessionKey === "agent:main:main" ? 1 : 0,
);
subagentRegistryMock.countPendingDescendantRuns.mockImplementation((sessionKey: string) =>
sessionKey === "agent:main:main" ? 1 : 0,
);
subagentRegistryMock.countPendingDescendantRunsExcludingRun.mockImplementation(
(sessionKey: string, runId: string) =>
sessionKey === "agent:main:main" && runId === "run-direct-cron-active-siblings" ? 1 : 0,
);
const didAnnounce = await runSubagentAnnounceFlow({
childSessionKey: "agent:main:subagent:test",
childRunId: "run-direct-cron-active-siblings",
requesterSessionKey: "agent:main:main",
requesterDisplayKey: "main",
requesterOrigin: { channel: "discord", to: "channel:12345", accountId: "acct-1" },
announceType: "cron job",
...defaultOutcomeAnnounce,
expectsCompletionMessage: true,
});
expect(didAnnounce).toBe(true);
expect(sendSpy).toHaveBeenCalledTimes(1);
expect(agentSpy).not.toHaveBeenCalled();
const call = sendSpy.mock.calls[0]?.[0] as { params?: Record<string, unknown> };
const rawMessage = call?.params?.message;
const msg = typeof rawMessage === "string" ? rawMessage : "";
expect(call?.params?.channel).toBe("discord");
expect(call?.params?.to).toBe("channel:12345");
expect(msg).toContain("final answer: cron");
expect(msg).not.toContain("There are still 1 active subagent run for this session.");
});
it("suppresses completion delivery when subagent reply is ANNOUNCE_SKIP", async () => {
const didAnnounce = await runSubagentAnnounceFlow({
childSessionKey: "agent:main:subagent:test",

View File

@@ -736,6 +736,7 @@ async function sendSubagentAnnounceDirectly(params: {
bestEffortDeliver?: boolean;
completionRouteMode?: "bound" | "fallback" | "hook";
spawnMode?: SpawnSubagentMode;
announceType?: SubagentAnnounceType;
directIdempotencyKey: string;
currentRunId?: string;
completionDirectOrigin?: DeliveryContext;
@@ -778,8 +779,9 @@ async function sendSubagentAnnounceDirectly(params: {
const forceBoundSessionDirectDelivery =
params.spawnMode === "session" &&
(params.completionRouteMode === "bound" || params.completionRouteMode === "hook");
const forceCronDirectDelivery = params.announceType === "cron job";
let shouldSendCompletionDirectly = true;
if (!forceBoundSessionDirectDelivery) {
if (!forceBoundSessionDirectDelivery && !forceCronDirectDelivery) {
let pendingDescendantRuns = 0;
try {
const { countPendingDescendantRuns, countPendingDescendantRunsExcludingRun } =
@@ -919,6 +921,7 @@ async function deliverSubagentAnnouncement(params: {
bestEffortDeliver?: boolean;
completionRouteMode?: "bound" | "fallback" | "hook";
spawnMode?: SpawnSubagentMode;
announceType?: SubagentAnnounceType;
directIdempotencyKey: string;
currentRunId?: string;
signal?: AbortSignal;
@@ -948,6 +951,7 @@ async function deliverSubagentAnnouncement(params: {
completionDirectOrigin: params.completionDirectOrigin,
completionRouteMode: params.completionRouteMode,
spawnMode: params.spawnMode,
announceType: params.announceType,
directOrigin: params.directOrigin,
requesterIsSubagent: params.requesterIsSubagent,
expectsCompletionMessage: params.expectsCompletionMessage,
@@ -1233,7 +1237,8 @@ export async function runSubagentAnnounceFlow(params: {
} catch {
// Best-effort only; fall back to direct announce behavior when unavailable.
}
if (pendingChildDescendantRuns > 0) {
const isCronAnnounce = params.announceType === "cron job";
if (pendingChildDescendantRuns > 0 && !isCronAnnounce) {
// The finished run still has pending descendant subagents (either active,
// or ended but still finishing their own announce and cleanup flow). Defer
// announcing this run until descendants fully settle.
@@ -1406,6 +1411,7 @@ export async function runSubagentAnnounceFlow(params: {
bestEffortDeliver: params.bestEffortDeliver,
completionRouteMode: completionResolution.routeMode,
spawnMode: params.spawnMode,
announceType,
directIdempotencyKey,
currentRunId: params.childRunId,
signal: params.signal,