MAS-WP-0012 finished: one outbound path and one send cap

mas_modes.mailer.Mailer is the only outbound path (own SQLite + lock),
shared by the ticket service and both EmailMode roles. Over the cap mail
is held and reported (send-cap-reached, held); held mail is purged after
email-held-duration-max (7d) and not stored beyond emails-held-storage-max
(100MB). The ticket service's legacy outbox is imported once so the live
count carries over (checked on a copy of the live DB: 4 sent). Entrypoint
passes the cap and both limits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 5787@bnt-lap001
Assistant-Session: 5db26b82-138b-40cc-9856-4d5eb0f752fd
This commit is contained in:
tegwick 2026-10-19 12:44:16 +02:00
parent bdf200a600
commit 8de8fddf37
10 changed files with 371 additions and 101 deletions

View file

@ -4,7 +4,7 @@ type: workplan
title: "One outbound path and one send cap"
domain: communication
repo: mail-address-semantics
status: proposed
status: finished
owner: claude
topic_slug: coulomb-social
created: "2026-10-19"
@ -28,7 +28,7 @@ total".
```task
id: MAS-WP-0012-T01
status: todo
status: done
priority: medium
state_hub_task_id: "5d9fefac-684d-5fe0-bead-f664952047e0"
```
@ -44,7 +44,7 @@ semantics.
```task
id: MAS-WP-0012-T02
status: todo
status: done
priority: medium
state_hub_task_id: "431cc528-cf9f-5991-8044-7cbab2ac0021"
```
@ -52,3 +52,32 @@ state_hub_task_id: "431cc528-cf9f-5991-8044-7cbab2ac0021"
Add a test that a deployment with both roles stops at exactly N sends in
total, and that the live entrypoint still passes `MAS_MAX_SENT` to the
single cap.
## Outcome
- **Founder interview (2026-10-19).** Over the cap, mail is **held and
reported**. Held mail is purged after `email-held-duration-max`
(default 7d) and not stored beyond `emails-held-storage-max` (default
100MB). The live counter is **migrated once at start**.
- **`mas_modes.mailer.Mailer`** is the one outbound path: its own SQLite
file and lock, principle 10, one lifetime cap, holding with a purge on
every send and `purge_held()`, a storage limit, `import_legacy()` with a
once-only marker, and `parse_duration` / `parse_size`.
- **Ticket service.** It sends through a Mailer it is given, or builds one.
`/outbox` shows the shared outbox (`ticket_id` = case ref). Its legacy
`outbox` table is imported once and then left alone. New CLI options:
`--outbox-db` (default `<db>-outbox.sqlite3`),
`--email-held-duration-max` and `--emails-held-storage-max`. Over the
cap the closed notice reports `notified: false`.
- **EmailMode** takes the shared Mailer. `mail.send` over the cap answers
`send-cap-reached` with `held`. CONTRACT.md §5 and §6 are updated.
- **Deploy.** The entrypoint passes `MAS_MAX_SENT` (300) and the two
hold limits (`MAS_EMAIL_HELD_DURATION_MAX`,
`MAS_EMAILS_HELD_STORAGE_MAX`). The migration was checked on a copy of
the live DB: 9 legacy rows, 4 sent, carried over; no second import on
restart.
- **Tests.** `tests/test_outbound.py` (15): exactly N sends across both
roles, purge by age, storage limit, sent mail never purged, the legacy
migration, parsers, entrypoint flags.
- **Not deployed.** The live MX still runs `v0.2-a95f848`; this takes
effect on the next redeploy.