You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A MentorReflectionScheduler that runs proactive reflection mentor sessions on a per-user cadence. The scheduler reads each user's preference row, resolves the next due time in the user's timezone, and triggers a SLACK_DM-surface mentor session through the same orchestrator + adapter the on-demand entry points use.
Cadence options:
Option
Trigger
OFF
No proactive reflection; user can still open mentor on demand.
WEEKLY (default)
One reflection per week. Default: Monday 09:00 in the user's resolved timezone. User configurable time_of_day + weekday.
END_OF_SPRINT
One reflection per sprint, fired at sprint-end + 1 day at the user's configured time_of_day. Consumes the workspace's sprint calendar.
DAILY
One reflection per weekday at the user's configured time_of_day. Tightened min-interval enforced by #1259.
A new user_reflection_preference(user_id, workspace_id, cadence, weekday, time_of_day, timezone, updated_at) table stores the configuration. Default for a new user in a workspace is WEEKLY, Monday, 09:00, with timezone resolved from the identity-link OIDC zoneinfo claim (#1238). A user whose timezone cannot be resolved is treated as OFF until they configure it via #1262.
The scheduler is a @Scheduled Spring task running every 5 minutes. Each tick queries due users (next_due_at <= now()) with a SKIP LOCKED row lock so multiple instances do not double-fire. Each due user enqueues one reflection job through the shared notification budget from #1260; the job composes the three-phase reflection template (#1261) and the DM safety rails (#1259) before sending.
Why
The scheduled cadence is the SDT-aligned proactive intervention the SRL literature recommends — the system asks the right question on a predictable rhythm the learner chose. Without per-user cadence + timezone resolution, the alternative is either spam (one cadence for all) or silence (no proactive reflection). The default of weekly Monday 09:00 matches the formative-feedback literature's "weekly retrospective" finding and is the cadence pilot workspaces converged on; the option set is small enough that the configuration UI from #1262 is one screen.
Acceptance criteria
user_reflection_preference table exists via Liquibase changeset with the columns above, a unique (user_id, workspace_id) index, and a CHECK constraint on cadence enum values
On user-workspace creation a default row is inserted: cadence=WEEKLY, weekday=MONDAY, time_of_day=09:00, timezone=<resolved from OIDC or NULL>
The MentorReflectionScheduler runs at a 5-minute cadence and uses a SKIP LOCKED query to fetch due users; an integration test runs the scheduler twice concurrently and asserts no user fires twice in one tick
Cadence OFF is honored — no DMs fired; an integration test seeds three users with OFF / WEEKLY / DAILY and asserts only the latter two fire in a one-week window
Timezone resolution falls back to "treat as OFF" when unknown; a structured INFO log carries workspace_id + user_id and the absent-tz reason
END_OF_SPRINT consumes the workspace sprint calendar; the next-due-at is recomputed when sprint dates change (an integration test moves sprint-end and asserts the next due time tracks)
The next_due_at computation is the load-bearing piece: for WEEKLY it's the next occurrence of weekday at time_of_day in timezone after the previous send; for END_OF_SPRINT it's the next sprint-end + 1 day at time_of_day; for DAILY it's the next weekday at time_of_day. A pure function with property tests is cheaper than a stateful one.
The scheduler runs in the application-server JVM, not in a separate worker; the SKIP LOCKED row lock is the multi-instance coordination primitive. Spring Modulith Event Publication Registry from feat(server): Modulith Event Publication Registry as outbound outbox #1209 is not on the proactive path — it's the outbound-delivery outbox, downstream of the budget.
Workspaces without a sprint calendar treat END_OF_SPRINT as WEEKLY Monday with a structured WARN; the configuration UI surfaces this fallback to avoid silent surprise.
Part of #1204.
What ships
A
MentorReflectionSchedulerthat runs proactive reflection mentor sessions on a per-user cadence. The scheduler reads each user's preference row, resolves the next due time in the user's timezone, and triggers aSLACK_DM-surface mentor session through the same orchestrator + adapter the on-demand entry points use.Cadence options:
OFFWEEKLY(default)time_of_day+weekday.END_OF_SPRINTtime_of_day. Consumes the workspace's sprint calendar.DAILYtime_of_day. Tightened min-interval enforced by #1259.A new
user_reflection_preference(user_id, workspace_id, cadence, weekday, time_of_day, timezone, updated_at)table stores the configuration. Default for a new user in a workspace isWEEKLY, Monday, 09:00, with timezone resolved from the identity-link OIDCzoneinfoclaim (#1238). A user whose timezone cannot be resolved is treated asOFFuntil they configure it via #1262.The scheduler is a
@ScheduledSpring task running every 5 minutes. Each tick queries due users (next_due_at <= now()) with a SKIP LOCKED row lock so multiple instances do not double-fire. Each due user enqueues one reflection job through the shared notification budget from #1260; the job composes the three-phase reflection template (#1261) and the DM safety rails (#1259) before sending.Why
The scheduled cadence is the SDT-aligned proactive intervention the SRL literature recommends — the system asks the right question on a predictable rhythm the learner chose. Without per-user cadence + timezone resolution, the alternative is either spam (one cadence for all) or silence (no proactive reflection). The default of weekly Monday 09:00 matches the formative-feedback literature's "weekly retrospective" finding and is the cadence pilot workspaces converged on; the option set is small enough that the configuration UI from #1262 is one screen.
Acceptance criteria
user_reflection_preferencetable exists via Liquibase changeset with the columns above, a unique(user_id, workspace_id)index, and a CHECK constraint oncadenceenum valuescadence=WEEKLY, weekday=MONDAY, time_of_day=09:00, timezone=<resolved from OIDC or NULL>MentorReflectionSchedulerruns at a 5-minute cadence and uses a SKIP LOCKED query to fetch due users; an integration test runs the scheduler twice concurrently and asserts no user fires twice in one tickOFFis honored — no DMs fired; an integration test seeds three users withOFF/WEEKLY/DAILYand asserts only the latter two fire in a one-week windowworkspace_id+user_idand the absent-tz reasonEND_OF_SPRINTconsumes the workspace sprint calendar; the next-due-at is recomputed when sprint dates change (an integration test moves sprint-end and asserts the next due time tracks)DAILYuser with a 17 h gap since last reflection is rejected by the tighter 18 h min-interval from feat(server): DM safety rails for proactive mentor sends #1259Tests to write
MentorReflectionSchedulerIT— per-cadence happy path + skip-locked concurrency.UserReflectionPreferenceDefaultsTest— workspace-membership creation inserts the default row.TimezoneResolutionFallbackTest— absent OIDCzoneinfoclaim → treat-as-OFF behavior.EndOfSprintCadenceIT— sprint-end shift propagates tonext_due_at.Implementation notes
next_due_atcomputation is the load-bearing piece: forWEEKLYit's the next occurrence ofweekdayattime_of_dayintimezoneafter the previous send; forEND_OF_SPRINTit's the next sprint-end + 1 day attime_of_day; forDAILYit's the next weekday attime_of_day. A pure function with property tests is cheaper than a stateful one.END_OF_SPRINTasWEEKLYMonday with a structured WARN; the configuration UI surfaces this fallback to avoid silent surprise.Dependencies
Depends on #1257. Depends on #1238. Blocks #1259. Blocks #1261. Blocks #1262.