diff --git a/CHANGELOG.md b/CHANGELOG.md index 25c4aed8ce9..7c9c649d7f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ Docs: https://docs.openclaw.ai - ACP/setSessionMode: propagate gateway `sessions.patch` failures back to ACP clients so rejected mode changes no longer return silent success. (#41185) thanks @pejmanjohn. - Agents/embedded logs: add structured, sanitized lifecycle and failover observation events so overload and provider failures are easier to tail and filter. (#41336) thanks @altaywtf. - iOS/gateway foreground recovery: reconnect immediately on foreground return after stale background sockets are torn down, so the app no longer stays disconnected until a later wake path happens. (#41384) Thanks @mbelinky. +- Cron/subagent followup: do not misclassify empty or `NO_REPLY` cron responses as interim acknowledgements that need a rerun, so deliberately silent cron jobs are no longer retried. (#41383) thanks @jackal092927. ## 2026.3.8 diff --git a/src/cron/isolated-agent/subagent-followup.test.ts b/src/cron/isolated-agent/subagent-followup.test.ts index 093da010026..c670e4c8c13 100644 --- a/src/cron/isolated-agent/subagent-followup.test.ts +++ b/src/cron/isolated-agent/subagent-followup.test.ts @@ -47,8 +47,12 @@ describe("isLikelyInterimCronMessage", () => { false, ); }); - it("treats empty as interim", () => { - expect(isLikelyInterimCronMessage("")).toBe(true); + it("does not treat empty as interim (empty = NO_REPLY was stripped)", () => { + expect(isLikelyInterimCronMessage("")).toBe(false); + }); + + it("does not treat whitespace-only as interim", () => { + expect(isLikelyInterimCronMessage(" ")).toBe(false); }); }); diff --git a/src/cron/isolated-agent/subagent-followup.ts b/src/cron/isolated-agent/subagent-followup.ts index 6d5f9d4c502..9d6ec7e78ac 100644 --- a/src/cron/isolated-agent/subagent-followup.ts +++ b/src/cron/isolated-agent/subagent-followup.ts @@ -42,7 +42,10 @@ function normalizeHintText(value: string): string { export function isLikelyInterimCronMessage(value: string): boolean { const normalized = normalizeHintText(value); if (!normalized) { - return true; + // Empty text after payload filtering means the agent either returned + // NO_REPLY (deliberately silent) or produced no deliverable content. + // Do not treat this as an interim acknowledgement that needs a rerun. + return false; } const words = normalized.split(" ").filter(Boolean).length; return words <= 45 && INTERIM_CRON_HINTS.some((hint) => normalized.includes(hint));