Add Daily Digest plan; record render-loop invariants + deploy model in AGENTS.md
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
# Daily Digest — plan
|
||||
|
||||
Status: **proposed** (awaiting go-ahead). Captures the design agreed with Grant on
|
||||
2026-06-15. Build only after sign-off.
|
||||
|
||||
## Goal
|
||||
|
||||
An **opt-in** (off by default) daily "wake-up" email to recaps.cc users: the recaps
|
||||
added to their library in the last ~24 hours, each shown as a **synthesized 1–2
|
||||
paragraph overview** generated from that recap's existing per-topic summaries. Turns
|
||||
passive subscriptions into a daily touchpoint without making the user open the app.
|
||||
|
||||
## Decisions (locked 2026-06-15)
|
||||
|
||||
- **Content** — "overnight recaps": library additions since the user's last digest.
|
||||
- **Audience / opt-in** — multi-mode (recaps.cc) first; **off by default**; per-user toggle.
|
||||
- **Per-episode depth** — a 1–2 paragraph overview *synthesized from the stored topic
|
||||
summaries* (`chunks`). NOT raw full text (too long, Gmail clips >~102 KB), NOT a
|
||||
one-sentence blurb (too thin). This is Grant's call and it's what bounds email size.
|
||||
- **Volume** — per-episode size is bounded by the 2-paragraph synthesis. Still cap at
|
||||
~10 episodes per email with an "and N more in your library →" overflow link for
|
||||
extreme days.
|
||||
- **Cadence** — once per user per ~24h at a fixed server-time hour (default 08:00).
|
||||
Timezone-aware send is a v2. **Skip the email entirely when nothing is new.**
|
||||
- **Dedup** — a per-user `last_digest_at` watermark; each digest covers recaps created
|
||||
since that instant, so nothing repeats and nothing is missed.
|
||||
|
||||
## Data (grounded in code)
|
||||
|
||||
- Saved recap record (`server/history.js` `saveToHistory`): `id`, `title`, `type`,
|
||||
`url`, `createdAt` (ISO), `topicCount`, `chunks` (topics, each with bullet
|
||||
summaries), `entries` (transcript), `speakers`/`speakerNames`. **No top-level
|
||||
summary is stored** → the 1–2 paragraph overview must be synthesized.
|
||||
- Multi-mode users live in the `users` table (`id`, `email`, …); a user's library
|
||||
scope is their user id.
|
||||
|
||||
## Architecture
|
||||
|
||||
Mirror `server/subscription-reminders.js` (the proven daily-scan-plus-email pattern:
|
||||
self-gating, deduped, never throws).
|
||||
|
||||
- **`server/daily-digest.js`** (new)
|
||||
- `runDigestScan({ force })`: gate on `isSmtpReady()` + public URL set. For each
|
||||
opted-in user, list sessions with `createdAt > last_digest_at`; if none, skip. For
|
||||
each new recap, get-or-generate its overview (see below), render the email,
|
||||
`sendMail`, then advance the watermark. Returns a `{sent, skipped}` summary; never
|
||||
throws.
|
||||
- `startDigestScheduler()`: boot delay + interval, fires near the target hour.
|
||||
Idempotent; safe to start unconditionally in multi mode.
|
||||
- **Synthesis** — `synthesizeEpisodeOverview(record)`: send the recap's topic titles +
|
||||
bullet summaries to the relay LLM with a "write a 1–2 paragraph overview" prompt.
|
||||
**Cache** the result back onto the session JSON (e.g. `digestOverview`) so it's
|
||||
generated once and could later power an in-app episode overview. **Sanitize
|
||||
operator-internal strings at this boundary** (Parakeet/CUDA/LAN IPs etc. must not
|
||||
reach cloud users — existing repo convention).
|
||||
- **Email** — `renderDigestEmail({ brandName, episodes, manageUrl, unsubscribeUrl })`
|
||||
in `server/email-template.js`, matching the existing reminder/magic-link templates.
|
||||
- **Opt-in storage** — migration in `server/db.js`: add `users.digest_enabled`
|
||||
(default 0) and `users.last_digest_at` (ms, nullable). Toggle endpoint in
|
||||
`server/account-routes.js` (requires session). Settings-modal toggle in
|
||||
`public/index.html`.
|
||||
- **Unsubscribe** — a one-click tokenized GET link in every email that flips
|
||||
`digest_enabled = 0` without requiring login (signed token), plus the in-app toggle.
|
||||
Consent + deliverability hygiene on the young recaps.cc domain.
|
||||
- **Operator test trigger** — `POST /api/admin/digest/run { test_email }`, mirroring
|
||||
the reminders test hook, so it can be smoke-tested without waiting a day.
|
||||
|
||||
## Cost / credits
|
||||
|
||||
The synthesis is one small relay LLM call per new recap per opted-in user, run once and
|
||||
cached. Bounded by (opted-in users × new recaps/day). **Recommend operator-absorbed**
|
||||
(it's a retention feature, input is already-short topic summaries) rather than drawing
|
||||
the user's credits. Confirm.
|
||||
|
||||
## Open questions (defaults chosen; confirm or adjust)
|
||||
|
||||
1. **Synthesis cost owner** — operator-absorbed (default) vs user credits?
|
||||
2. **Send hour** — 08:00 server time (default)?
|
||||
3. **Single-mode operator digest** — defer to a follow-on (default: multi-mode only v1)?
|
||||
4. **Relay contract** — does an existing relay endpoint (`/relay/analyze`) fit the
|
||||
"summarize these topic summaries into 2 paragraphs" call, or is a small new relay
|
||||
capability/prompt-mode needed? If new, update `../recap-relay` + both repos'
|
||||
`AGENTS.md`/`ROADMAP.md` per the cross-repo rule. **Resolve before phase 2.**
|
||||
|
||||
## Build phases
|
||||
|
||||
1. Schema + opt-in toggle (migration, account endpoint, settings UI).
|
||||
2. Synthesis + cache (relay call + write-back + operator-string scrub). Resolve the
|
||||
relay-contract question first.
|
||||
3. Email template + scan loop + scheduler + watermark dedup + overflow cap.
|
||||
4. Operator test trigger.
|
||||
5. Tests — pure-function coverage (episode selection vs watermark, cap/overflow, empty
|
||||
→ skip), in the `subscription-reminders` test style.
|
||||
Reference in New Issue
Block a user