state-hub/migrations/versions/d7e8f9a0b1c2_broadcast_receipts_and_notices.py
tegwick ef541f58cf
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Build and Publish Multi-Context Image / build-and-push (push) Successful in 53s
STATE-WP-0093: per-recipient broadcast receipts and standing notices (T01-T06).
Founder approved T01 on 2026-09-22 (D2, D3, D6 as recommended).
- T02: message_receipts table; kind/expires_at/supersedes_id on
  agent_messages; migration d7e8f9a0b1c2 archives existing broadcasts,
  leaves direct messages untouched, reversible.
- T03: reader-aware mark-read (unattributed broadcast mark-read is a
  metered, deprecated no-op), delivery receipts on the scoped unread inbox,
  POST /messages/{id}/ack, news/standing kinds, expiry, supersede, broadcast
  archive no longer stamps read_at, reply writes the replier's receipt.
- T04 (state-hub part): Codex MCP reader param and acknowledge_notice;
  hub-core part handed off (message 69fc387c).
- T05: GET /messages/notices, standing_notices in /state/summary,
  dashboard standing-notices panel.
- T06: 17 new tests; full suite green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 63291@bnt-lap001
Assistant-Session: 8bd77868-ca68-4f49-bb1e-d539ecc0d703
2026-09-22 00:26:33 +02:00

92 lines
3.1 KiB
Python

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