diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c1eab9ba8b..5595aeafe6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -120,6 +120,7 @@ Docs: https://docs.openclaw.ai - Agents/exec: restore `host=node` routing for node-pinned and `host=auto` sessions, while still blocking sandboxed `auto` sessions from jumping to gateway. (#60788) Thanks @openperf. - Agents/compaction: keep assistant tool calls and displaced tool results in the same compaction chunk so strict summarization providers stop rejecting orphaned tool pairs. (#58849) Thanks @openperf. - Cron: suppress exact `NO_REPLY` sentinel direct-delivery payloads, keep silent direct replies from falling back into duplicate main-summary sends, and treat structured `deleteAfterRun` silent replies the same as text silent replies. (#45737) Thanks @openperf. +- Cron: keep exact silent-token detection case-insensitive again so mixed-case `NO_REPLY` outputs still stay silent in text and direct delivery paths. Thanks @obviyus. ## 2026.4.2 diff --git a/src/auto-reply/tokens.test.ts b/src/auto-reply/tokens.test.ts index f610fa35462..9c4b3611f1c 100644 --- a/src/auto-reply/tokens.test.ts +++ b/src/auto-reply/tokens.test.ts @@ -11,6 +11,11 @@ describe("isSilentReplyText", () => { expect(isSilentReplyText("\nNO_REPLY\n")).toBe(true); }); + it("returns true for mixed-case token", () => { + expect(isSilentReplyText("no_reply")).toBe(true); + expect(isSilentReplyText(" No_RePlY ")).toBe(true); + }); + it("returns false for undefined/empty", () => { expect(isSilentReplyText(undefined)).toBe(false); expect(isSilentReplyText("")).toBe(false); diff --git a/src/auto-reply/tokens.ts b/src/auto-reply/tokens.ts index 58812d68117..d028d009162 100644 --- a/src/auto-reply/tokens.ts +++ b/src/auto-reply/tokens.ts @@ -12,7 +12,7 @@ function getSilentExactRegex(token: string): RegExp { return cached; } const escaped = escapeRegExp(token); - const regex = new RegExp(`^\\s*${escaped}\\s*$`); + const regex = new RegExp(`^\\s*${escaped}\\s*$`, "i"); silentExactRegexByToken.set(token, regex); return regex; } diff --git a/src/cron/isolated-agent/delivery-dispatch.double-announce.test.ts b/src/cron/isolated-agent/delivery-dispatch.double-announce.test.ts index 6319dcd24d2..438a72d2cf5 100644 --- a/src/cron/isolated-agent/delivery-dispatch.double-announce.test.ts +++ b/src/cron/isolated-agent/delivery-dispatch.double-announce.test.ts @@ -673,6 +673,32 @@ describe("dispatchCronDelivery — double-announce guard", () => { ).toBe(false); }); + it("suppresses mixed-case NO_REPLY in text delivery", async () => { + vi.mocked(countActiveDescendantRuns).mockReturnValue(0); + vi.mocked(isLikelyInterimCronMessage).mockReturnValue(false); + + const params = makeBaseParams({ synthesizedText: "No_Reply" }); + const state = await dispatchCronDelivery(params); + + expect(deliverOutboundPayloads).not.toHaveBeenCalled(); + expect(state.result).toEqual( + expect.objectContaining({ + status: "ok", + delivered: false, + }), + ); + expect( + shouldEnqueueCronMainSummary({ + summaryText: "No_Reply", + deliveryRequested: true, + delivered: state.result?.delivered, + deliveryAttempted: state.result?.deliveryAttempted, + suppressMainSummary: false, + isCronSystemEvent: () => true, + }), + ).toBe(false); + }); + it("cleans up the direct cron session after a structured silent reply when deleteAfterRun is enabled", async () => { vi.mocked(countActiveDescendantRuns).mockReturnValue(0); vi.mocked(isLikelyInterimCronMessage).mockReturnValue(false);