Skip to content

Commit 18eca26

Browse files
Sonra0claude
andcommitted
fix: use snapshot tick time so assessments don't miss their window
When processReminders() made Twilio calls (Gemini + ElevenLabs + Twilio), it could take over a minute. By the time processAssessments() ran, the current minute had changed and the exact-time match failed silently. Now all functions use the same tickTime captured at the start of each cron tick, ensuring assessments match even if earlier processing delays. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 5e806a0 commit 18eca26

1 file changed

Lines changed: 25 additions & 22 deletions

File tree

src/cron/worker.ts

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import { executeAssessmentCall } from "../lib/assessment-call";
77
const MAX_ATTEMPTS = 3;
88
const RETRY_INTERVAL_MINUTES = 5;
99

10-
function getTimeInTimezone(timezone: string) {
11-
const now = new Date();
10+
function getTimeInTimezone(timezone: string, referenceTime?: Date) {
11+
const now = referenceTime || new Date();
1212
const formatted = now.toLocaleString("en-US", {
1313
timeZone: timezone,
1414
hour: "2-digit",
@@ -20,26 +20,26 @@ function getTimeInTimezone(timezone: string) {
2020
return `${h.padStart(2, "0")}:${m.padStart(2, "0")}`;
2121
}
2222

23-
function getDayOfWeekInTimezone(timezone: string) {
24-
const now = new Date();
23+
function getDayOfWeekInTimezone(timezone: string, referenceTime?: Date) {
24+
const now = referenceTime || new Date();
2525
const dayStr = now.toLocaleString("en-US", { timeZone: timezone, weekday: "short" });
2626
const dayMap: Record<string, number> = { Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6 };
2727
return dayMap[dayStr] ?? now.getDay();
2828
}
2929

30-
function getTodayDateStringInTimezone(timezone: string) {
31-
const now = new Date();
30+
function getTodayDateStringInTimezone(timezone: string, referenceTime?: Date) {
31+
const now = referenceTime || new Date();
3232
const parts = now.toLocaleDateString("en-CA", { timeZone: timezone }).split("-");
3333
return parts.join("-");
3434
}
3535

36-
function getNowMinutesInTimezone(timezone: string) {
37-
const timeStr = getTimeInTimezone(timezone);
36+
function getNowMinutesInTimezone(timezone: string, referenceTime?: Date) {
37+
const timeStr = getTimeInTimezone(timezone, referenceTime);
3838
const [h, m] = timeStr.split(":").map(Number);
3939
return h * 60 + m;
4040
}
4141

42-
async function deactivatePastReminders() {
42+
async function deactivatePastReminders(tickTime: Date) {
4343
// Find all active reminders with a scheduledDate
4444
const reminders = await prisma.reminder.findMany({
4545
where: { active: true, scheduledDate: { not: null } },
@@ -48,7 +48,7 @@ async function deactivatePastReminders() {
4848

4949
for (const reminder of reminders) {
5050
const tz = reminder.elderlyProfile.timezone || "UTC";
51-
const todayStr = getTodayDateStringInTimezone(tz);
51+
const todayStr = getTodayDateStringInTimezone(tz, tickTime);
5252
if (reminder.scheduledDate! < todayStr) {
5353
await prisma.reminder.update({
5454
where: { id: reminder.id },
@@ -59,7 +59,7 @@ async function deactivatePastReminders() {
5959
}
6060
}
6161

62-
async function processReminders() {
62+
async function processReminders(tickTime: Date) {
6363
const reminders = await prisma.reminder.findMany({
6464
where: {
6565
active: true,
@@ -78,9 +78,9 @@ async function processReminders() {
7878

7979
for (const reminder of reminders) {
8080
const tz = reminder.elderlyProfile.timezone || "UTC";
81-
const currentTime = getTimeInTimezone(tz);
82-
const currentDay = getDayOfWeekInTimezone(tz);
83-
const todayStr = getTodayDateStringInTimezone(tz);
81+
const currentTime = getTimeInTimezone(tz, tickTime);
82+
const currentDay = getDayOfWeekInTimezone(tz, tickTime);
83+
const todayStr = getTodayDateStringInTimezone(tz, tickTime);
8484

8585
// If reminder has a specific date, only fire on that date
8686
if (reminder.scheduledDate && reminder.scheduledDate !== todayStr) continue;
@@ -109,7 +109,7 @@ async function processReminders() {
109109
if (existingLog) continue;
110110
} else if (intervalHours) {
111111
// Hour-based intervals: first call at scheduledTime, then repeat every N hours
112-
const nowMinutes = getNowMinutesInTimezone(tz);
112+
const nowMinutes = getNowMinutesInTimezone(tz, tickTime);
113113
const startMinutes = h * 60 + m;
114114

115115
// Only fire at or after the scheduled start time
@@ -274,7 +274,7 @@ async function processEmergencyCalls() {
274274
}
275275
}
276276

277-
async function processAssessments() {
277+
async function processAssessments(tickTime: Date) {
278278
const configs = await prisma.assessmentConfig.findMany({
279279
where: {
280280
active: true,
@@ -287,13 +287,13 @@ async function processAssessments() {
287287

288288
for (const config of configs) {
289289
const tz = config.elderlyProfile.timezone || "UTC";
290-
const currentTime = getTimeInTimezone(tz);
290+
const currentTime = getTimeInTimezone(tz, tickTime);
291291

292292
console.log(`[Assessments] ${config.elderlyProfile.name}: scheduled=${config.scheduledTime}, current=${currentTime} (tz=${tz})`);
293293

294294
if (config.scheduledTime !== currentTime) continue;
295295

296-
const todayStr = getTodayDateStringInTimezone(tz);
296+
const todayStr = getTodayDateStringInTimezone(tz, tickTime);
297297

298298
const existingSession = await prisma.assessmentSession.findFirst({
299299
where: { configId: config.id, date: todayStr },
@@ -348,12 +348,15 @@ async function processAssessments() {
348348
}
349349

350350
cron.schedule("* * * * *", async () => {
351-
console.log(`[${new Date().toISOString()}] Checking reminders...`);
351+
// Snapshot the time at tick start so all functions use the same minute
352+
// even if earlier functions (e.g. making Twilio calls) take a while
353+
const tickTime = new Date();
354+
console.log(`[${tickTime.toISOString()}] Checking reminders...`);
352355

353-
try { await deactivatePastReminders(); } catch (error) {
356+
try { await deactivatePastReminders(tickTime); } catch (error) {
354357
console.error("Error in deactivatePastReminders:", error);
355358
}
356-
try { await processReminders(); } catch (error) {
359+
try { await processReminders(tickTime); } catch (error) {
357360
console.error("Error in processReminders:", error);
358361
}
359362
try { await processRetries(); } catch (error) {
@@ -362,7 +365,7 @@ cron.schedule("* * * * *", async () => {
362365
try { await processEmergencyCalls(); } catch (error) {
363366
console.error("Error in processEmergencyCalls:", error);
364367
}
365-
try { await processAssessments(); } catch (error) {
368+
try { await processAssessments(tickTime); } catch (error) {
366369
console.error("Error in processAssessments:", error);
367370
}
368371
});

0 commit comments

Comments
 (0)