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
3.2 KiB
| id | type | title | domain | repo | status | owner | topic_slug | created | updated | origin | origin_ref | state_hub_workstream_id |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| MAS-WP-0012 | workplan | One outbound path and one send cap | communication | mail-address-semantics | finished | claude | coulomb-social | 2026-10-19 | 2026-10-19 | residual | MAS-WP-0010 | be2db53f-9acc-5bf0-a6dc-fb99cbc8e46b |
One outbound path and one send cap
Risk C in history/2026-10-19T090725Z-scope-vs-intent.md. With both
roles in one deployment, the ticket service and mas_modes.mailer each
keep their own outbox and cap. Total outbound can then reach about twice
the configured cap. They also act differently over the cap: the ticket
service keeps replies unsent, while the mailer refuses with
send-cap-reached. Principle 10 says "caps how much it can send in
total".
Route all mail through the mailer
id: MAS-WP-0012-T01
status: done
priority: medium
state_hub_task_id: "5d9fefac-684d-5fe0-bead-f664952047e0"
Make mas_modes.mailer.Mailer the only outbound path. The ticket service
then sends through a Mailer it is given, sharing the DB or an outbox
service, instead of its own outbox table and cap. Settle one behavior
over the cap, and record it in CONTRACT.md §6. Keep the ticket service's
/outbox inspection working, and keep the deployment's --max-sent
semantics.
Test the shared cap
id: MAS-WP-0012-T02
status: done
priority: medium
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 beyondemails-held-storage-max(default 100MB). The live counter is migrated once at start. mas_modes.mailer.Maileris the one outbound path: its own SQLite file and lock, principle 10, one lifetime cap, holding with a purge on every send andpurge_held(), a storage limit,import_legacy()with a once-only marker, andparse_duration/parse_size.- Ticket service. It sends through a Mailer it is given, or builds one.
/outboxshows the shared outbox (ticket_id= case ref). Its legacyoutboxtable is imported once and then left alone. New CLI options:--outbox-db(default<db>-outbox.sqlite3),--email-held-duration-maxand--emails-held-storage-max. Over the cap the closed notice reportsnotified: false. - EmailMode takes the shared Mailer.
mail.sendover the cap answerssend-cap-reachedwithheld. 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.