diff --git a/CHANGELOG.md b/CHANGELOG.md index c4faaf01b..d99bbb80e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/src/cron/isolated-agent.delivers-response-has-heartbeat-ok-but-includes.test.ts b/src/cron/isolated-agent.delivers-response-has-heartbeat-ok-but-includes.test.ts index b7e2b3c6d..92d191687 100644 --- a/src/cron/isolated-agent.delivers-response-has-heartbeat-ok-but-includes.test.ts +++ b/src/cron/isolated-agent.delivers-response-has-heartbeat-ok-but-includes.test.ts @@ -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", diff --git a/src/cron/isolated-agent.skips-delivery-without-whatsapp-recipient-besteffortdeliver-true.e2e.test.ts b/src/cron/isolated-agent.skips-delivery-without-whatsapp-recipient-besteffortdeliver-true.e2e.test.ts index 94bfd4f27..6eef3fa3e 100644 --- a/src/cron/isolated-agent.skips-delivery-without-whatsapp-recipient-besteffortdeliver-true.e2e.test.ts +++ b/src/cron/isolated-agent.skips-delivery-without-whatsapp-recipient-besteffortdeliver-true.e2e.test.ts @@ -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); }); }); }); diff --git a/src/cron/isolated-agent/run.ts b/src/cron/isolated-agent/run.ts index 9cd052879..ff839e185 100644 --- a/src/cron/isolated-agent/run.ts +++ b/src/cron/isolated-agent/run.ts @@ -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