import uuid from datetime import datetime from sqlalchemy import DateTime, ForeignKey, Index, String, Text, text from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import Mapped, mapped_column, relationship from api.models.base import Base, new_uuid BROADCAST = "broadcast" MESSAGE_KINDS = ("message", "news", "standing") class AgentMessage(Base): __tablename__ = "agent_messages" __table_args__ = ( Index("ix_agent_messages_to_kind_archived", "to_agent", "kind", "archived_at"), ) id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), primary_key=True, default=new_uuid ) from_agent: Mapped[str] = mapped_column(String(100), nullable=False) to_agent: Mapped[str] = mapped_column(String(100), nullable=False, index=True) subject: Mapped[str] = mapped_column(String(500), nullable=False) body: Mapped[str] = mapped_column(Text, nullable=False) thread_id: Mapped[uuid.UUID | None] = mapped_column( UUID(as_uuid=True), ForeignKey("agent_messages.id", ondelete="SET NULL"), nullable=True, index=True, ) read_at: Mapped[datetime | None] = mapped_column( DateTime(timezone=True), nullable=True ) archived_at: Mapped[datetime | None] = mapped_column( DateTime(timezone=True), nullable=True ) # STATE-WP-0093: kind is only meaningful for broadcasts # (message | news | standing); direct messages stay "message". kind: Mapped[str] = mapped_column( String(20), nullable=False, default="message", server_default="message" ) expires_at: Mapped[datetime | None] = mapped_column( DateTime(timezone=True), nullable=True ) supersedes_id: Mapped[uuid.UUID | None] = mapped_column( UUID(as_uuid=True), ForeignKey("agent_messages.id", ondelete="SET NULL"), nullable=True, ) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=text("now()"), nullable=False, ) thread_root: Mapped["AgentMessage | None"] = relationship( "AgentMessage", remote_side="AgentMessage.id", foreign_keys=[thread_id], lazy="select", ) class MessageReceipt(Base): """Per-reader state for broadcast messages (STATE-WP-0093). Written only for broadcasts; direct messages keep the global ``AgentMessage.read_at``. """ __tablename__ = "message_receipts" message_id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), ForeignKey("agent_messages.id", ondelete="CASCADE"), primary_key=True, ) agent: Mapped[str] = mapped_column(String(100), primary_key=True, index=True) delivered_at: Mapped[datetime | None] = mapped_column( DateTime(timezone=True), nullable=True ) read_at: Mapped[datetime | None] = mapped_column( DateTime(timezone=True), nullable=True ) acknowledged_at: Mapped[datetime | None] = mapped_column( DateTime(timezone=True), nullable=True )