"""pending_curator_actions — curator-proposed mutations awaiting user approval Revision ID: 0051 Revises: 0050 Create Date: 2026-05-23 Architecture: the curator runs unattended and can be confidently wrong. Additive operations (create_*, record_moment, log_work, save_person) land directly because adds are easily undone. Mutating operations (update_*, delete_*) are proposed to this table instead — the user sees them in a "Needs Review" panel and approves or rejects each one. A proposed action stores: - The action type (`update_task`, `delete_note`, etc.) — drives which handler gets re-executed on approval. - The target id + type for display ("update task 42", "delete note 17"). - The proposed arguments (`payload`) — what the curator wanted to do. - A snapshot of the target's state at proposal time (`current_snapshot`) — so the review UI can render a real before/after diff even if other work modified the entity since. - The status (`pending` / `approved` / `rejected`) and review timestamp. Approval replays the original tool call via execute_tool with authority="user" so the request bypasses the curator interceptor and just runs. """ from alembic import op import sqlalchemy as sa from sqlalchemy.dialects.postgresql import JSONB revision = "0051" down_revision = "0050" branch_labels = None depends_on = None def upgrade() -> None: op.create_table( "pending_curator_actions", sa.Column("id", sa.Integer, primary_key=True), sa.Column( "user_id", sa.Integer, sa.ForeignKey("users.id", ondelete="CASCADE"), nullable=False, ), sa.Column( "conv_id", sa.Integer, sa.ForeignKey("conversations.id", ondelete="SET NULL"), nullable=True, ), sa.Column("action_type", sa.Text, nullable=False), sa.Column("target_type", sa.Text, nullable=True), sa.Column("target_id", sa.Integer, nullable=True), sa.Column("target_label", sa.Text, nullable=True), sa.Column("payload", JSONB, nullable=False, server_default=sa.text("'{}'::jsonb")), sa.Column( "current_snapshot", JSONB, nullable=False, server_default=sa.text("'{}'::jsonb"), ), sa.Column( "status", sa.Text, nullable=False, server_default=sa.text("'pending'"), ), sa.Column( "created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()"), ), sa.Column("reviewed_at", sa.DateTime(timezone=True), nullable=True), sa.CheckConstraint( "status IN ('pending', 'approved', 'rejected')", name="pending_curator_actions_status_check", ), ) # Pending-only index — the Needs Review panel fetches "user's pending" # constantly, history rows just accumulate. op.create_index( "ix_pending_curator_actions_user_pending", "pending_curator_actions", ["user_id", sa.text("created_at DESC")], postgresql_where=sa.text("status = 'pending'"), ) def downgrade() -> None: op.drop_index( "ix_pending_curator_actions_user_pending", table_name="pending_curator_actions", ) op.drop_table("pending_curator_actions")