Merge ff613f4c658aaffa57eaff7ef93726b31542c88f into 5e417b44e1540f528d2ae63e3e20229a902d1db2

This commit is contained in:
Shion Eria 2026-03-21 03:06:53 +00:00 committed by GitHub
commit bb95ad5cf2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 98 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

@ -61,4 +61,46 @@ describe("runCronIsolatedAgentTurn forum topic delivery", () => {
});
});
});
it('suppresses exact "NO_REPLY" for plain announce delivery', async () => {
await withTempCronHome(async (home) => {
const storePath = await writeSessionStore(home, { lastProvider: "webchat", lastTo: "" });
const deps = createCliDeps();
mockAgentPayloads([{ text: "NO_REPLY" }]);
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('suppresses exact "NO_REPLY" for forum-topic announce delivery', async () => {
await withTempCronHome(async (home) => {
const storePath = await writeSessionStore(home, { lastProvider: "webchat", lastTo: "" });
const deps = createCliDeps();
mockAgentPayloads([{ text: "NO_REPLY" }]);
const res = await runTelegramAnnounceTurn({
home,
storePath,
deps,
delivery: { mode: "announce", channel: "telegram", to: "123:topic:42" },
});
expect(res.status).toBe("ok");
expect(res.delivered).toBe(false);
expect(res.deliveryAttempted).toBe(true);
expect(runSubagentAnnounceFlow).not.toHaveBeenCalled();
expect(deps.sendMessageTelegram).not.toHaveBeenCalled();
});
});
});

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";
@ -112,6 +112,24 @@ export type DispatchCronDeliveryState = {
deliveryPayloads: ReplyPayload[];
};
function isDirectSilentReplyOnly(payloads: readonly ReplyPayload[]): boolean {
if (payloads.length !== 1) {
return false;
}
const [payload] = payloads;
if (!payload || !isSilentReplyText(payload.text, SILENT_REPLY_TOKEN)) {
return false;
}
return !(
payload.mediaUrl ||
(payload.mediaUrls?.length ?? 0) > 0 ||
payload.interactive ||
payload.btw ||
payload.audioAsVoice === true ||
Object.keys(payload.channelData ?? {}).length > 0
);
}
const TRANSIENT_DIRECT_CRON_DELIVERY_ERROR_PATTERNS: readonly RegExp[] = [
/\berrorcode=unavailable\b/i,
/\bstatus\s*[:=]\s*"?unavailable\b/i,
@ -340,6 +358,18 @@ export async function dispatchCronDelivery(
if (payloadsForDelivery.length === 0) {
return null;
}
if (isDirectSilentReplyOnly(payloadsForDelivery)) {
deliveryAttempted = true;
delivered = false;
return params.withRunSession({
status: "ok",
summary,
outputText,
delivered: false,
deliveryAttempted: true,
...params.telemetry,
});
}
if (params.isAborted()) {
return params.withRunSession({
status: "error",
@ -522,7 +552,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 +567,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,
});
}