fix(cron): suppress NO_REPLY sentinel in direct delivery path

This commit is contained in:
openperf 2026-03-14 12:26:19 +08:00
parent b6d1d0d72d
commit bfc7f0d468
2 changed files with 34 additions and 2 deletions

View File

@ -413,4 +413,32 @@ describe("dispatchCronDelivery — double-announce guard", () => {
vi.unstubAllEnvs();
}
});
it("suppresses NO_REPLY payload in direct delivery so sentinel never leaks to external channels", async () => {
vi.mocked(countActiveDescendantRuns).mockReturnValue(0);
vi.mocked(isLikelyInterimCronMessage).mockReturnValue(false);
const params = makeBaseParams({ synthesizedText: "NO_REPLY" });
// Force the useDirectDelivery path (structured content) to exercise
// deliverViaDirect without going through finalizeTextDelivery.
(params as Record<string, unknown>).deliveryPayloadHasStructuredContent = true;
const state = await dispatchCronDelivery(params);
// NO_REPLY must be filtered out before reaching the outbound adapter.
expect(deliverOutboundPayloads).not.toHaveBeenCalled();
// No delivery was sent, so delivered stays false.
expect(state.delivered).toBe(false);
});
it("suppresses NO_REPLY payload with surrounding whitespace", async () => {
vi.mocked(countActiveDescendantRuns).mockReturnValue(0);
vi.mocked(isLikelyInterimCronMessage).mockReturnValue(false);
const params = makeBaseParams({ synthesizedText: " NO_REPLY " });
(params as Record<string, unknown>).deliveryPayloadHasStructuredContent = true;
const state = await dispatchCronDelivery(params);
expect(deliverOutboundPayloads).not.toHaveBeenCalled();
expect(state.delivered).toBe(false);
});
});

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";
@ -314,12 +314,16 @@ export async function dispatchCronDelivery(
delivery,
});
try {
const payloadsForDelivery =
const rawPayloads =
deliveryPayloads.length > 0
? deliveryPayloads
: synthesizedText
? [{ text: synthesizedText }]
: [];
// Suppress NO_REPLY sentinel so it never leaks to external channels.
const payloadsForDelivery = rawPayloads.filter(
(p) => !isSilentReplyText(p.text, SILENT_REPLY_TOKEN),
);
if (payloadsForDelivery.length === 0) {
return null;
}