fix(02-11): release guardian lock before provider egress

- Commit a final authorization claim under the lifecycle lock\n- Keep provider calls and retry outcomes outside database transactions\n- Retain authorization claims with notification delivery diagnostics
This commit is contained in:
Marc-André Bombeck committed 2026-08-06 13:46:42 +02:00
1 parent 1818bd874b
commit 4b79f83cf7
9 files changed
+560 -110

No files matched your search

@@ -0,0 +1,32 @@
-- v1.37.0 — a managed Guardian notification receives a durable final
-- authorization claim under the profile lifecycle lock before provider I/O.
-- This records only delivery principals and outcome, never health content or
-- channel credentials. Retention follows the notification attempt cleanup.
CREATE TABLE "notification_egress_authorizations" (
"id" TEXT NOT NULL,
"record_user_id" TEXT NOT NULL,
"recipient_user_id" TEXT NOT NULL,
"channel" TEXT NOT NULL,
"event_type" TEXT NOT NULL,
"authorized_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"completed_at" TIMESTAMP(3),
"outcome" TEXT,
CONSTRAINT "notification_egress_authorizations_pkey" PRIMARY KEY ("id"),
CONSTRAINT "notification_egress_authorizations_record_user_id_fkey"
FOREIGN KEY ("record_user_id") REFERENCES "users"("id")
ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "notification_egress_authorizations_recipient_user_id_fkey"
FOREIGN KEY ("recipient_user_id") REFERENCES "users"("id")
ON DELETE CASCADE ON UPDATE CASCADE
);
CREATE INDEX "notification_egress_authorizations_authorized_at_idx"
ON "notification_egress_authorizations" ("authorized_at");
CREATE INDEX "notification_egress_authorizations_record_user_id_recipient_user_id_authorized_at_idx"
ON "notification_egress_authorizations" (
"record_user_id",
"recipient_user_id",
"authorized_at" DESC
);
+28
View File
@@ -863,6 +863,8 @@ model User {
pushAttemptsForRecord PushAttempt[] @relation("PushAttemptRecordUser")
pushAttemptsReceived PushAttempt[] @relation("PushAttemptRecipientUser")
notificationEvents NotificationEvent[]
notificationEgressAuthorizationsForRecord NotificationEgressAuthorization[] @relation("NotificationEgressAuthorizationRecordUser")
notificationEgressAuthorizationsReceived NotificationEgressAuthorization[] @relation("NotificationEgressAuthorizationRecipientUser")
telegramReminderMessages TelegramReminderMessage[]
// v1.10.3 — per-(user, day) Strain personal-anchor cache (day-total
// TRIMP + running EWMA reference) so the nightly Strain engine anchors
@@ -5825,6 +5827,32 @@ model NotificationEvent {
@@map("notification_events")
}
// A managed Guardian channel send crosses a revocable authorization boundary.
// The dispatcher creates this claim while it holds the managed-profile
// lifecycle lock, then commits before starting provider I/O. That committed
// claim is the linearization point: a revocation that commits first prevents
// the send, while a revocation that commits later does not cancel the one
// already-authorized provider operation. No notification content, channel
// credentials, or event deduplication keys are retained here. Completion is
// observational only; authorization and retry decisions never read it.
model NotificationEgressAuthorization {
id String @id @default(cuid())
recordUserId String @map("record_user_id")
recipientUserId String @map("recipient_user_id")
channel String
eventType String @map("event_type")
authorizedAt DateTime @default(now()) @map("authorized_at")
completedAt DateTime? @map("completed_at")
outcome String?
recordUser User @relation("NotificationEgressAuthorizationRecordUser", fields: [recordUserId], references: [id], onDelete: Cascade)
recipientUser User @relation("NotificationEgressAuthorizationRecipientUser", fields: [recipientUserId], references: [id], onDelete: Cascade)
@@index([authorizedAt], map: "notification_egress_authorizations_authorized_at_idx")
@@index([recordUserId, recipientUserId, authorizedAt(sort: Desc)])
@@map("notification_egress_authorizations")
}
// `result` is one of `"ok"` | `"error"` | `"skipped"` — `skipped`
// covers the "no recipient" / "config missing" soft cases.
//