Files
bvandeusenandClaude Opus 5.5 5237ffc663
CI / lint (push) Successful in 3s
CI / integration (push) Failing after 1m13s
CI / unit (push) Successful in 1m25s
CI / publish (push) Skipped
feat(alerts): alert email waits 2 minutes in the outbox and each alert records its outcome (#5507)
#5507 "Alert emails through the outbox, gathered for 2 minutes"
(milestone 501). Replaces the in-memory core/alert_burst.py.

- outbox.hold_alert: an alert joins each recipient's held alert email, or
  opens one due 2 minutes later (System 156 ruling). A Postgres advisory
  lock keeps two alerts at once from opening two windows for one reader.
- email_outbox_events.alert_json (migration 0043) keeps the alert as it
  fired, so the email written when the window closes says what it said.
- write_held writes it then, in the reader's zone: the usual alert email
  for one alert, the list for several (outbox.alert_words).
- after_settle records on each AlertEvent, once every email carrying it
  has settled: email_sent, or email_error naming each reader it didn't
  reach and the server's reason.
- A held email survives a restart and goes out when its window closes.
- A refusal of a message's one recipient no longer repeats the address.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 15:59:17 -04:00

27 lines
882 B
Python

"""The alert each queued email carries, as it was when it fired (#5507), so
an email written when its 2-minute window closes says what the alert said.
Revision ID: 0043_outbox_alert_words
Revises: 0042_email_outbox
Create Date: 2026-10-09
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
revision: str = "0043_outbox_alert_words"
down_revision: Union[str, None] = "0042_email_outbox"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# No row links an alert yet (#5506 shipped the table unused), so the
# empty default is only for the ALTER.
op.add_column("email_outbox_events",
sa.Column("alert_json", sa.Text, nullable=False, server_default="{}"))
def downgrade() -> None:
op.drop_column("email_outbox_events", "alert_json")