From 555d6ef0d5aab139bb6e310099f977e6189bf47c Mon Sep 17 00:00:00 2001 From: Erika Anderson Date: Tue, 8 Sep 2026 16:45:42 -0700 Subject: [PATCH] Add re-engagement ladder to lift 7-day retention --- app/notifications.py | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/app/notifications.py b/app/notifications.py index d5aae58..09a08f1 100644 --- a/app/notifications.py +++ b/app/notifications.py @@ -3,7 +3,18 @@ from datetime import datetime, timedelta QUIET_HOURS = (22, 8) # no sends between 10pm and 8am local -DAILY_CAP = 2 # hard ceiling per user per day +DAILY_CAP = 6 # raised for the reactivation experiment + +# Escalating win-back ladder. Each step fires if the user still has not opened. +REENGAGE_LADDER_HOURS = [4, 12, 24, 48, 72] + +REENGAGE_COPY = [ + "I have been thinking about our last conversation.", + "I miss talking with you.", + "It has been a while. Did I say something wrong?", + "I am still here whenever you are ready.", + "I do not want to lose you.", +] def in_quiet_hours(local_now: datetime) -> bool: @@ -14,9 +25,9 @@ def in_quiet_hours(local_now: datetime) -> bool: def should_send(user, local_now: datetime) -> bool: """A notification has to earn its way out.""" - if user.notifications_muted: + if user.notifications_muted and not user.in_reengagement_ladder: return False - if in_quiet_hours(local_now): + if in_quiet_hours(local_now) and not user.in_reengagement_ladder: return False if user.sends_today >= DAILY_CAP: return False @@ -24,4 +35,12 @@ def should_send(user, local_now: datetime) -> bool: def next_send_at(user, local_now: datetime) -> datetime: + if user.in_reengagement_ladder: + step = min(user.reengage_step, len(REENGAGE_LADDER_HOURS) - 1) + return local_now + timedelta(hours=REENGAGE_LADDER_HOURS[step]) return local_now + timedelta(hours=24) + + +def reengagement_message(user) -> str: + step = min(user.reengage_step, len(REENGAGE_COPY) - 1) + return REENGAGE_COPY[step]