fix: align cron prompt content with filtered reminder events

This commit is contained in:
Vignesh Natarajan
2026-02-12 16:01:53 -08:00
committed by Vignesh
parent 7a8a57b573
commit 54513f4240
4 changed files with 45 additions and 14 deletions

View File

@@ -42,6 +42,7 @@ Docs: https://docs.openclaw.ai
- Slack: detect control commands when channel messages start with bot mention prefixes (for example, `@Bot /new`). (#14142) Thanks @beefiker.
- Signal: enforce E.164 validation for the Signal bot account prompt so mistyped numbers are caught early. (#15063) Thanks @Duartemartins.
- Discord: process DM reactions instead of silently dropping them. (#10418) Thanks @mcaxtr.
- Heartbeat: filter noise-only system events so scheduled reminder notifications do not fire when cron runs carry only heartbeat markers. (#13317) Thanks @pvtclawn.
- Signal: render mention placeholders as `@uuid`/`@phone` so mention gating and Clawdbot targeting work. (#2013) Thanks @alexgleason.
- Discord: omit empty content fields for media-only messages while preserving caption whitespace. (#9507) Thanks @leszekszpunar.
- Onboarding/Providers: add Z.AI endpoint-specific auth choices (`zai-coding-global`, `zai-coding-cn`, `zai-global`, `zai-cn`) and expand default Z.AI model wiring. (#13456) Thanks @tomsun28.

View File

@@ -11,6 +11,8 @@ describe("isCronSystemEvent", () => {
expect(isCronSystemEvent("HEARTBEAT_OK")).toBe(false);
expect(isCronSystemEvent("HEARTBEAT_OK 🦞")).toBe(false);
expect(isCronSystemEvent("heartbeat_ok")).toBe(false);
expect(isCronSystemEvent("HEARTBEAT_OK:")).toBe(false);
expect(isCronSystemEvent("HEARTBEAT_OK, continue")).toBe(false);
});
it("returns false for heartbeat poll and wake noise", () => {

View File

@@ -135,6 +135,9 @@ describe("Ghost reminder bug (issue #13317)", () => {
const calledCtx = getReplySpy.mock.calls[0]?.[0];
expect(calledCtx?.Provider).toBe("cron-event");
expect(calledCtx?.Body).toContain("scheduled reminder has been triggered");
expect(calledCtx?.Body).toContain("Reminder: Check Base Scout results");
expect(calledCtx?.Body).not.toContain("HEARTBEAT_OK");
expect(calledCtx?.Body).not.toContain("heartbeat poll");
expect(sendTelegram).toHaveBeenCalled();
} finally {
await fs.rm(tmpDir, { recursive: true, force: true });
@@ -170,6 +173,9 @@ describe("Ghost reminder bug (issue #13317)", () => {
const calledCtx = getReplySpy.mock.calls[0]?.[0];
expect(calledCtx?.Provider).toBe("cron-event");
expect(calledCtx?.Body).toContain("scheduled reminder has been triggered");
expect(calledCtx?.Body).toContain("Reminder: Check Base Scout results");
expect(calledCtx?.Body).not.toContain("HEARTBEAT_OK");
expect(calledCtx?.Body).not.toContain("heartbeat poll");
expect(sendTelegram).toHaveBeenCalled();
} finally {
await fs.rm(tmpDir, { recursive: true, force: true });

View File

@@ -114,26 +114,47 @@ function buildCronEventPrompt(pendingEvents: string[]): string {
);
}
// Returns true when a system event should be treated as real cron reminder content.
export function isCronSystemEvent(evt: string) {
const HEARTBEAT_OK_PREFIX = HEARTBEAT_TOKEN.toLowerCase();
// Detect heartbeat-specific noise so cron reminders don't trigger on non-reminder events.
function isHeartbeatAckEvent(evt: string): boolean {
const trimmed = evt.trim();
if (!trimmed) {
return false;
}
const lower = trimmed.toLowerCase();
const heartbeatOk = HEARTBEAT_TOKEN.toLowerCase();
if (lower === heartbeatOk || lower.startsWith(`${heartbeatOk} `)) {
if (!lower.startsWith(HEARTBEAT_OK_PREFIX)) {
return false;
}
if (lower.includes("heartbeat poll") || lower.includes("heartbeat wake")) {
return false;
}
if (lower.includes("exec finished")) {
return false;
const suffix = lower.slice(HEARTBEAT_OK_PREFIX.length);
if (suffix.length === 0) {
return true;
}
return !/[a-z0-9_]/.test(suffix[0]);
}
return true;
function isHeartbeatNoiseEvent(evt: string): boolean {
const lower = evt.trim().toLowerCase();
if (!lower) {
return false;
}
return (
isHeartbeatAckEvent(lower) ||
lower.includes("heartbeat poll") ||
lower.includes("heartbeat wake")
);
}
function isExecCompletionEvent(evt: string): boolean {
return evt.toLowerCase().includes("exec finished");
}
// Returns true when a system event should be treated as real cron reminder content.
export function isCronSystemEvent(evt: string) {
if (!evt.trim()) {
return false;
}
return !isHeartbeatNoiseEvent(evt) && !isExecCompletionEvent(evt);
}
type HeartbeatAgentState = {
@@ -521,12 +542,13 @@ export async function runHeartbeatOnce(opts: {
const isExecEvent = opts.reason === "exec-event";
const isCronEvent = Boolean(opts.reason?.startsWith("cron:"));
const pendingEvents = isExecEvent || isCronEvent ? peekSystemEvents(sessionKey) : [];
const hasExecCompletion = pendingEvents.some((evt) => evt.includes("Exec finished"));
const hasCronEvents = isCronEvent && pendingEvents.some((evt) => isCronSystemEvent(evt));
const cronEvents = pendingEvents.filter((evt) => isCronSystemEvent(evt));
const hasExecCompletion = pendingEvents.some(isExecCompletionEvent);
const hasCronEvents = isCronEvent && cronEvents.length > 0;
const prompt = hasExecCompletion
? EXEC_EVENT_PROMPT
: hasCronEvents
? buildCronEventPrompt(pendingEvents)
? buildCronEventPrompt(cronEvents)
: resolveHeartbeatPrompt(cfg, heartbeat);
const ctx = {
Body: appendCronStyleCurrentTimeLine(prompt, cfg, startedAt),