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
{{ message }}
Repository navigation
Commit c6585bb
Browse filesBrowse the repository at this point in the historyBrowse files
feat(digest): per-recipient Slack delivery with UI prefs (PR3) (#105)
<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
## Summary
**Same Brain, different lens per role.**
This PR implements per-recipient Slack delivery for digests, consuming
the `DigestAssemblyResult` API from PR2. Two users with different
personas on the same connection now get different Slack digests for the
same time window.
### Honesty-gap fix (per-user cron)
UI schedule presets and `UserDigestPreference.cronExpression` (+
timezone) were stored but ignored: `SlackDailyDigestTaskConfig` only ran
the global `slack.daily-digest.cron` and then delivered to every
preference. Product Evangelist blocked marketing “your own schedule”
until this was fixed.
The scheduler now ticks every minute (`slack.daily-digest.tick-cron`,
default `0 * * * * *`) via `processDigestTick()`:
- **No enabled prefs** → legacy channel broadcast only when the global
cron is due (UTC)
- **Prefs exist** → each enabled `SLACK_DM` preference is evaluated with
Spring `CronExpression` in that user’s timezone; already-logged fire
windows are skipped (`SlackDigestLog` idempotency)
- Blank preference cron still falls back to `slack.daily-digest.cron`
- Manual/admin trigger still uses `sendDailyDigestHybrid()` (force all
recipients)
## Changes
### Backend
- **`SlackDailyDigestService`**: `sendPersonalizedDigests()` /
`sendDailyDigestHybrid()` / **`processDigestTick()`** — per-recipient
DMs, EXEC formatting, personalized logs, and per-user cron on the minute
tick
- **`DigestCronMatcher`**: Spring `CronExpression` due-window helper
(timezone-aware)
- **`DigestPreferenceSeedService`**: seed from singleton; persona
inference; skips inactive users
- **`SlackDailyDigestTaskConfig`**: minute tick → `processDigestTick()`
- **`DigestPreferenceController`**: seed/status; refuses EMAIL
### Frontend
- Digest preferences API + panel (persona, schedule presets, seed) +
DigestSection bell icon
### Documentation
- `docs/DIGEST_PREFERENCES.md` updated for tick vs global cron and
accurate “your own schedule”
## Acceptance Criteria
- [x] Two users, different roles/personas, same connection → different
Slack digests same window
- [x] Cron/config not singleton-only (**per-user cron honored on tick**)
- [x] Tests for per-recipient delivery + seed/legacy + due-cron /
timezone / idempotency
- [x] UI prefs for role/schedule/persona
- [x] Docs updated — “your own schedule” is accurate
- [x] No fake EMAIL / WhatsApp delivery advertised
## Out of Scope (Follow-ups)
1. Idle-in-transaction insight when Brain has data
2. `join_collapse_limit` cliffs under CONFIG_TUNING
3. DOCUMENTATION_GAPS miner
4. COST_CAPACITY dedicated cost miner for EXEC
5. Hermes/WhatsApp = PR4
## Dependencies
- PR1 #103: per-user digest prefs
- PR2 #104: role-aware insight assembler
## Testing
```bash
cd backend
./mvnw test -Dtest=PerRecipientDigestDeliveryTest,PerRecipientDigestCronSchedulingTest,DigestCronMatcherTest,DigestPreferenceSeedServiceTest,SlackDailyDigestServiceTest
```
Key scenarios: different crons → only due user; timezone
America/New_York 9AM = 13:00 UTC; idempotent window skip; EXEC 3
bullets; EMAIL skipped; seed idempotent; legacy when no enabled prefs.
<!-- CURSOR_AGENT_PR_BODY_END -->
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
0 commit comments