fix/heartbeat ok delivery filter (#246)

* cron: skip delivery for HEARTBEAT_OK responses

When an isolated cron job has deliver:true, skip message delivery if the
response is just HEARTBEAT_OK (or contains HEARTBEAT_OK at edges with
short remaining content <= 30 chars). This allows cron jobs to silently
ack when nothing to report but still deliver actual content when there
is something meaningful to say.

Media is still delivered even if text is HEARTBEAT_OK, since the
presence of media indicates there's something to share.

* fix(heartbeat): make ack padding configurable

* chore(deps): update to latest

---------

Co-authored-by: Josh Lehman <josh@martian.engineering>
This commit is contained in:
Peter Steinberger
2026-01-05 22:52:13 +00:00
committed by GitHub
parent dae7f560a5
commit f790f3f3ba
17 changed files with 549 additions and 397 deletions

View File

@@ -18,7 +18,10 @@ import {
ensureAgentWorkspace,
} from "../agents/workspace.js";
import { chunkText, resolveTextChunkLimit } from "../auto-reply/chunk.js";
import { stripHeartbeatToken } from "../auto-reply/heartbeat.js";
import {
DEFAULT_HEARTBEAT_ACK_MAX_CHARS,
stripHeartbeatToken,
} from "../auto-reply/heartbeat.js";
import { normalizeThinkLevel } from "../auto-reply/thinking.js";
import type { CliDeps } from "../cli/deps.js";
import type { ClawdbotConfig } from "../config/config.js";
@@ -64,6 +67,7 @@ function pickSummaryFromPayloads(
*/
function isHeartbeatOnlyResponse(
payloads: Array<{ text?: string; mediaUrl?: string; mediaUrls?: string[] }>,
ackMaxChars: number,
) {
if (payloads.length === 0) return true;
return payloads.every((payload) => {
@@ -72,11 +76,13 @@ function isHeartbeatOnlyResponse(
(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.
const result = stripHeartbeatToken(payload.text, { mode: "heartbeat" });
const result = stripHeartbeatToken(payload.text, {
mode: "heartbeat",
maxAckChars: ackMaxChars,
});
return result.shouldSkip;
});
}
function resolveDeliveryTarget(
cfg: ClawdbotConfig,
jobPayload: {
@@ -366,7 +372,10 @@ export async function runCronIsolatedAgentTurn(params: {
// Skip delivery for heartbeat-only responses (HEARTBEAT_OK with no real content).
// This allows cron jobs to silently ack when nothing to report but still deliver
// actual content when there is something to say.
const skipHeartbeatDelivery = delivery && isHeartbeatOnlyResponse(payloads);
const ackMaxChars =
params.cfg.agent?.heartbeat?.ackMaxChars ?? DEFAULT_HEARTBEAT_ACK_MAX_CHARS;
const skipHeartbeatDelivery =
delivery && isHeartbeatOnlyResponse(payloads, Math.max(0, ackMaxChars));
if (delivery && !skipHeartbeatDelivery) {
if (resolvedDelivery.channel === "whatsapp") {