"""Move task tools into task backend resources without replaying existing tasks.""" from alembic import op import sqlalchemy as sa from sqlalchemy.dialects import postgresql revision = "20261023_0027" down_revision = "20261023_0026" branch_labels = None depends_on = None def upgrade(): op.create_table( "task_backend_resources", sa.Column("id", sa.String(40), primary_key=True), sa.Column("name", sa.String(128), nullable=False), sa.Column("type", sa.String(16), nullable=False), sa.Column("description", sa.String(2048), nullable=False), sa.Column("config", postgresql.JSONB(), nullable=False), sa.Column("secrets", postgresql.JSONB(), nullable=False), sa.Column("enabled", sa.Boolean(), nullable=False), sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False), sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False), sa.CheckConstraint("type IN ('reasoner', 'opencode')", name="ck_task_backend_type"), ) op.add_column("assistants", sa.Column("task_backend", postgresql.JSONB(), nullable=True)) op.add_column("background_tasks", sa.Column("backend_id", sa.String(40), nullable=False, server_default="")) op.add_column("background_tasks", sa.Column("backend_type", sa.String(16), nullable=False, server_default="opencode")) # NULL preserves pre-migration observation-window semantics for historical tasks. op.add_column("background_tasks", sa.Column("deadline_at", sa.String(40), nullable=True)) op.alter_column("background_tasks", "tool_id", server_default="") op.execute(""" DO $$ BEGIN IF EXISTS ( SELECT b.assistant_id FROM assistant_tool_bindings b JOIN tools t ON t.id=b.tool_id WHERE t.type='task' GROUP BY b.assistant_id HAVING count(*) > 1 ) THEN RAISE EXCEPTION 'Task backend migration: an assistant has multiple task bindings; resolve before retrying'; END IF; IF EXISTS ( SELECT 1 FROM assistant_tool_bindings b JOIN tools t ON t.id=b.tool_id JOIN assistants a ON a.id=b.assistant_id WHERE t.type='task' AND (a.type != 'prompt' OR a.runtime_mode != 'pipeline') ) THEN RAISE EXCEPTION 'Task backend migration: task bindings require Prompt Pipeline'; END IF; END $$ """) # Resource IDs may retain the old tool prefix: IDs are local to each resource table. op.execute(""" INSERT INTO task_backend_resources (id,name,type,description,config,secrets,enabled,created_at,updated_at) SELECT id,name,'opencode',description, jsonb_build_object( 'url',definition->'config'->'url', 'directory',definition->'config'->'directory', 'username',COALESCE(definition->'config'->'username','\"opencode\"'::jsonb), 'timeout_seconds',COALESCE(definition->'config'->'timeout_seconds',definition->'config'->'timeoutSeconds','600'::jsonb), 'max_concurrency',1,'instructions','','agent','','model_provider_id','','model_id','','skills_note','' ), secrets,status='active',created_at,updated_at FROM tools WHERE type='task' """) op.execute(""" UPDATE assistants a SET task_backend=jsonb_build_object( 'backend_id', b.tool_id, 'instruction_source',COALESCE( b.config->'argument_bindings'->'instruction', b.config->'argumentBindings'->'instruction', '{"source":"model"}'::jsonb ), 'instructions','' ) FROM assistant_tool_bindings b JOIN tools t ON t.id=b.tool_id WHERE a.id=b.assistant_id AND t.type='task' """) op.execute("UPDATE background_tasks SET backend_id=tool_id, backend_type='opencode'") op.execute("DELETE FROM assistant_tool_bindings WHERE tool_id IN (SELECT id FROM tools WHERE type='task')") op.execute("DELETE FROM tools WHERE type='task'") op.drop_constraint("ck_tools_external_type", "tools", type_="check") op.create_check_constraint("ck_tools_external_type", "tools", "type IN ('http', 'mcp', 'client')") def downgrade(): raise RuntimeError( "Task backend resources cannot be downgraded safely to task tools. " "Restore a pre-migration database backup to roll back this resource migration." )