Files
HealthLog/prisma/migrations/0196_coach_reminder/migration.sql
T
Marc-André Bombeck 9b61769a32 feat(coach): add the CoachReminder episodic-memory model
Introduce a third Coach memory type alongside facts (durable traits) and
plans (standing if-then habits): a time-anchored "bring this back later"
reminder with a due moment and a done state. The free-text note is
AES-256-GCM ciphertext via the shared Bytes codec; metric, status, the
trigger fields and the dates stay plain so the sweep and the injection
picker rank and filter without a per-row decrypt.

Migration 0196 is purely additive (status and trigger_kind are app-side
closed enums, not DB enums). Register the new note_encrypted column in the
encrypted-columns registry and the rotation script so a key rotation can
never silently skip it.
2026-06-27 13:37:50 +02:00

43 lines
2.2 KiB
SQL

-- v1.22 (B2/B6) — Coach episodic reminder memory.
--
-- A time-anchored "bring this back later" intent the Coach captured from a
-- conversation. The missing memory type between coach_facts (durable traits)
-- and coach_plans (standing if-then habits): an episode with a due moment and
-- a done state. The free-text note is AES-256-GCM ciphertext stored as `bytea`
-- via the shared Bytes codec — the same shape as coach_plans.if_cue_encrypted.
-- metric / status / trigger fields / dates / source stay plain so the daily
-- sweep and the injection picker can rank and filter without a per-row decrypt.
-- status (proposed | active | due | surfaced | done | dismissed) and
-- trigger_kind (date | context) are app-side closed enums, deliberately NOT DB
-- enums so this migration stays purely additive. Soft-delete via deleted_at.
CREATE TABLE "coach_reminders" (
"id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"note_encrypted" BYTEA NOT NULL,
"metric" TEXT,
"related_plan_id" TEXT,
"trigger_kind" TEXT NOT NULL DEFAULT 'date',
"due_at" TIMESTAMP(3),
"context_cue" TEXT,
"status" TEXT NOT NULL DEFAULT 'active',
"source" TEXT NOT NULL,
"source_conversation_id" TEXT,
"last_surfaced_at" TIMESTAMP(3),
"surface_count" INTEGER NOT NULL DEFAULT 0,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
"deleted_at" TIMESTAMP(3),
CONSTRAINT "coach_reminders_pkey" PRIMARY KEY ("id")
);
-- The daily sweep scans a user's active reminders whose due_at has passed; the
-- status + deleted_at columns are part of that predicate, so the composite
-- index lets the planner serve the sweep + the injection read index-backed.
CREATE INDEX "coach_reminders_user_id_status_due_at_deleted_at_idx" ON "coach_reminders"("user_id", "status", "due_at", "deleted_at");
-- The context-cue evaluation pass filters by trigger_kind + status per user.
CREATE INDEX "coach_reminders_user_id_trigger_kind_status_idx" ON "coach_reminders"("user_id", "trigger_kind", "status");
ALTER TABLE "coach_reminders" ADD CONSTRAINT "coach_reminders_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;