fix: strengthen cron heartbeat multi-payload suppression (#32131) (thanks @adhishthite)

This commit is contained in:
Peter Steinberger
2026-03-02 22:15:58 +00:00
parent 2330c71b63
commit 7253e91300
3 changed files with 24 additions and 2 deletions

View File

@@ -41,6 +41,7 @@ Docs: https://docs.openclaw.ai
- Security/Prompt spoofing hardening: stop injecting queued runtime events into user-role prompt text, route them through trusted system-prompt context, and neutralize inbound spoof markers like `[System Message]` and line-leading `System:` in untrusted message content. (#30448)
- Auto-reply/followup queue: avoid stale callback reuse across idle-window restarts by caching the followup runner only when a drain actually starts, preserving enqueue ordering after empty-finalize paths. (#31902) Thanks @Lanfei.
- Cron/HEARTBEAT_OK summary leak: suppress fallback main-session enqueue for heartbeat/internal ack summaries in isolated announce mode so `HEARTBEAT_OK` noise never appears in user chat while real summaries still forward. (#32093) Thanks @scoootscooob.
- Cron/isolated announce heartbeat suppression: treat multi-payload runs as skippable when any payload is a heartbeat ack token and no payload has media, preventing internal narration + trailing `HEARTBEAT_OK` from being delivered to users. (#32131) Thanks @adhishthite.
- Sessions/lock recovery: reclaim orphan legacy same-PID lock files missing `starttime` when no in-process lock ownership exists, avoiding false lock timeouts after PID reuse while preserving active lock safety checks. (#32081) Thanks @bmendonca3.
- Sessions/store cache invalidation: reload cached session stores when file size changes within the same mtime tick by keying cache validation on a single file-stat snapshot (`mtimeMs` + `sizeBytes`), with regression coverage for same-tick rewrites. (#32191) Thanks @jalehman.
- Config/raw redaction safety: preserve non-sensitive literals during raw redaction round-trips, scope SecretRef redaction to secret IDs (not structural fields like `source`/`provider`), and fall back to structured raw redaction when text replacement cannot restore the original config shape. (#32174) Thanks @bmendonca3.

View File

@@ -116,6 +116,27 @@ describe("runCronIsolatedAgentTurn", () => {
});
});
it("suppresses announce delivery for multi-payload narration ending in HEARTBEAT_OK", async () => {
await withTempHome(async (home) => {
const { storePath, deps } = await createTelegramDeliveryFixture(home);
mockEmbeddedAgentPayloads([
{ text: "Checked inbox and calendar. Nothing actionable yet." },
{ text: "HEARTBEAT_OK" },
]);
const res = await runTelegramAnnounceTurn({
home,
storePath,
deps,
});
expect(res.status).toBe("ok");
expect(res.delivered).toBe(false);
expect(deps.sendMessageTelegram).not.toHaveBeenCalled();
expect(runSubagentAnnounceFlow).not.toHaveBeenCalled();
});
});
it("handles media heartbeat delivery and announce cleanup modes", async () => {
await withTempHome(async (home) => {
const { storePath, deps } = await createTelegramDeliveryFixture(home);

View File

@@ -87,8 +87,8 @@ export function pickLastDeliverablePayload(payloads: DeliveryPayload[]) {
}
/**
* Check if all payloads are just heartbeat ack responses (HEARTBEAT_OK).
* Returns true if delivery should be skipped because there's no real content.
* Check if delivery should be skipped because the agent signaled no user-visible update.
* Returns true when any payload is a heartbeat ack token and no payload contains media.
*/
export function isHeartbeatOnlyResponse(payloads: DeliveryPayload[], ackMaxChars: number) {
if (payloads.length === 0) {