"""generation_tool_log: per-turn tool-call telemetry Revision ID: 0046 Revises: 0045 Create Date: 2026-05-21 Captures one row per assistant turn, recording which tools the model could have used, which it attempted, which fired successfully, and which failed (with error details). The empirical surface for evaluating model swaps and answering "does model X actually fire record_moment when it should?" without relying on anecdote. """ from alembic import op import sqlalchemy as sa from sqlalchemy.dialects.postgresql import ARRAY, JSONB revision = "0046" down_revision = "0045" branch_labels = None depends_on = None def upgrade() -> None: op.create_table( "generation_tool_log", 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="CASCADE"), nullable=False, ), # SET NULL (not CASCADE) so the telemetry row survives if the # underlying assistant message is later deleted — we want to keep # the per-turn outcome record for retrospective analysis. sa.Column( "assistant_message_id", sa.Integer, sa.ForeignKey("messages.id", ondelete="SET NULL"), nullable=True, ), sa.Column("model", sa.Text, nullable=False), sa.Column("think_enabled", sa.Boolean, nullable=False), sa.Column( "tools_available", ARRAY(sa.Text), nullable=False, server_default=sa.text("'{}'::text[]"), ), sa.Column( "tools_attempted", ARRAY(sa.Text), nullable=False, server_default=sa.text("'{}'::text[]"), ), sa.Column( "tools_succeeded", ARRAY(sa.Text), nullable=False, server_default=sa.text("'{}'::text[]"), ), # JSONB array of {name, error} objects so failed-tool details are # queryable without a separate failure table. sa.Column( "tools_failed", JSONB, nullable=False, server_default=sa.text("'[]'::jsonb"), ), sa.Column( "created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()"), ), ) # Common query: "recent tool outcomes for this user, filterable by model." op.create_index( "ix_generation_tool_log_user_created", "generation_tool_log", ["user_id", sa.text("created_at DESC")], ) # Per-conversation lookup for the journal page's own retrospection. op.create_index( "ix_generation_tool_log_conv", "generation_tool_log", ["conv_id"], ) def downgrade() -> None: op.drop_index("ix_generation_tool_log_conv", table_name="generation_tool_log") op.drop_index( "ix_generation_tool_log_user_created", table_name="generation_tool_log" ) op.drop_table("generation_tool_log")