Files
roboco/alembic/versions/080_task_project_budgets.py
T

60 lines
2.2 KiB
Python
Raw Normal View History

"""Per-task and per-project cost budgets (feature-flagged, default off).
``tasks.budget_usd`` caps one task's own accumulated agent-spawn spend;
``projects.monthly_budget_usd`` caps a project's calendar-month spend across
all its tasks. Both nullable and additive — null means "no cap" and is a pure
no-op regardless of ``ROBOCO_TASK_BUDGETS_ENABLED`` (the flag itself gates
whether the caps are ever consulted at all). Mirrors the ``ci_watch_enabled``
per-project opt-in shape (migration 048): the column exists unconditionally,
the feature flag decides whether anything reads it.
``ix_agent_spawn_sessions_task_id`` rides along: both the claim-time monthly-
spend query (``TaskService.project_month_spend_usd``) and the orchestrator's
per-tick task-spend sweep (``_task_spend_usd``) filter this table by
``task_id`` with no existing index — an unbounded per-row scan every minute
once the flag is armed. Added here rather than a separate migration since it
exists to serve these same two new read paths.
Revision ID: 080_task_project_budgets
Revises: 079_notification_backoff
Create Date: 2026-07-22
NOTE: down_revision was re-chained from 078_project_codegen_command to
079_notification_backoff (PR #652 landed first at integration) — the
079_task_project_budgets -> 080_task_project_budgets rename + re-chain is
exactly the "may be re-chained at integration" case flagged in the original
docstring, not a fork of the tree.
"""
from __future__ import annotations
import sqlalchemy as sa
from alembic import op
revision = "080_task_project_budgets"
down_revision = "079_notification_backoff"
branch_labels: dict[str, str] | None = None
depends_on: dict[str, str] | None = None
def upgrade() -> None:
op.add_column(
"tasks",
sa.Column("budget_usd", sa.Float(), nullable=True),
)
op.add_column(
"projects",
sa.Column("monthly_budget_usd", sa.Float(), nullable=True),
)
op.create_index(
"ix_agent_spawn_sessions_task_id",
"agent_spawn_sessions",
["task_id"],
)
def downgrade() -> None:
op.drop_index("ix_agent_spawn_sessions_task_id", table_name="agent_spawn_sessions")
op.drop_column("projects", "monthly_budget_usd")
op.drop_column("tasks", "budget_usd")