fix: deliver cron output to explicit targets (#16360) (thanks @rubyrunsstuff)

This commit is contained in:
Shadow
2026-02-14 12:39:31 -06:00
committed by Shadow
parent d14be8472e
commit a73ccf2b53
4 changed files with 34 additions and 28 deletions

View File

@@ -42,6 +42,7 @@ Docs: https://docs.openclaw.ai
- Security/BlueBubbles: reject ambiguous shared-path webhook routing when multiple webhook targets match the same guid/password.
- Security/BlueBubbles: require explicit `mediaLocalRoots` allowlists for local outbound media path reads to prevent local file disclosure. (#16322) Thanks @mbelinky.
- Cron/Slack: preserve agent identity (name and icon) when cron jobs deliver outbound messages. (#16242) Thanks @robbyczgw-cla.
- Cron: deliver text-only output directly when `delivery.to` is set so cron recipients get full output instead of summaries. (#16360) Thanks @rubyrunsstuff.
- Cron: repair missing/corrupt `nextRunAtMs` for the updated job without globally recomputing unrelated due jobs during `cron update`. (#15750)
- Discord: prefer gateway guild id when logging inbound messages so cached-miss guilds do not appear as `guild=dm`. Thanks @thewilloftheshadow.
- TUI: refactor searchable select list description layout and add regression coverage for ANSI-highlight width bounds.

View File

@@ -46,8 +46,9 @@ async function writeSessionStore(home: string) {
"agent:main:main": {
sessionId: "main-session",
updatedAt: Date.now(),
lastProvider: "webchat",
lastTo: "",
lastProvider: "telegram",
lastChannel: "telegram",
lastTo: "123",
},
},
null,
@@ -184,7 +185,7 @@ describe("runCronIsolatedAgentTurn", () => {
kind: "agentTurn",
message: "do it",
}),
delivery: { mode: "announce", channel: "telegram", to: "123" },
delivery: { mode: "announce", channel: "last" },
},
message: "do it",
sessionKey: "cron:job-1",
@@ -210,7 +211,7 @@ describe("runCronIsolatedAgentTurn", () => {
message: "do it",
}),
deleteAfterRun: true,
delivery: { mode: "announce", channel: "telegram", to: "123" },
delivery: { mode: "announce", channel: "last" },
},
message: "do it",
sessionKey: "cron:job-1",

View File

@@ -102,7 +102,7 @@ describe("runCronIsolatedAgentTurn", () => {
);
});
it("announces via shared subagent flow when delivery is requested", async () => {
it("delivers directly when delivery has an explicit target", async () => {
await withTempHome(async (home) => {
const storePath = await writeSessionStore(home);
const deps: CliDeps = {
@@ -136,16 +136,15 @@ describe("runCronIsolatedAgentTurn", () => {
expect(res.status).toBe("ok");
expect(res.delivered).toBe(true);
expect(runSubagentAnnounceFlow).toHaveBeenCalledTimes(1);
const announceArgs = vi.mocked(runSubagentAnnounceFlow).mock.calls[0]?.[0] as
| { announceType?: string }
| undefined;
expect(announceArgs?.announceType).toBe("cron job");
expect(deps.sendMessageTelegram).not.toHaveBeenCalled();
expect(runSubagentAnnounceFlow).not.toHaveBeenCalled();
expect(deps.sendMessageTelegram).toHaveBeenCalledTimes(1);
const [to, text] = vi.mocked(deps.sendMessageTelegram).mock.calls[0] ?? [];
expect(to).toBe("123");
expect(text).toBe("hello from cron");
});
});
it("passes final payload text into shared subagent announce flow", async () => {
it("delivers the final payload text when delivery has an explicit target", async () => {
await withTempHome(async (home) => {
const storePath = await writeSessionStore(home);
const deps: CliDeps = {
@@ -178,12 +177,12 @@ describe("runCronIsolatedAgentTurn", () => {
});
expect(res.status).toBe("ok");
expect(runSubagentAnnounceFlow).toHaveBeenCalledTimes(1);
const announceArgs = vi.mocked(runSubagentAnnounceFlow).mock.calls[0]?.[0] as
| { roundOneReply?: string; requesterOrigin?: { threadId?: string | number } }
| undefined;
expect(announceArgs?.roundOneReply).toBe("Final weather summary");
expect(announceArgs?.requesterOrigin?.threadId).toBeUndefined();
expect(res.delivered).toBe(true);
expect(runSubagentAnnounceFlow).not.toHaveBeenCalled();
expect(deps.sendMessageTelegram).toHaveBeenCalledTimes(1);
const [to, text] = vi.mocked(deps.sendMessageTelegram).mock.calls[0] ?? [];
expect(to).toBe("123");
expect(text).toBe("Final weather summary");
});
});
@@ -237,6 +236,7 @@ describe("runCronIsolatedAgentTurn", () => {
});
expect(res.status).toBe("ok");
expect(runSubagentAnnounceFlow).toHaveBeenCalledTimes(1);
const announceArgs = vi.mocked(runSubagentAnnounceFlow).mock.calls[0]?.[0] as
| { requesterOrigin?: { threadId?: string | number; channel?: string; to?: string } }
| undefined;
@@ -369,7 +369,7 @@ describe("runCronIsolatedAgentTurn", () => {
});
});
it("fails when shared announce flow fails and best-effort is disabled", async () => {
it("fails when direct delivery fails and best-effort is disabled", async () => {
await withTempHome(async (home) => {
const storePath = await writeSessionStore(home);
const deps: CliDeps = {
@@ -386,7 +386,6 @@ describe("runCronIsolatedAgentTurn", () => {
agentMeta: { sessionId: "s", provider: "p", model: "m" },
},
});
vi.mocked(runSubagentAnnounceFlow).mockResolvedValue(false);
const res = await runCronIsolatedAgentTurn({
cfg: makeCfg(home, storePath, {
channels: { telegram: { botToken: "t-1" } },
@@ -402,11 +401,13 @@ describe("runCronIsolatedAgentTurn", () => {
});
expect(res.status).toBe("error");
expect(res.error).toBe("cron announce delivery failed");
expect(res.error).toContain("boom");
expect(runSubagentAnnounceFlow).not.toHaveBeenCalled();
expect(deps.sendMessageTelegram).toHaveBeenCalledTimes(1);
});
});
it("ignores shared announce flow failures when best-effort is enabled", async () => {
it("ignores direct delivery failures when best-effort is enabled", async () => {
await withTempHome(async (home) => {
const storePath = await writeSessionStore(home);
const deps: CliDeps = {
@@ -423,7 +424,6 @@ describe("runCronIsolatedAgentTurn", () => {
agentMeta: { sessionId: "s", provider: "p", model: "m" },
},
});
vi.mocked(runSubagentAnnounceFlow).mockResolvedValue(false);
const res = await runCronIsolatedAgentTurn({
cfg: makeCfg(home, storePath, {
channels: { telegram: { botToken: "t-1" } },
@@ -444,8 +444,9 @@ describe("runCronIsolatedAgentTurn", () => {
});
expect(res.status).toBe("ok");
expect(runSubagentAnnounceFlow).toHaveBeenCalledTimes(1);
expect(deps.sendMessageTelegram).not.toHaveBeenCalled();
expect(res.delivered).toBe(false);
expect(runSubagentAnnounceFlow).not.toHaveBeenCalled();
expect(deps.sendMessageTelegram).toHaveBeenCalledTimes(1);
});
});
});

View File

@@ -558,9 +558,12 @@ export async function runCronIsolatedAgentTurn(params: {
}
const identity = resolveAgentOutboundIdentity(cfgWithAgentDefaults, agentId);
// Shared subagent announce flow is text-based. When we have an explicit sender
// identity to preserve, prefer direct outbound delivery even for plain-text payloads.
if (deliveryPayloadHasStructuredContent || identity) {
// Shared subagent announce flow is text-based and prompts the main agent to
// summarize. When we have an explicit delivery target (delivery.to), sender
// identity, or structured content, prefer direct outbound delivery to send
// the actual cron output without summarization.
const hasExplicitDeliveryTarget = Boolean(deliveryPlan.to);
if (deliveryPayloadHasStructuredContent || identity || hasExplicitDeliveryTarget) {
try {
const payloadsForDelivery =
deliveryPayloadHasStructuredContent && deliveryPayloads.length > 0