Merge ff613f4c658aaffa57eaff7ef93726b31542c88f into 5e417b44e1540f528d2ae63e3e20229a902d1db2
This commit is contained in:
commit
bb95ad5cf2
@ -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`.
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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" },
|
||||
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user