cron: skip announce delivery for NO_REPLY

This commit is contained in:
ShionElia 2026-03-20 08:47:55 +00:00
parent df536c3248
commit ec1d653620
4 changed files with 26 additions and 4 deletions

View File

@ -212,6 +212,7 @@ Behavior details:
- Content: delivery uses the isolated run's outbound payloads (text/media) with normal chunking and
channel formatting.
- Heartbeat-only responses (`HEARTBEAT_OK` with no real content) are not delivered.
- Exact silent responses (`NO_REPLY`, after trimming) are not delivered.
- If the isolated run already sent a message to the same target via the message tool, delivery is
skipped to avoid duplicates.
- Missing or invalid delivery targets fail the job unless `delivery.bestEffort = true`.

View File

@ -18,6 +18,8 @@ Tip: run `openclaw cron --help` for the full command surface.
Note: isolated `cron add` jobs default to `--announce` delivery. Use `--no-deliver` to keep
output internal. `--deliver` remains as a deprecated alias for `--announce`.
When an announced isolated run replies with exact `NO_REPLY` (after trimming), OpenClaw suppresses
the outbound delivery.
Note: one-shot (`--at`) jobs delete after success by default. Use `--keep-after-run` to keep them.

View File

@ -407,6 +407,24 @@ describe("runCronIsolatedAgentTurn", () => {
});
});
it("skips announce when the agent reply is whitespace-padded NO_REPLY", async () => {
await withTelegramAnnounceFixture(async ({ home, storePath, deps }) => {
mockAgentPayloads([{ text: " NO_REPLY \n" }]);
const res = await runTelegramAnnounceTurn({
home,
storePath,
deps,
delivery: { mode: "announce", channel: "telegram", to: "123" },
});
expect(res.status).toBe("ok");
expect(res.delivered).toBe(false);
expect(res.deliveryAttempted).toBe(true);
expect(runSubagentAnnounceFlow).not.toHaveBeenCalled();
expect(deps.sendMessageTelegram).not.toHaveBeenCalled();
});
});
it("fails when structured direct delivery fails and best-effort is disabled", async () => {
await expectStructuredTelegramFailure({
payload: { text: "hello from cron", mediaUrl: "https://example.com/img.png" },

View File

@ -1,5 +1,5 @@
import { countActiveDescendantRuns } from "../../agents/subagent-registry.js";
import { SILENT_REPLY_TOKEN } from "../../auto-reply/tokens.js";
import { isSilentReplyText, SILENT_REPLY_TOKEN } from "../../auto-reply/tokens.js";
import type { ReplyPayload } from "../../auto-reply/types.js";
import { createOutboundSendDeps, type CliDeps } from "../../cli/outbound-send-deps.js";
import type { OpenClawConfig } from "../../config/config.js";
@ -522,7 +522,7 @@ export async function dispatchCronDelivery(
hadDescendants &&
synthesizedText.trim() === initialSynthesizedText &&
isLikelyInterimCronMessage(initialSynthesizedText) &&
initialSynthesizedText.toUpperCase() !== SILENT_REPLY_TOKEN.toUpperCase()
!isSilentReplyText(initialSynthesizedText, SILENT_REPLY_TOKEN)
) {
// Descendants existed but no post-orchestration synthesis arrived AND
// no descendant fallback reply was available. Suppress stale parent
@ -537,12 +537,13 @@ export async function dispatchCronDelivery(
...params.telemetry,
});
}
if (synthesizedText.toUpperCase() === SILENT_REPLY_TOKEN.toUpperCase()) {
if (isSilentReplyText(synthesizedText, SILENT_REPLY_TOKEN)) {
return params.withRunSession({
status: "ok",
summary,
outputText,
delivered: true,
delivered: false,
deliveryAttempted: true,
...params.telemetry,
});
}