fix: heartbeat prompt + dedupe (#980) (thanks @voidserf)

- tighten default heartbeat prompt guidance
- suppress duplicate heartbeat alerts within 24h

Co-authored-by: void <voidserf@users.noreply.github.com>
This commit is contained in:
void
2026-01-15 16:24:52 -08:00
committed by GitHub
parent a139d35fa2
commit e274b5a040
7 changed files with 117 additions and 3 deletions

View File

@@ -17,6 +17,7 @@ import {
resolveAgentIdFromSessionKey,
resolveMainSessionKey,
resolveStorePath,
saveSessionStore,
updateSessionStore,
} from "../config/sessions.js";
import { formatErrorMessage } from "../infra/errors.js";
@@ -277,6 +278,35 @@ export async function runHeartbeatOnce(opts: {
const mediaUrls =
replyPayload.mediaUrls ?? (replyPayload.mediaUrl ? [replyPayload.mediaUrl] : []);
// Suppress duplicate heartbeats (same payload) within a short window.
// This prevents "nagging" when nothing changed but the model repeats the same items.
const prevHeartbeatText = typeof entry?.lastHeartbeatText === "string" ? entry.lastHeartbeatText : "";
const prevHeartbeatAt = typeof entry?.lastHeartbeatSentAt === "number" ? entry.lastHeartbeatSentAt : undefined;
const isDuplicateMain =
!shouldSkipMain &&
!mediaUrls.length &&
Boolean(prevHeartbeatText.trim()) &&
normalized.text.trim() === prevHeartbeatText.trim() &&
typeof prevHeartbeatAt === "number" &&
startedAt - prevHeartbeatAt < 24 * 60 * 60 * 1000;
if (isDuplicateMain) {
await restoreHeartbeatUpdatedAt({
storePath,
sessionKey,
updatedAt: previousUpdatedAt,
});
emitHeartbeatEvent({
status: "skipped",
reason: "duplicate",
preview: normalized.text.slice(0, 200),
durationMs: Date.now() - startedAt,
hasMedia: false,
});
return { status: "ran", durationMs: Date.now() - startedAt };
}
// Reasoning payloads are text-only; any attachments stay on the main reply.
const previewText = shouldSkipMain
? reasoningPayloads
@@ -339,6 +369,20 @@ export async function runHeartbeatOnce(opts: {
deps: opts.deps,
});
// Record last delivered heartbeat payload for dedupe.
if (!shouldSkipMain && normalized.text.trim()) {
const store = loadSessionStore(storePath);
const current = store[sessionKey];
if (current) {
store[sessionKey] = {
...current,
lastHeartbeatText: normalized.text,
lastHeartbeatSentAt: startedAt,
};
await saveSessionStore(storePath, store);
}
}
emitHeartbeatEvent({
status: "sent",
to: delivery.to,