"""per-recipient broadcast receipts and standing notices (STATE-WP-0093) Creates ``message_receipts`` (per-reader delivered/read/acknowledged state for broadcasts) and adds ``kind``, ``expires_at`` and ``supersedes_id`` to ``agent_messages``. Existing broadcasts are backfilled to ``kind='news'`` and, per the founder decision on D6 (2026-09-22), archived so that none of them re-surfaces once broadcast visibility stops depending on the global ``read_at``. Direct messages are not touched. Downgrade drops the table, index and columns. Archive stamps stay (they are reversible through the API and harmless under the old global read state). Revision ID: d7e8f9a0b1c2 Revises: c6f7a8b9d0e1 """ import sqlalchemy as sa from alembic import op from sqlalchemy.dialects import postgresql revision = "d7e8f9a0b1c2" down_revision = "c6f7a8b9d0e1" branch_labels = None depends_on = None def upgrade() -> None: op.add_column( "agent_messages", sa.Column( "kind", sa.String(length=20), nullable=False, server_default="message", ), ) op.add_column( "agent_messages", sa.Column("expires_at", sa.DateTime(timezone=True), nullable=True), ) op.add_column( "agent_messages", sa.Column( "supersedes_id", postgresql.UUID(as_uuid=True), sa.ForeignKey( "agent_messages.id", ondelete="SET NULL", name="fk_agent_messages_supersedes_id", ), nullable=True, ), ) op.create_index( "ix_agent_messages_to_kind_archived", "agent_messages", ["to_agent", "kind", "archived_at"], ) op.create_table( "message_receipts", sa.Column( "message_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("agent_messages.id", ondelete="CASCADE"), primary_key=True, ), sa.Column("agent", sa.String(length=100), primary_key=True), sa.Column("delivered_at", sa.DateTime(timezone=True), nullable=True), sa.Column("read_at", sa.DateTime(timezone=True), nullable=True), sa.Column("acknowledged_at", sa.DateTime(timezone=True), nullable=True), ) op.create_index("ix_message_receipts_agent", "message_receipts", ["agent"]) # D6: existing broadcasts become news and are archived (only broadcasts). op.execute( "UPDATE agent_messages SET kind = 'news' WHERE to_agent = 'broadcast'" ) op.execute( "UPDATE agent_messages SET archived_at = now() " "WHERE to_agent = 'broadcast' AND archived_at IS NULL" ) def downgrade() -> None: op.drop_index("ix_message_receipts_agent", table_name="message_receipts") op.drop_table("message_receipts") op.drop_index("ix_agent_messages_to_kind_archived", table_name="agent_messages") op.drop_constraint( "fk_agent_messages_supersedes_id", "agent_messages", type_="foreignkey" ) op.drop_column("agent_messages", "supersedes_id") op.drop_column("agent_messages", "expires_at") op.drop_column("agent_messages", "kind")