state-hub/migrations/versions/d7e8f9a0b1c2_broadcast_receipts_and_notices.py

93 lines
3.1 KiB
Python
Raw Normal View History

"""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")