fix(cron): suppress delivery when multi-payload response contains HEARTBEAT_OK
When a cron agent emits multiple text payloads (narration + tool summaries) followed by a final HEARTBEAT_OK, the delivery suppression check `isHeartbeatOnlyResponse` fails because it uses `.every()` — requiring ALL payloads to be heartbeat tokens. In practice, agents narrate their work before signaling nothing needs attention. Fix: check if ANY payload contains HEARTBEAT_OK (`.some()`) while preserving the media delivery exception (if any payload has media, always deliver). This matches the semantic intent: HEARTBEAT_OK is the agent's explicit signal that nothing needs user attention. Real-world example: heartbeat agent returns 3 payloads: 1. "It's 12:49 AM — quiet hours. Let me run the checks quickly." 2. "Emails: Just 2 calendar invites. Not urgent." 3. "HEARTBEAT_OK" Previously: all 3 delivered to Telegram. Now: correctly suppressed. Related: #32013 (fixed a different HEARTBEAT_OK leak path via system events in timer.ts)
This commit is contained in:
committed by
Peter Steinberger
parent
477de545f9
commit
2330c71b63
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
isHeartbeatOnlyResponse,
|
||||
pickLastDeliverablePayload,
|
||||
pickLastNonEmptyTextFromPayloads,
|
||||
pickSummaryFromPayloads,
|
||||
@@ -84,3 +85,65 @@ describe("pickLastDeliverablePayload", () => {
|
||||
expect(pickLastDeliverablePayload([normal, error])).toBe(normal);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isHeartbeatOnlyResponse", () => {
|
||||
const ACK_MAX = 300;
|
||||
|
||||
it("returns true for empty payloads", () => {
|
||||
expect(isHeartbeatOnlyResponse([], ACK_MAX)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for a single HEARTBEAT_OK payload", () => {
|
||||
expect(isHeartbeatOnlyResponse([{ text: "HEARTBEAT_OK" }], ACK_MAX)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for a single non-heartbeat payload", () => {
|
||||
expect(isHeartbeatOnlyResponse([{ text: "Something important happened" }], ACK_MAX)).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it("returns true when multiple payloads include narration followed by HEARTBEAT_OK", () => {
|
||||
// Agent narrates its work then signals nothing needs attention.
|
||||
expect(
|
||||
isHeartbeatOnlyResponse(
|
||||
[
|
||||
{ text: "It's 12:49 AM — quiet hours. Let me run the checks quickly." },
|
||||
{ text: "Emails: Just 2 calendar invites. Not urgent." },
|
||||
{ text: "HEARTBEAT_OK" },
|
||||
],
|
||||
ACK_MAX,
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false when media is present even with HEARTBEAT_OK text", () => {
|
||||
expect(
|
||||
isHeartbeatOnlyResponse(
|
||||
[{ text: "HEARTBEAT_OK", mediaUrl: "https://example.com/img.png" }],
|
||||
ACK_MAX,
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when media is in a different payload than HEARTBEAT_OK", () => {
|
||||
expect(
|
||||
isHeartbeatOnlyResponse(
|
||||
[
|
||||
{ text: "HEARTBEAT_OK" },
|
||||
{ text: "Here's an image", mediaUrl: "https://example.com/img.png" },
|
||||
],
|
||||
ACK_MAX,
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when no payload contains HEARTBEAT_OK", () => {
|
||||
expect(
|
||||
isHeartbeatOnlyResponse(
|
||||
[{ text: "Checked emails — found 3 urgent messages from your manager." }],
|
||||
ACK_MAX,
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -94,13 +94,18 @@ export function isHeartbeatOnlyResponse(payloads: DeliveryPayload[], ackMaxChars
|
||||
if (payloads.length === 0) {
|
||||
return true;
|
||||
}
|
||||
return payloads.every((payload) => {
|
||||
// If there's media, we should deliver regardless of text content.
|
||||
const hasMedia = (payload.mediaUrls?.length ?? 0) > 0 || Boolean(payload.mediaUrl);
|
||||
if (hasMedia) {
|
||||
return false;
|
||||
}
|
||||
// Use heartbeat mode to check if text is just HEARTBEAT_OK or short ack.
|
||||
// If any payload has media, deliver regardless — there's real content.
|
||||
const hasAnyMedia = payloads.some(
|
||||
(payload) => (payload.mediaUrls?.length ?? 0) > 0 || Boolean(payload.mediaUrl),
|
||||
);
|
||||
if (hasAnyMedia) {
|
||||
return false;
|
||||
}
|
||||
// An agent may emit multiple text payloads (narration, tool summaries)
|
||||
// before a final HEARTBEAT_OK. If *any* payload is a heartbeat ack token,
|
||||
// the agent is signaling "nothing needs attention" — the preceding text
|
||||
// payloads are just internal narration and should not be delivered.
|
||||
return payloads.some((payload) => {
|
||||
const result = stripHeartbeatToken(payload.text, {
|
||||
mode: "heartbeat",
|
||||
maxAckChars: ackMaxChars,
|
||||
|
||||
Reference in New Issue
Block a user