mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
fix(cron): resolve #35185 bot review findings
This commit is contained in:
@@ -516,6 +516,40 @@ describe("subagent announce formatting", () => {
|
|||||||
expect(msg).not.toContain("There are still 1 active subagent run for this session.");
|
expect(msg).not.toContain("There are still 1 active subagent run for this session.");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps cron child session when descendants are still pending", async () => {
|
||||||
|
sessionStore = {
|
||||||
|
"agent:main:subagent:test": {
|
||||||
|
sessionId: "child-session-cron-pending-descendants",
|
||||||
|
},
|
||||||
|
"agent:main:main": {
|
||||||
|
sessionId: "requester-session-cron-pending-descendants",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
readLatestAssistantReplyMock.mockResolvedValue("");
|
||||||
|
chatHistoryMock.mockResolvedValueOnce({
|
||||||
|
messages: [{ role: "assistant", content: [{ type: "text", text: "final answer: cron" }] }],
|
||||||
|
});
|
||||||
|
subagentRegistryMock.countPendingDescendantRuns.mockImplementation((sessionKey: string) =>
|
||||||
|
sessionKey === "agent:main:subagent:test" ? 1 : 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
const didAnnounce = await runSubagentAnnounceFlow({
|
||||||
|
childSessionKey: "agent:main:subagent:test",
|
||||||
|
childRunId: "run-direct-cron-pending-descendants",
|
||||||
|
requesterSessionKey: "agent:main:main",
|
||||||
|
requesterDisplayKey: "main",
|
||||||
|
requesterOrigin: { channel: "discord", to: "channel:12345", accountId: "acct-1" },
|
||||||
|
announceType: "cron job",
|
||||||
|
...defaultOutcomeAnnounce,
|
||||||
|
cleanup: "delete",
|
||||||
|
expectsCompletionMessage: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(didAnnounce).toBe(true);
|
||||||
|
expect(sendSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(sessionsDeleteSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it("suppresses completion delivery when subagent reply is ANNOUNCE_SKIP", async () => {
|
it("suppresses completion delivery when subagent reply is ANNOUNCE_SKIP", async () => {
|
||||||
const didAnnounce = await runSubagentAnnounceFlow({
|
const didAnnounce = await runSubagentAnnounceFlow({
|
||||||
childSessionKey: "agent:main:subagent:test",
|
childSessionKey: "agent:main:subagent:test",
|
||||||
|
|||||||
@@ -1238,12 +1238,14 @@ export async function runSubagentAnnounceFlow(params: {
|
|||||||
// Best-effort only; fall back to direct announce behavior when unavailable.
|
// Best-effort only; fall back to direct announce behavior when unavailable.
|
||||||
}
|
}
|
||||||
const isCronAnnounce = params.announceType === "cron job";
|
const isCronAnnounce = params.announceType === "cron job";
|
||||||
if (pendingChildDescendantRuns > 0 && !isCronAnnounce) {
|
if (pendingChildDescendantRuns > 0) {
|
||||||
// The finished run still has pending descendant subagents (either active,
|
// Descendants are still pending cleanup/announce work. Keep the child
|
||||||
// or ended but still finishing their own announce and cleanup flow). Defer
|
// session alive so descendant routing and retry paths are not orphaned.
|
||||||
// announcing this run until descendants fully settle.
|
|
||||||
shouldDeleteChildSession = false;
|
shouldDeleteChildSession = false;
|
||||||
return false;
|
if (!isCronAnnounce) {
|
||||||
|
// Non-cron announce flows still defer while descendants settle.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (requesterDepth >= 1 && reply?.trim()) {
|
if (requesterDepth >= 1 && reply?.trim()) {
|
||||||
|
|||||||
@@ -421,6 +421,38 @@ describe("runCronIsolatedAgentTurn", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps cron run status ok when announce and direct fallback both fail", async () => {
|
||||||
|
await withTelegramAnnounceFixture(
|
||||||
|
async ({ home, storePath, deps }) => {
|
||||||
|
mockAgentPayloads([{ text: "hello from cron" }]);
|
||||||
|
vi.mocked(runSubagentAnnounceFlow).mockResolvedValueOnce(false);
|
||||||
|
|
||||||
|
const res = await runTelegramAnnounceTurn({
|
||||||
|
home,
|
||||||
|
storePath,
|
||||||
|
deps,
|
||||||
|
delivery: {
|
||||||
|
mode: "announce",
|
||||||
|
channel: "telegram",
|
||||||
|
to: "123",
|
||||||
|
bestEffort: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).toBe("ok");
|
||||||
|
expect(res.delivered).toBe(false);
|
||||||
|
expect(res.deliveryAttempted).toBe(true);
|
||||||
|
expect(res.error).toContain("cron announce delivery failed");
|
||||||
|
expect(deps.sendMessageTelegram).toHaveBeenCalledTimes(1);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deps: {
|
||||||
|
sendMessageTelegram: vi.fn().mockRejectedValue(new Error("direct fallback failed")),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("marks attempted when announce delivery reports false and best-effort is enabled", async () => {
|
it("marks attempted when announce delivery reports false and best-effort is enabled", async () => {
|
||||||
const { res, deps } = await runAnnounceFlowResult(true);
|
const { res, deps } = await runAnnounceFlowResult(true);
|
||||||
expect(res.status).toBe("ok");
|
expect(res.status).toBe("ok");
|
||||||
|
|||||||
@@ -475,15 +475,14 @@ export async function dispatchCronDelivery(
|
|||||||
if (announceDeliveryWasAttempted && !delivered && !params.isAborted()) {
|
if (announceDeliveryWasAttempted && !delivered && !params.isAborted()) {
|
||||||
const directFallback = await deliverViaDirect(params.resolvedDelivery);
|
const directFallback = await deliverViaDirect(params.resolvedDelivery);
|
||||||
if (directFallback) {
|
if (directFallback) {
|
||||||
return {
|
// Preserve announce-mode semantics: announce failure should not
|
||||||
result: directFallback,
|
// downgrade an otherwise successful cron execution to hard error.
|
||||||
delivered,
|
// Keep announceResult and only use direct fallback on success.
|
||||||
deliveryAttempted,
|
logWarn(
|
||||||
summary,
|
`[cron:${params.job.id}] direct fallback after announce failure also failed: ${
|
||||||
outputText,
|
directFallback.error ?? "unknown error"
|
||||||
synthesizedText,
|
}`,
|
||||||
deliveryPayloads,
|
);
|
||||||
};
|
|
||||||
}
|
}
|
||||||
// If direct delivery succeeded (returned null without error),
|
// If direct delivery succeeded (returned null without error),
|
||||||
// `delivered` has been set to true by deliverViaDirect.
|
// `delivered` has been set to true by deliverViaDirect.
|
||||||
|
|||||||
Reference in New Issue
Block a user