Implement idempotency guard to prevent duplicate prompt scheduling
epic-scenario-based-follow-up-prompts-infrastructure-task-005 — Add an idempotency check at the start of each scheduler invocation cycle. Query the prompt_history table for entries already created within the current evaluation window. Skip any activity that already has a prompt record for the current window, ensuring repeated cron executions within the same hour produce no duplicate prompts.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 4 - 323 tasks
Can start after Tier 3 completes
Implementation Notes
Define a `getEvaluationWindow()` helper that returns `{ windowStart: Date, windowEnd: Date }` computed as UTC hour floor/ceiling of the current timestamp. Query: `supabase.from('prompt_history').select('activity_id').gte('created_at', windowStart.toISOString()).lte('created_at', windowEnd.toISOString())`. Build a `const alreadyScheduled = new Set(rows.map(r => r.activity_id))`. In the per-activity loop (from task-004), check `if (alreadyScheduled.has(activity.id))` before the try/catch wrapper and throw a `SkipSignal('already_scheduled_in_window')` which the outer loop catches and routes to the `skipped` array.
Keep the window duration configurable via an environment variable `IDEMPOTENCY_WINDOW_HOURS` defaulting to `1` so it can be widened for hourly cron without code changes.
Testing Requirements
Unit tests: (1) given a Set containing activity_id 'act-1', processing 'act-1' returns skip signal immediately without calling Scheduler Service; (2) given an empty Set, processing 'act-1' calls Scheduler Service normally; (3) evaluation window boundaries are correct for known timestamps (midnight, 23:59, etc.). Integration tests: (1) run function once, assert N new rows in `prompt_history`; (2) run function again within same window, assert zero new rows; (3) advance mock clock past window boundary, run again, assert N new rows. Use Supabase local dev (supabase start) for integration tests against a real SQLite-backed PostgREST stub.
Supabase Edge Functions on Deno can have cold-start latency of 500ms–2s. If the evaluation window contains many activities (e.g., post-holiday catch-up), the function may approach the 60-second invocation timeout before completing all evaluations.
Mitigation & Contingency
Mitigation: Implement pagination in the activity fetch query with a configurable page size; process pages sequentially and commit history records per page so partial runs are recoverable on the next invocation.
Contingency: If timeout remains an issue at scale, split the evaluation into per-chapter invocations triggered by a fan-out pattern using Supabase Realtime or a lightweight queue.
Supabase cron triggers (pg_cron or Edge Function schedules) may miss invocations during platform maintenance windows, causing evaluation gaps that delay time-sensitive prompts beyond their intended delivery window.
Mitigation & Contingency
Mitigation: Configure the look-back window to be 2× the cron interval (e.g., 2-hour look-back for hourly cron) so a single missed invocation does not result in missed prompts; log each run's look-back range for auditability.
Contingency: If missed invocations are detected via monitoring alerts, implement a manual re-trigger endpoint accessible to admins that runs the evaluation for a specified time range.