mail-address-semantics/workplans/MAS-WP-0012-single-outbound-path.md
tegwick a5310dd63c Live MX redeployed with mas-live:v0.2-480e69a (one outbound path, v0.2 final); cap counter migrated 4/300
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
2026-10-19 13:37:09 +02:00

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 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.
  • Deployed 2026-10-19 with mas-live:v0.2-480e69a. The live counter was migrated: 4 of 300 (see MAS-WP-0007).