Skip to content

Commit 023a101

Browse files
committed
fix(heartbeat): tolerate invalid commitment due timestamps
1 parent 8b92aca commit 023a101

2 files changed

Lines changed: 26 additions & 3 deletions

File tree

src/infra/heartbeat-runner.commitments.test.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ describe("runHeartbeatOnce commitments", () => {
3030
id: string;
3131
sessionKey: string;
3232
to: string;
33+
dueWindow?: CommitmentRecord["dueWindow"];
3334
sourceUserText?: string;
3435
sourceAssistantText?: string;
3536
}): CommitmentRecord {
@@ -48,7 +49,7 @@ describe("runHeartbeatOnce commitments", () => {
4849
suggestedText: "How did the interview go?",
4950
dedupeKey: "interview:2026-04-28",
5051
confidence: 0.92,
51-
dueWindow: {
52+
dueWindow: params.dueWindow ?? {
5253
earliestMs: nowMs - 60_000,
5354
latestMs: nowMs + 60 * 60_000,
5455
timezone: "America/Los_Angeles",
@@ -76,6 +77,7 @@ describe("runHeartbeatOnce commitments", () => {
7677
async function setupCommitmentCase(params?: {
7778
replyText?: string;
7879
target?: "last" | "none";
80+
dueWindow?: CommitmentRecord["dueWindow"];
7981
sourceUserText?: string;
8082
sourceAssistantText?: string;
8183
legacyRawSourceText?: boolean;
@@ -111,6 +113,7 @@ describe("runHeartbeatOnce commitments", () => {
111113
id: "cm_interview",
112114
sessionKey,
113115
to: "155462274",
116+
dueWindow: params?.dueWindow,
114117
sourceUserText: params?.sourceUserText,
115118
sourceAssistantText: params?.sourceAssistantText,
116119
}),
@@ -391,6 +394,25 @@ describe("runHeartbeatOnce commitments", () => {
391394
});
392395
});
393396

397+
it("tolerates Date-invalid commitment due timestamps in heartbeat prompts", async () => {
398+
const { result, sendTelegram, store } = await setupCommitmentCase({
399+
dueWindow: {
400+
earliestMs: nowMs - 60_000,
401+
latestMs: 8_700_000_000_000_000,
402+
timezone: "UTC",
403+
},
404+
});
405+
406+
expect(result.status).toBe("ran");
407+
expect(sendTelegram).toHaveBeenCalled();
408+
expectCommitmentFields(store.commitments[0], {
409+
id: "cm_interview",
410+
status: "sent",
411+
attempts: 1,
412+
sentAtMs: nowMs,
413+
});
414+
});
415+
394416
it("dismisses a due commitment when the heartbeat model declines to send a check-in", async () => {
395417
const { result, sendTelegram, store } = await setupCommitmentCase({
396418
replyText: HEARTBEAT_TOKEN,

src/infra/heartbeat-runner.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ import {
8989
toAgentStoreSessionKey,
9090
} from "../routing/session-key.js";
9191
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
92+
import { timestampMsToIsoString } from "../shared/number-coercion.js";
9293
import {
9394
normalizeLowercaseStringOrEmpty,
9495
normalizeOptionalString,
@@ -894,8 +895,8 @@ function buildCommitmentHeartbeatPrompt(params: {
894895
reason: commitment.reason,
895896
suggestedText: commitment.suggestedText,
896897
due: {
897-
earliest: new Date(commitment.dueWindow.earliestMs).toISOString(),
898-
latest: new Date(commitment.dueWindow.latestMs).toISOString(),
898+
earliest: timestampMsToIsoString(commitment.dueWindow.earliestMs) ?? "n/a",
899+
latest: timestampMsToIsoString(commitment.dueWindow.latestMs) ?? "n/a",
899900
timezone: commitment.dueWindow.timezone,
900901
},
901902
sourceMessageId: commitment.sourceMessageId,

0 commit comments

Comments
 (0)