From 56b00b60c60f8fa8c805cbfc38b48c9c25751c6a Mon Sep 17 00:00:00 2001 From: Thomas Joise Date: Fri, 17 Jul 2026 02:16:17 +0530 Subject: [PATCH] fix(reminders): deterministic tie-break in resolveSchedule on equal effective_from MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ORDER BY effective_from DESC alone picks an engine/plan-dependent winner when two reminder_schedule rows for the same rule_kind share an effective_from (a same-day cadence correction under rule 3's new-row-never-edit makes this likely). Append id DESC — UUIDv7 ids are time-ordered, so the latest insert wins deterministically on SQLite and Postgres alike. Co-Authored-By: Claude Fable 5 --- apps/hq/src/repos-reminders.ts | 2 +- apps/hq/test/reminder-schedule.test.ts | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/apps/hq/src/repos-reminders.ts b/apps/hq/src/repos-reminders.ts index 796e89c..18eec35 100644 --- a/apps/hq/src/repos-reminders.ts +++ b/apps/hq/src/repos-reminders.ts @@ -157,7 +157,7 @@ export function resolveSchedule(db: DB, ruleKind: ScheduleRuleKind, today: strin const row = db.prepare( `SELECT day_offsets, subject, body FROM reminder_schedule WHERE rule_kind=? AND effective_from <= ? AND (effective_to IS NULL OR effective_to > ?) - ORDER BY effective_from DESC LIMIT 1`, + ORDER BY effective_from DESC, id DESC LIMIT 1`, // id DESC breaks effective_from ties deterministically (UUIDv7 is time-ordered → latest insert wins on any engine) ).get(ruleKind, today, today) as { day_offsets: string; subject: string | null; body: string | null } | undefined if (row !== undefined) { const dayOffsets = parseDayOffsets(row.day_offsets) diff --git a/apps/hq/test/reminder-schedule.test.ts b/apps/hq/test/reminder-schedule.test.ts index 5e31987..842838a 100644 --- a/apps/hq/test/reminder-schedule.test.ts +++ b/apps/hq/test/reminder-schedule.test.ts @@ -44,6 +44,19 @@ describe('resolveSchedule', () => { expect(resolveSchedule(db, 'quote_followup', '2025-12-31').source).toBe('default') }) + it('tied effective_from resolves deterministically: highest id wins on any engine', () => { + const db = openDb(':memory:') + const ins = (id: string, offsets: string) => db.prepare( + `INSERT INTO reminder_schedule (id, rule_kind, effective_from, effective_to, day_offsets, subject, body) + VALUES (?, 'quote_followup', '2026-01-01', NULL, ?, NULL, NULL)`, + ).run(id, offsets) + // Insert the higher id FIRST so plain scan order would pick the other row — + // the tiebreaker (id DESC; UUIDv7 ids are time-ordered) must decide, not the plan. + ins('zzzz-high', '2,4') + ins('aaaa-low', '5,10') + expect(resolveSchedule(db, 'quote_followup', '2026-07-17').dayOffsets).toEqual([2, 4]) + }) + it('effective_to closes the window: active requires effective_to > today', () => { const db = openDb(':memory:') insertSchedule(db, { ruleKind: 'quote_followup', from: '2026-01-01', to: '2026-07-01', offsets: '1,2' })