mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 17:24:58 +00:00
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:
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user