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
83 lines
3.2 KiB
Markdown
83 lines
3.2 KiB
Markdown
---
|
|
id: MAS-WP-0012
|
|
type: workplan
|
|
title: "One outbound path and one send cap"
|
|
domain: communication
|
|
repo: mail-address-semantics
|
|
status: finished
|
|
owner: claude
|
|
topic_slug: coulomb-social
|
|
created: "2026-10-19"
|
|
updated: "2026-10-19"
|
|
origin: residual
|
|
origin_ref: MAS-WP-0010
|
|
state_hub_workstream_id: "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
|
|
|
|
```task
|
|
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
|
|
|
|
```task
|
|
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).
|