# Sprint 1.4 — Reply Rate Engine Probabilistic reply sampling so warmup mimics real mailbox engagement (~30% default, max 45%). ## What shipped - **`engaged_no_reply`** status on `WarmupStatus` enum - **`reply-rate.service.ts`** — `shouldReply()`, `getTodayReplyStats()`, simulation helper - **Campaign rescuer** samples replies; non-replied messages still get read/flagged/spam-rescue - **Hard daily cap** — skips reply when `actualReplies >= plannedReplies` - **Spam counters** — `spamDetected` / `spamRescued` increment on rescue - **`WarmupLog.placement`** — `inbox` or `spam` - **`POST /api/jobs/rescuer/run`** — manual rescuer trigger ## Algorithm Per [`docs/07-reply-rate-engine.md`](../07-reply-rate-engine.md): 1. If daily reply quota met → skip 2. If actual rate ≥ target → skip 3. Else probabilistic bias: `probability = needed / remaining` ## Manual QA ```bash # Start campaign, dispatch sends, then run rescuer repeatedly curl -X POST http://localhost:3000/api/jobs/dispatcher/run -H "x-api-key: $API_KEY" curl -X POST http://localhost:3000/api/jobs/rescuer/run -H "x-api-key: $API_KEY" # Check mix of complete vs replied logs curl "http://localhost:3000/api/logs?campaignId=" -H "x-api-key: $API_KEY" ``` Expect some logs with `engaged_no_reply` → `complete` and reply rate trending toward `replyRatePercent`. ## Key files - `src/services/reply-rate.service.ts` - `src/jobs/rescuer.job.ts`