feat(my-deepagent): v0.1.0 Step 0~5 — scaffolding through deepagent + OpenRouter
Python rewrite of the agent harness on top of deepagents 0.6.1 + langchain 1.x, replacing the abandoned TS attempt in packages/. 388 unit/integration tests pass. Steps ----- 0. Scaffolding — uv workspace, ruff/mypy/pre-commit/alembic, src/tests/docs trees with docs/schemas/ seeded from my-deepagent-seed/. 1. Core — config (pydantic-settings with MYDEEPAGENT_ env prefix and TOML source), enums (Backend, Capability, RiskLevel, ApprovalDecisionAction, ApprovalState, RunState, RunPhaseState, SessionState, ErrorClass), errors (MyDeepAgentError + BudgetExhaustedError with PEP-3134 cause + context suppression), hash (canonical JSON + sha256). 2. Persona/Workflow/Binding — pydantic v2 schemas with tuple-based deep immutability (post-construction hash drift prevented), YAML loaders, deterministic auto-select (preferred_backends → version → name → hash), override resolution with ineligibility diagnostics, PersonaConsentStore with fcntl.flock + tmp+fsync+rename atomic write. 3. Artifact schema registry — Draft202012Validator, multi-root resolution, structured ValidationFinding output. 4. Persistence — 18 SQLAlchemy 2.0 async ORM models with FK CASCADE/RESTRICT, WAL + busy_timeout + foreign_keys PRAGMA, alembic baseline + ux_active_run_repo_base partial unique index, LangGraph SqliteSaver as context manager only (lifecycle safety). 5. DeepAgent session — build_agent wires Persona → create_deep_agent with LocalShellBackend / FilesystemBackend / StateBackend / CompositeBackend, ChatOpenAI(base_url=openrouter) for openrouter: model strings, and 4 middleware classes (cost / audit-tool / safety-shell / fallback-model). Critical workarounds -------------------- - deepagents 0.6.1 rejects FilesystemPermission together with backends that implement SandboxBackendProtocol (LocalShellBackend). SafetyShellMiddleware enforces destructive-command and secret-path policy at the tool layer instead, and build_agent strips the permissions kwarg when the persona's deepagents_backend is local_shell. - FilesystemOperation in deepagents is Literal['read', 'write'] only; _map_operations collapses our richer schema (read/write/edit/ls) safely. Real OpenRouter smoke --------------------- test_openrouter_deepagents_local_shell_smoke calls DeepSeek via deepagents + LocalShellBackend + SafetyShellMiddleware end-to-end. PASS, ~$0.000001 cost, input=9 / output=1 tokens with content "OK". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1
my-deepagent/alembic/README
Normal file
1
my-deepagent/alembic/README
Normal file
@@ -0,0 +1 @@
|
||||
Generic single-database configuration.
|
||||
83
my-deepagent/alembic/env.py
Normal file
83
my-deepagent/alembic/env.py
Normal file
@@ -0,0 +1,83 @@
|
||||
import os
|
||||
from logging.config import fileConfig
|
||||
|
||||
from sqlalchemy import engine_from_config, pool
|
||||
|
||||
from alembic import context
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
config = context.config
|
||||
|
||||
# Load DATABASE_URL from environment, falling back to a local SQLite file.
|
||||
# Alembic uses synchronous SQLAlchemy, so strip the async driver prefix when
|
||||
# present (sqlite+aiosqlite:// → sqlite://).
|
||||
_raw_url: str = os.environ.get("DATABASE_URL", "sqlite:///./database.sqlite3")
|
||||
_sync_url: str = _raw_url.replace("sqlite+aiosqlite://", "sqlite://")
|
||||
config.set_main_option("sqlalchemy.url", _sync_url)
|
||||
|
||||
# Interpret the config file for Python logging.
|
||||
# This line sets up loggers basically.
|
||||
if config.config_file_name is not None:
|
||||
fileConfig(config.config_file_name)
|
||||
|
||||
# add your model's MetaData object here
|
||||
# for 'autogenerate' support
|
||||
from my_deepagent.persistence.models import Base # noqa: E402
|
||||
|
||||
target_metadata = Base.metadata
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
# can be acquired:
|
||||
# my_important_option = config.get_main_option("my_important_option")
|
||||
# ... etc.
|
||||
|
||||
|
||||
def run_migrations_offline() -> None:
|
||||
"""Run migrations in 'offline' mode.
|
||||
|
||||
This configures the context with just a URL
|
||||
and not an Engine, though an Engine is acceptable
|
||||
here as well. By skipping the Engine creation
|
||||
we don't even need a DBAPI to be available.
|
||||
|
||||
Calls to context.execute() here emit the given string to the
|
||||
script output.
|
||||
|
||||
"""
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(
|
||||
url=url,
|
||||
target_metadata=target_metadata,
|
||||
literal_binds=True,
|
||||
dialect_opts={"paramstyle": "named"},
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def run_migrations_online() -> None:
|
||||
"""Run migrations in 'online' mode.
|
||||
|
||||
In this scenario we need to create an Engine
|
||||
and associate a connection with the context.
|
||||
|
||||
"""
|
||||
connectable = engine_from_config(
|
||||
config.get_section(config.config_ini_section, {}),
|
||||
prefix="sqlalchemy.",
|
||||
poolclass=pool.NullPool,
|
||||
)
|
||||
|
||||
with connectable.connect() as connection:
|
||||
context.configure(connection=connection, target_metadata=target_metadata)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
||||
28
my-deepagent/alembic/script.py.mako
Normal file
28
my-deepagent/alembic/script.py.mako
Normal file
@@ -0,0 +1,28 @@
|
||||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = ${repr(up_revision)}
|
||||
down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)}
|
||||
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
||||
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
${downgrades if downgrades else "pass"}
|
||||
@@ -0,0 +1,303 @@
|
||||
"""baseline schema for v0.1.0
|
||||
|
||||
Revision ID: 79945fdc2649
|
||||
Revises:
|
||||
Create Date: 2026-05-15 17:19:09.577439
|
||||
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "79945fdc2649"
|
||||
down_revision: str | Sequence[str] | None = None
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table(
|
||||
"agent_personas",
|
||||
sa.Column("id", sa.String(length=36), nullable=False),
|
||||
sa.Column("name", sa.Text(), nullable=False),
|
||||
sa.Column("version", sa.Integer(), nullable=False),
|
||||
sa.Column("hash", sa.Text(), nullable=False),
|
||||
sa.Column("definition", sa.JSON(), nullable=False),
|
||||
sa.Column("created_at", sa.Text(), nullable=False),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("hash"),
|
||||
)
|
||||
op.create_table(
|
||||
"budget_ledger",
|
||||
sa.Column("scope", sa.Text(), nullable=False),
|
||||
sa.Column("spent_usd", sa.Float(), nullable=False),
|
||||
sa.Column("cap_usd", sa.Float(), nullable=True),
|
||||
sa.Column("last_updated", sa.Text(), nullable=False),
|
||||
sa.PrimaryKeyConstraint("scope"),
|
||||
)
|
||||
op.create_table(
|
||||
"interactive_sessions",
|
||||
sa.Column("id", sa.String(length=36), nullable=False),
|
||||
sa.Column("persona_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("persona_hash", sa.Text(), nullable=False),
|
||||
sa.Column("started_at", sa.Text(), nullable=True),
|
||||
sa.Column("ended_at", sa.Text(), nullable=True),
|
||||
sa.Column("last_message_at", sa.Text(), nullable=True),
|
||||
sa.Column("state", sa.Text(), nullable=False),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_table(
|
||||
"llm_calls",
|
||||
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
||||
sa.Column("run_id", sa.String(length=36), nullable=True),
|
||||
sa.Column("phase_id", sa.String(length=36), nullable=True),
|
||||
sa.Column("interactive_session_id", sa.String(length=36), nullable=True),
|
||||
sa.Column("thread_id", sa.Text(), nullable=False),
|
||||
sa.Column("persona_name", sa.Text(), nullable=False),
|
||||
sa.Column("persona_version", sa.Integer(), nullable=False),
|
||||
sa.Column("model", sa.Text(), nullable=False),
|
||||
sa.Column("role", sa.Text(), nullable=False),
|
||||
sa.Column("turn_index", sa.Integer(), nullable=False),
|
||||
sa.Column("input_tokens", sa.Integer(), nullable=False),
|
||||
sa.Column("output_tokens", sa.Integer(), nullable=False),
|
||||
sa.Column("cached_tokens", sa.Integer(), nullable=False),
|
||||
sa.Column("reasoning_tokens", sa.Integer(), nullable=False),
|
||||
sa.Column("cost_usd_input", sa.Float(), nullable=False),
|
||||
sa.Column("cost_usd_output", sa.Float(), nullable=False),
|
||||
sa.Column("cost_usd_total", sa.Float(), nullable=False),
|
||||
sa.Column("latency_ms", sa.Integer(), nullable=False),
|
||||
sa.Column("status", sa.Text(), nullable=False),
|
||||
sa.Column("error_code", sa.Text(), nullable=True),
|
||||
sa.Column("request_id", sa.Text(), nullable=True),
|
||||
sa.Column("ts", sa.Text(), nullable=False),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index(
|
||||
"llm_calls_interactive_session_id_ts_idx",
|
||||
"llm_calls",
|
||||
["interactive_session_id", "ts"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index("llm_calls_model_ts_idx", "llm_calls", ["model", "ts"], unique=False)
|
||||
op.create_index("llm_calls_run_id_ts_idx", "llm_calls", ["run_id", "ts"], unique=False)
|
||||
op.create_table(
|
||||
"model_pricing",
|
||||
sa.Column("model", sa.Text(), nullable=False),
|
||||
sa.Column("input_per_1k_usd", sa.Float(), nullable=False),
|
||||
sa.Column("output_per_1k_usd", sa.Float(), nullable=False),
|
||||
sa.Column("context_length", sa.Integer(), nullable=False),
|
||||
sa.Column("fetched_at", sa.Text(), nullable=False),
|
||||
sa.Column("raw_payload", sa.Text(), nullable=False),
|
||||
sa.PrimaryKeyConstraint("model"),
|
||||
)
|
||||
op.create_table(
|
||||
"persona_consents",
|
||||
sa.Column("persona_hash", sa.Text(), nullable=False),
|
||||
sa.Column("persona_name", sa.Text(), nullable=False),
|
||||
sa.Column("persona_version", sa.Integer(), nullable=False),
|
||||
sa.Column("decision", sa.Text(), nullable=False),
|
||||
sa.Column("decided_at", sa.Text(), nullable=False),
|
||||
sa.PrimaryKeyConstraint("persona_hash"),
|
||||
)
|
||||
op.create_table(
|
||||
"phase_feedback",
|
||||
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
||||
sa.Column("run_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("phase_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("reaction", sa.Text(), nullable=True),
|
||||
sa.Column("comment", sa.Text(), nullable=True),
|
||||
sa.Column("created_at", sa.Text(), nullable=False),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_table(
|
||||
"runs",
|
||||
sa.Column("id", sa.String(length=36), nullable=False),
|
||||
sa.Column("template_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("template_hash", sa.Text(), nullable=False),
|
||||
sa.Column("state", sa.Text(), nullable=False),
|
||||
sa.Column("repo_path", sa.Text(), nullable=False),
|
||||
sa.Column("base_branch", sa.Text(), nullable=False),
|
||||
sa.Column("worktree_root", sa.Text(), nullable=False),
|
||||
sa.Column("current_phase_id", sa.String(length=36), nullable=True),
|
||||
sa.Column("started_at", sa.Text(), nullable=True),
|
||||
sa.Column("ended_at", sa.Text(), nullable=True),
|
||||
sa.Column("final_report_path", sa.Text(), nullable=True),
|
||||
sa.Column("paused_from_state", sa.Text(), nullable=True),
|
||||
sa.Column("created_at", sa.Text(), nullable=False),
|
||||
sa.Column("updated_at", sa.Text(), nullable=False),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_table(
|
||||
"tool_calls",
|
||||
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
||||
sa.Column("run_id", sa.String(length=36), nullable=True),
|
||||
sa.Column("phase_id", sa.String(length=36), nullable=True),
|
||||
sa.Column("interactive_session_id", sa.String(length=36), nullable=True),
|
||||
sa.Column("tool_name", sa.Text(), nullable=False),
|
||||
sa.Column("args", sa.JSON(), nullable=False),
|
||||
sa.Column("result", sa.JSON(), nullable=True),
|
||||
sa.Column("error", sa.Text(), nullable=True),
|
||||
sa.Column("duration_ms", sa.Integer(), nullable=False),
|
||||
sa.Column("ts", sa.Text(), nullable=False),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index("tool_calls_run_id_ts_idx", "tool_calls", ["run_id", "ts"], unique=False)
|
||||
op.create_table(
|
||||
"workflow_templates",
|
||||
sa.Column("id", sa.String(length=36), nullable=False),
|
||||
sa.Column("name", sa.Text(), nullable=False),
|
||||
sa.Column("version", sa.Integer(), nullable=False),
|
||||
sa.Column("hash", sa.Text(), nullable=False),
|
||||
sa.Column("definition", sa.JSON(), nullable=False),
|
||||
sa.Column("created_at", sa.Text(), nullable=False),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("hash"),
|
||||
)
|
||||
op.create_table(
|
||||
"approval_requests",
|
||||
sa.Column("id", sa.String(length=36), nullable=False),
|
||||
sa.Column("run_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("phase_id", sa.String(length=36), nullable=True),
|
||||
sa.Column("gate_key", sa.Text(), nullable=False),
|
||||
sa.Column("state", sa.Text(), nullable=False),
|
||||
sa.Column("idempotency_key", sa.Text(), nullable=False),
|
||||
sa.Column("payload", sa.JSON(), nullable=False),
|
||||
sa.Column("created_at", sa.Text(), nullable=False),
|
||||
sa.Column("resolved_at", sa.Text(), nullable=True),
|
||||
sa.ForeignKeyConstraint(["run_id"], ["runs.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("idempotency_key"),
|
||||
)
|
||||
op.create_table(
|
||||
"artifacts",
|
||||
sa.Column("id", sa.String(length=36), nullable=False),
|
||||
sa.Column("run_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("phase_id", sa.String(length=36), nullable=True),
|
||||
sa.Column("path", sa.Text(), nullable=False),
|
||||
sa.Column("schema_id", sa.Text(), nullable=False),
|
||||
sa.Column("hash", sa.Text(), nullable=False),
|
||||
sa.Column("valid", sa.Boolean(), nullable=False),
|
||||
sa.Column("validation_error", sa.JSON(), nullable=True),
|
||||
sa.Column("created_at", sa.Text(), nullable=False),
|
||||
sa.ForeignKeyConstraint(["run_id"], ["runs.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("run_id", "path", "hash", name="uq_artifacts_run_path_hash"),
|
||||
)
|
||||
op.create_table(
|
||||
"run_bindings",
|
||||
sa.Column("id", sa.String(length=36), nullable=False),
|
||||
sa.Column("run_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("role_id", sa.Text(), nullable=False),
|
||||
sa.Column("persona_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("persona_hash", sa.Text(), nullable=False),
|
||||
sa.Column("backend", sa.Text(), nullable=False),
|
||||
sa.Column("binding_hash", sa.Text(), nullable=False),
|
||||
sa.ForeignKeyConstraint(["run_id"], ["runs.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("run_id", "role_id", name="uq_run_bindings_run_role"),
|
||||
)
|
||||
op.create_table(
|
||||
"run_commands",
|
||||
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
||||
sa.Column("run_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("command", sa.Text(), nullable=False),
|
||||
sa.Column("payload", sa.JSON(), nullable=False),
|
||||
sa.Column("idempotency_key", sa.Text(), nullable=False),
|
||||
sa.Column("created_at", sa.Text(), nullable=False),
|
||||
sa.Column("processed_at", sa.Text(), nullable=True),
|
||||
sa.ForeignKeyConstraint(["run_id"], ["runs.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("idempotency_key"),
|
||||
)
|
||||
op.create_table(
|
||||
"run_events",
|
||||
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
||||
sa.Column("run_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("phase_id", sa.String(length=36), nullable=True),
|
||||
sa.Column("seq", sa.Integer(), nullable=False),
|
||||
sa.Column("type", sa.Text(), nullable=False),
|
||||
sa.Column("payload", sa.JSON(), nullable=False),
|
||||
sa.Column("idempotency_key", sa.Text(), nullable=False),
|
||||
sa.Column("ts", sa.Text(), nullable=False),
|
||||
sa.ForeignKeyConstraint(["run_id"], ["runs.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("run_id", "idempotency_key", name="uq_run_events_run_idempotency"),
|
||||
sa.UniqueConstraint("run_id", "seq", name="uq_run_events_run_seq"),
|
||||
)
|
||||
op.create_index("run_events_run_id_ts_idx", "run_events", ["run_id", "ts"], unique=False)
|
||||
op.create_table(
|
||||
"run_inputs",
|
||||
sa.Column("id", sa.String(length=36), nullable=False),
|
||||
sa.Column("run_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("requirements_md", sa.Text(), nullable=False),
|
||||
sa.Column("objective", sa.JSON(), nullable=False),
|
||||
sa.Column("extra", sa.JSON(), nullable=False),
|
||||
sa.Column("input_hash", sa.Text(), nullable=False),
|
||||
sa.ForeignKeyConstraint(["run_id"], ["runs.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("run_id"),
|
||||
)
|
||||
op.create_table(
|
||||
"run_phases",
|
||||
sa.Column("id", sa.String(length=36), nullable=False),
|
||||
sa.Column("run_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("phase_key", sa.Text(), nullable=False),
|
||||
sa.Column("seq", sa.Integer(), nullable=False),
|
||||
sa.Column("state", sa.Text(), nullable=False),
|
||||
sa.Column("attempts", sa.Integer(), nullable=False),
|
||||
sa.Column("started_at", sa.Text(), nullable=True),
|
||||
sa.Column("ended_at", sa.Text(), nullable=True),
|
||||
sa.ForeignKeyConstraint(["run_id"], ["runs.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("run_id", "phase_key", name="uq_run_phases_run_phase"),
|
||||
)
|
||||
op.create_table(
|
||||
"approval_decisions",
|
||||
sa.Column("id", sa.String(length=36), nullable=False),
|
||||
sa.Column("approval_request_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("action", sa.Text(), nullable=False),
|
||||
sa.Column("comment", sa.Text(), nullable=True),
|
||||
sa.Column("decided_at", sa.Text(), nullable=False),
|
||||
sa.Column("idempotency_key", sa.Text(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["approval_request_id"], ["approval_requests.id"], ondelete="CASCADE"
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("idempotency_key"),
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table("approval_decisions")
|
||||
op.drop_table("run_phases")
|
||||
op.drop_table("run_inputs")
|
||||
op.drop_index("run_events_run_id_ts_idx", table_name="run_events")
|
||||
op.drop_table("run_events")
|
||||
op.drop_table("run_commands")
|
||||
op.drop_table("run_bindings")
|
||||
op.drop_table("artifacts")
|
||||
op.drop_table("approval_requests")
|
||||
op.drop_table("workflow_templates")
|
||||
op.drop_index("tool_calls_run_id_ts_idx", table_name="tool_calls")
|
||||
op.drop_table("tool_calls")
|
||||
op.drop_table("runs")
|
||||
op.drop_table("phase_feedback")
|
||||
op.drop_table("persona_consents")
|
||||
op.drop_table("model_pricing")
|
||||
op.drop_index("llm_calls_run_id_ts_idx", table_name="llm_calls")
|
||||
op.drop_index("llm_calls_model_ts_idx", table_name="llm_calls")
|
||||
op.drop_index("llm_calls_interactive_session_id_ts_idx", table_name="llm_calls")
|
||||
op.drop_table("llm_calls")
|
||||
op.drop_table("interactive_sessions")
|
||||
op.drop_table("budget_ledger")
|
||||
op.drop_table("agent_personas")
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,638 @@
|
||||
"""add active-run partial unique index and FK constraints
|
||||
|
||||
Revision ID: 839f2233e346
|
||||
Revises: 79945fdc2649
|
||||
Create Date: 2026-05-15 18:51:14.343577
|
||||
|
||||
Notes:
|
||||
- P0-1: Adds partial unique index ux_active_run_repo_base on runs(repo_path, base_branch)
|
||||
WHERE state NOT IN ('completed', 'failed', 'aborted'). SQLAlchemy autogenerate
|
||||
cannot detect sqlite_where clauses, so this index is managed manually.
|
||||
- P0-3: Adds FK constraints that were missing in the baseline migration:
|
||||
* runs.template_id -> workflow_templates.id RESTRICT
|
||||
* run_bindings.persona_id -> agent_personas.id RESTRICT
|
||||
* interactive_sessions.persona_id -> agent_personas.id RESTRICT
|
||||
* run_events.phase_id -> run_phases.id CASCADE
|
||||
* approval_requests.phase_id -> run_phases.id CASCADE
|
||||
* artifacts.phase_id -> run_phases.id CASCADE
|
||||
* tool_calls.run_id -> runs.id CASCADE
|
||||
* tool_calls.phase_id -> run_phases.id CASCADE
|
||||
* tool_calls.interactive_session_id -> interactive_sessions.id CASCADE
|
||||
* llm_calls.run_id -> runs.id CASCADE
|
||||
* llm_calls.phase_id -> run_phases.id CASCADE
|
||||
* llm_calls.interactive_session_id -> interactive_sessions.id CASCADE
|
||||
* phase_feedback.run_id -> runs.id CASCADE
|
||||
* phase_feedback.phase_id -> run_phases.id CASCADE
|
||||
- runs.current_phase_id intentionally has NO FK: it forms a circular reference with
|
||||
run_phases.run_id. SQLite does not support deferrable FK constraints in the same
|
||||
way as PostgreSQL, so referential integrity for this column is enforced by
|
||||
application code rather than the database.
|
||||
- SQLite does not support ADD CONSTRAINT via ALTER TABLE. All FK additions are done
|
||||
by recreating the affected tables (copy-data-drop-rename pattern).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "839f2233e346"
|
||||
down_revision: str | Sequence[str] | None = "79945fdc2649"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema.
|
||||
|
||||
SQLite does not support ALTER TABLE ... ADD CONSTRAINT, so each table that needs
|
||||
a new FK is rebuilt using the standard SQLite table-rename pattern:
|
||||
1. Disable FK enforcement during rebuild (PRAGMA foreign_keys=OFF).
|
||||
2. Create new table with correct FK constraints.
|
||||
3. Copy data from old table.
|
||||
4. Drop old table.
|
||||
5. Rename new table to original name.
|
||||
6. Re-enable FK enforcement (PRAGMA foreign_keys=ON).
|
||||
|
||||
Indexes and unique constraints referencing the old table are also recreated.
|
||||
"""
|
||||
# Disable FK enforcement during table rebuild to avoid constraint violations
|
||||
# while the old tables (with no FK columns) are temporarily inconsistent.
|
||||
op.execute("PRAGMA foreign_keys=OFF")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# runs: add template_id FK (RESTRICT) + P0-1 partial unique index.
|
||||
# Rebuild because SQLite cannot ADD CONSTRAINT.
|
||||
# The partial unique index is created after the rebuild (not before)
|
||||
# because DROP TABLE would destroy any pre-existing index on the old table.
|
||||
# ------------------------------------------------------------------
|
||||
op.execute(
|
||||
"""
|
||||
CREATE TABLE runs_new (
|
||||
id TEXT NOT NULL,
|
||||
template_id TEXT NOT NULL
|
||||
REFERENCES workflow_templates (id) ON DELETE RESTRICT,
|
||||
template_hash TEXT NOT NULL,
|
||||
state TEXT NOT NULL,
|
||||
repo_path TEXT NOT NULL,
|
||||
base_branch TEXT NOT NULL,
|
||||
worktree_root TEXT NOT NULL,
|
||||
current_phase_id TEXT,
|
||||
started_at TEXT,
|
||||
ended_at TEXT,
|
||||
final_report_path TEXT,
|
||||
paused_from_state TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
PRIMARY KEY (id)
|
||||
)
|
||||
"""
|
||||
)
|
||||
op.execute(
|
||||
"INSERT INTO runs_new SELECT id, template_id, template_hash, state, "
|
||||
"repo_path, base_branch, worktree_root, current_phase_id, "
|
||||
"started_at, ended_at, final_report_path, paused_from_state, "
|
||||
"created_at, updated_at FROM runs"
|
||||
)
|
||||
op.execute("DROP TABLE runs")
|
||||
op.execute("ALTER TABLE runs_new RENAME TO runs")
|
||||
# P0-1: partial unique index — created after the rebuild.
|
||||
op.execute(
|
||||
"CREATE UNIQUE INDEX ux_active_run_repo_base "
|
||||
"ON runs (repo_path, base_branch) "
|
||||
"WHERE state NOT IN ('completed', 'failed', 'aborted')"
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# run_bindings: add persona_id FK (RESTRICT)
|
||||
# ------------------------------------------------------------------
|
||||
op.execute(
|
||||
"""
|
||||
CREATE TABLE run_bindings_new (
|
||||
id TEXT NOT NULL,
|
||||
run_id TEXT NOT NULL
|
||||
REFERENCES runs (id) ON DELETE CASCADE,
|
||||
role_id TEXT NOT NULL,
|
||||
persona_id TEXT NOT NULL
|
||||
REFERENCES agent_personas (id) ON DELETE RESTRICT,
|
||||
persona_hash TEXT NOT NULL,
|
||||
backend TEXT NOT NULL,
|
||||
binding_hash TEXT NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE (run_id, role_id)
|
||||
)
|
||||
"""
|
||||
)
|
||||
op.execute(
|
||||
"INSERT INTO run_bindings_new SELECT id, run_id, role_id, persona_id, "
|
||||
"persona_hash, backend, binding_hash FROM run_bindings"
|
||||
)
|
||||
op.execute("DROP TABLE run_bindings")
|
||||
op.execute("ALTER TABLE run_bindings_new RENAME TO run_bindings")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# interactive_sessions: add persona_id FK (RESTRICT)
|
||||
# ------------------------------------------------------------------
|
||||
op.execute(
|
||||
"""
|
||||
CREATE TABLE interactive_sessions_new (
|
||||
id TEXT NOT NULL,
|
||||
persona_id TEXT NOT NULL
|
||||
REFERENCES agent_personas (id) ON DELETE RESTRICT,
|
||||
persona_hash TEXT NOT NULL,
|
||||
started_at TEXT,
|
||||
ended_at TEXT,
|
||||
last_message_at TEXT,
|
||||
state TEXT NOT NULL,
|
||||
PRIMARY KEY (id)
|
||||
)
|
||||
"""
|
||||
)
|
||||
op.execute(
|
||||
"INSERT INTO interactive_sessions_new SELECT id, persona_id, persona_hash, "
|
||||
"started_at, ended_at, last_message_at, state FROM interactive_sessions"
|
||||
)
|
||||
op.execute("DROP TABLE interactive_sessions")
|
||||
op.execute("ALTER TABLE interactive_sessions_new RENAME TO interactive_sessions")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# run_events: add phase_id FK (CASCADE)
|
||||
# ------------------------------------------------------------------
|
||||
op.execute(
|
||||
"""
|
||||
CREATE TABLE run_events_new (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
run_id TEXT NOT NULL
|
||||
REFERENCES runs (id) ON DELETE CASCADE,
|
||||
phase_id TEXT
|
||||
REFERENCES run_phases (id) ON DELETE CASCADE,
|
||||
seq INTEGER NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
payload JSON NOT NULL,
|
||||
idempotency_key TEXT NOT NULL,
|
||||
ts TEXT NOT NULL,
|
||||
UNIQUE (run_id, seq),
|
||||
UNIQUE (run_id, idempotency_key)
|
||||
)
|
||||
"""
|
||||
)
|
||||
op.execute(
|
||||
"INSERT INTO run_events_new SELECT id, run_id, phase_id, seq, type, "
|
||||
"payload, idempotency_key, ts FROM run_events"
|
||||
)
|
||||
op.execute("DROP INDEX IF EXISTS run_events_run_id_ts_idx")
|
||||
op.execute("DROP TABLE run_events")
|
||||
op.execute("ALTER TABLE run_events_new RENAME TO run_events")
|
||||
op.execute("CREATE INDEX run_events_run_id_ts_idx ON run_events (run_id, ts)")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# approval_requests: add phase_id FK (CASCADE)
|
||||
# ------------------------------------------------------------------
|
||||
op.execute(
|
||||
"""
|
||||
CREATE TABLE approval_requests_new (
|
||||
id TEXT NOT NULL,
|
||||
run_id TEXT NOT NULL
|
||||
REFERENCES runs (id) ON DELETE CASCADE,
|
||||
phase_id TEXT
|
||||
REFERENCES run_phases (id) ON DELETE CASCADE,
|
||||
gate_key TEXT NOT NULL,
|
||||
state TEXT NOT NULL,
|
||||
idempotency_key TEXT NOT NULL,
|
||||
payload JSON NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
resolved_at TEXT,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE (idempotency_key)
|
||||
)
|
||||
"""
|
||||
)
|
||||
op.execute(
|
||||
"INSERT INTO approval_requests_new SELECT id, run_id, phase_id, gate_key, "
|
||||
"state, idempotency_key, payload, created_at, resolved_at FROM approval_requests"
|
||||
)
|
||||
op.execute("DROP TABLE approval_requests")
|
||||
op.execute("ALTER TABLE approval_requests_new RENAME TO approval_requests")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# artifacts: add phase_id FK (CASCADE)
|
||||
# ------------------------------------------------------------------
|
||||
op.execute(
|
||||
"""
|
||||
CREATE TABLE artifacts_new (
|
||||
id TEXT NOT NULL,
|
||||
run_id TEXT NOT NULL
|
||||
REFERENCES runs (id) ON DELETE CASCADE,
|
||||
phase_id TEXT
|
||||
REFERENCES run_phases (id) ON DELETE CASCADE,
|
||||
path TEXT NOT NULL,
|
||||
schema_id TEXT NOT NULL,
|
||||
hash TEXT NOT NULL,
|
||||
valid INTEGER NOT NULL,
|
||||
validation_error JSON,
|
||||
created_at TEXT NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE (run_id, path, hash)
|
||||
)
|
||||
"""
|
||||
)
|
||||
op.execute(
|
||||
"INSERT INTO artifacts_new SELECT id, run_id, phase_id, path, schema_id, "
|
||||
"hash, valid, validation_error, created_at FROM artifacts"
|
||||
)
|
||||
op.execute("DROP TABLE artifacts")
|
||||
op.execute("ALTER TABLE artifacts_new RENAME TO artifacts")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# tool_calls: add run_id / phase_id / interactive_session_id FKs (CASCADE)
|
||||
# ------------------------------------------------------------------
|
||||
op.execute(
|
||||
"""
|
||||
CREATE TABLE tool_calls_new (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
run_id TEXT
|
||||
REFERENCES runs (id) ON DELETE CASCADE,
|
||||
phase_id TEXT
|
||||
REFERENCES run_phases (id) ON DELETE CASCADE,
|
||||
interactive_session_id TEXT
|
||||
REFERENCES interactive_sessions (id) ON DELETE CASCADE,
|
||||
tool_name TEXT NOT NULL,
|
||||
args JSON NOT NULL,
|
||||
result JSON,
|
||||
error TEXT,
|
||||
duration_ms INTEGER NOT NULL,
|
||||
ts TEXT NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
op.execute(
|
||||
"INSERT INTO tool_calls_new SELECT id, run_id, phase_id, interactive_session_id, "
|
||||
"tool_name, args, result, error, duration_ms, ts FROM tool_calls"
|
||||
)
|
||||
op.execute("DROP INDEX IF EXISTS tool_calls_run_id_ts_idx")
|
||||
op.execute("DROP TABLE tool_calls")
|
||||
op.execute("ALTER TABLE tool_calls_new RENAME TO tool_calls")
|
||||
op.execute("CREATE INDEX tool_calls_run_id_ts_idx ON tool_calls (run_id, ts)")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# llm_calls: add run_id / phase_id / interactive_session_id FKs (CASCADE)
|
||||
# ------------------------------------------------------------------
|
||||
op.execute(
|
||||
"""
|
||||
CREATE TABLE llm_calls_new (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
run_id TEXT
|
||||
REFERENCES runs (id) ON DELETE CASCADE,
|
||||
phase_id TEXT
|
||||
REFERENCES run_phases (id) ON DELETE CASCADE,
|
||||
interactive_session_id TEXT
|
||||
REFERENCES interactive_sessions (id) ON DELETE CASCADE,
|
||||
thread_id TEXT NOT NULL,
|
||||
persona_name TEXT NOT NULL,
|
||||
persona_version INTEGER NOT NULL,
|
||||
model TEXT NOT NULL,
|
||||
role TEXT NOT NULL,
|
||||
turn_index INTEGER NOT NULL,
|
||||
input_tokens INTEGER NOT NULL,
|
||||
output_tokens INTEGER NOT NULL,
|
||||
cached_tokens INTEGER NOT NULL,
|
||||
reasoning_tokens INTEGER NOT NULL,
|
||||
cost_usd_input REAL NOT NULL,
|
||||
cost_usd_output REAL NOT NULL,
|
||||
cost_usd_total REAL NOT NULL,
|
||||
latency_ms INTEGER NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
error_code TEXT,
|
||||
request_id TEXT,
|
||||
ts TEXT NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
op.execute(
|
||||
"INSERT INTO llm_calls_new SELECT id, run_id, phase_id, interactive_session_id, "
|
||||
"thread_id, persona_name, persona_version, model, role, turn_index, "
|
||||
"input_tokens, output_tokens, cached_tokens, reasoning_tokens, "
|
||||
"cost_usd_input, cost_usd_output, cost_usd_total, latency_ms, status, "
|
||||
"error_code, request_id, ts FROM llm_calls"
|
||||
)
|
||||
op.execute("DROP INDEX IF EXISTS llm_calls_run_id_ts_idx")
|
||||
op.execute("DROP INDEX IF EXISTS llm_calls_interactive_session_id_ts_idx")
|
||||
op.execute("DROP INDEX IF EXISTS llm_calls_model_ts_idx")
|
||||
op.execute("DROP TABLE llm_calls")
|
||||
op.execute("ALTER TABLE llm_calls_new RENAME TO llm_calls")
|
||||
op.execute("CREATE INDEX llm_calls_run_id_ts_idx ON llm_calls (run_id, ts)")
|
||||
op.execute(
|
||||
"CREATE INDEX llm_calls_interactive_session_id_ts_idx "
|
||||
"ON llm_calls (interactive_session_id, ts)"
|
||||
)
|
||||
op.execute("CREATE INDEX llm_calls_model_ts_idx ON llm_calls (model, ts)")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# phase_feedback: add run_id / phase_id FKs (CASCADE)
|
||||
# ------------------------------------------------------------------
|
||||
op.execute(
|
||||
"""
|
||||
CREATE TABLE phase_feedback_new (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
run_id TEXT NOT NULL
|
||||
REFERENCES runs (id) ON DELETE CASCADE,
|
||||
phase_id TEXT NOT NULL
|
||||
REFERENCES run_phases (id) ON DELETE CASCADE,
|
||||
reaction TEXT,
|
||||
comment TEXT,
|
||||
created_at TEXT NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
op.execute(
|
||||
"INSERT INTO phase_feedback_new SELECT id, run_id, phase_id, "
|
||||
"reaction, comment, created_at FROM phase_feedback"
|
||||
)
|
||||
op.execute("DROP TABLE phase_feedback")
|
||||
op.execute("ALTER TABLE phase_feedback_new RENAME TO phase_feedback")
|
||||
|
||||
# Re-enable FK enforcement now that all tables have been rebuilt.
|
||||
op.execute("PRAGMA foreign_keys=ON")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema.
|
||||
|
||||
Reverses all FK additions and drops the partial unique index.
|
||||
Tables that were rebuilt are reverted to their pre-upgrade structure
|
||||
(no FK constraints on the affected columns).
|
||||
"""
|
||||
op.execute("PRAGMA foreign_keys=OFF")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Revert phase_feedback
|
||||
# ------------------------------------------------------------------
|
||||
op.execute(
|
||||
"""
|
||||
CREATE TABLE phase_feedback_old (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
run_id TEXT NOT NULL,
|
||||
phase_id TEXT NOT NULL,
|
||||
reaction TEXT,
|
||||
comment TEXT,
|
||||
created_at TEXT NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
op.execute(
|
||||
"INSERT INTO phase_feedback_old SELECT id, run_id, phase_id, "
|
||||
"reaction, comment, created_at FROM phase_feedback"
|
||||
)
|
||||
op.execute("DROP TABLE phase_feedback")
|
||||
op.execute("ALTER TABLE phase_feedback_old RENAME TO phase_feedback")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Revert llm_calls
|
||||
# ------------------------------------------------------------------
|
||||
op.execute(
|
||||
"""
|
||||
CREATE TABLE llm_calls_old (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
run_id TEXT,
|
||||
phase_id TEXT,
|
||||
interactive_session_id TEXT,
|
||||
thread_id TEXT NOT NULL,
|
||||
persona_name TEXT NOT NULL,
|
||||
persona_version INTEGER NOT NULL,
|
||||
model TEXT NOT NULL,
|
||||
role TEXT NOT NULL,
|
||||
turn_index INTEGER NOT NULL,
|
||||
input_tokens INTEGER NOT NULL,
|
||||
output_tokens INTEGER NOT NULL,
|
||||
cached_tokens INTEGER NOT NULL,
|
||||
reasoning_tokens INTEGER NOT NULL,
|
||||
cost_usd_input REAL NOT NULL,
|
||||
cost_usd_output REAL NOT NULL,
|
||||
cost_usd_total REAL NOT NULL,
|
||||
latency_ms INTEGER NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
error_code TEXT,
|
||||
request_id TEXT,
|
||||
ts TEXT NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
op.execute(
|
||||
"INSERT INTO llm_calls_old SELECT id, run_id, phase_id, interactive_session_id, "
|
||||
"thread_id, persona_name, persona_version, model, role, turn_index, "
|
||||
"input_tokens, output_tokens, cached_tokens, reasoning_tokens, "
|
||||
"cost_usd_input, cost_usd_output, cost_usd_total, latency_ms, status, "
|
||||
"error_code, request_id, ts FROM llm_calls"
|
||||
)
|
||||
op.execute("DROP INDEX IF EXISTS llm_calls_run_id_ts_idx")
|
||||
op.execute("DROP INDEX IF EXISTS llm_calls_interactive_session_id_ts_idx")
|
||||
op.execute("DROP INDEX IF EXISTS llm_calls_model_ts_idx")
|
||||
op.execute("DROP TABLE llm_calls")
|
||||
op.execute("ALTER TABLE llm_calls_old RENAME TO llm_calls")
|
||||
op.execute("CREATE INDEX llm_calls_run_id_ts_idx ON llm_calls (run_id, ts)")
|
||||
op.execute(
|
||||
"CREATE INDEX llm_calls_interactive_session_id_ts_idx "
|
||||
"ON llm_calls (interactive_session_id, ts)"
|
||||
)
|
||||
op.execute("CREATE INDEX llm_calls_model_ts_idx ON llm_calls (model, ts)")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Revert tool_calls
|
||||
# ------------------------------------------------------------------
|
||||
op.execute(
|
||||
"""
|
||||
CREATE TABLE tool_calls_old (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
run_id TEXT,
|
||||
phase_id TEXT,
|
||||
interactive_session_id TEXT,
|
||||
tool_name TEXT NOT NULL,
|
||||
args JSON NOT NULL,
|
||||
result JSON,
|
||||
error TEXT,
|
||||
duration_ms INTEGER NOT NULL,
|
||||
ts TEXT NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
op.execute(
|
||||
"INSERT INTO tool_calls_old SELECT id, run_id, phase_id, interactive_session_id, "
|
||||
"tool_name, args, result, error, duration_ms, ts FROM tool_calls"
|
||||
)
|
||||
op.execute("DROP INDEX IF EXISTS tool_calls_run_id_ts_idx")
|
||||
op.execute("DROP TABLE tool_calls")
|
||||
op.execute("ALTER TABLE tool_calls_old RENAME TO tool_calls")
|
||||
op.execute("CREATE INDEX tool_calls_run_id_ts_idx ON tool_calls (run_id, ts)")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Revert artifacts
|
||||
# ------------------------------------------------------------------
|
||||
op.execute(
|
||||
"""
|
||||
CREATE TABLE artifacts_old (
|
||||
id TEXT NOT NULL,
|
||||
run_id TEXT NOT NULL
|
||||
REFERENCES runs (id) ON DELETE CASCADE,
|
||||
phase_id TEXT,
|
||||
path TEXT NOT NULL,
|
||||
schema_id TEXT NOT NULL,
|
||||
hash TEXT NOT NULL,
|
||||
valid INTEGER NOT NULL,
|
||||
validation_error JSON,
|
||||
created_at TEXT NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE (run_id, path, hash)
|
||||
)
|
||||
"""
|
||||
)
|
||||
op.execute(
|
||||
"INSERT INTO artifacts_old SELECT id, run_id, phase_id, path, schema_id, "
|
||||
"hash, valid, validation_error, created_at FROM artifacts"
|
||||
)
|
||||
op.execute("DROP TABLE artifacts")
|
||||
op.execute("ALTER TABLE artifacts_old RENAME TO artifacts")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Revert approval_requests
|
||||
# ------------------------------------------------------------------
|
||||
op.execute(
|
||||
"""
|
||||
CREATE TABLE approval_requests_old (
|
||||
id TEXT NOT NULL,
|
||||
run_id TEXT NOT NULL
|
||||
REFERENCES runs (id) ON DELETE CASCADE,
|
||||
phase_id TEXT,
|
||||
gate_key TEXT NOT NULL,
|
||||
state TEXT NOT NULL,
|
||||
idempotency_key TEXT NOT NULL,
|
||||
payload JSON NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
resolved_at TEXT,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE (idempotency_key)
|
||||
)
|
||||
"""
|
||||
)
|
||||
op.execute(
|
||||
"INSERT INTO approval_requests_old SELECT id, run_id, phase_id, gate_key, "
|
||||
"state, idempotency_key, payload, created_at, resolved_at FROM approval_requests"
|
||||
)
|
||||
op.execute("DROP TABLE approval_requests")
|
||||
op.execute("ALTER TABLE approval_requests_old RENAME TO approval_requests")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Revert run_events
|
||||
# ------------------------------------------------------------------
|
||||
op.execute(
|
||||
"""
|
||||
CREATE TABLE run_events_old (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
run_id TEXT NOT NULL
|
||||
REFERENCES runs (id) ON DELETE CASCADE,
|
||||
phase_id TEXT,
|
||||
seq INTEGER NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
payload JSON NOT NULL,
|
||||
idempotency_key TEXT NOT NULL,
|
||||
ts TEXT NOT NULL,
|
||||
UNIQUE (run_id, seq),
|
||||
UNIQUE (run_id, idempotency_key)
|
||||
)
|
||||
"""
|
||||
)
|
||||
op.execute(
|
||||
"INSERT INTO run_events_old SELECT id, run_id, phase_id, seq, type, "
|
||||
"payload, idempotency_key, ts FROM run_events"
|
||||
)
|
||||
op.execute("DROP INDEX IF EXISTS run_events_run_id_ts_idx")
|
||||
op.execute("DROP TABLE run_events")
|
||||
op.execute("ALTER TABLE run_events_old RENAME TO run_events")
|
||||
op.execute("CREATE INDEX run_events_run_id_ts_idx ON run_events (run_id, ts)")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Revert interactive_sessions
|
||||
# ------------------------------------------------------------------
|
||||
op.execute(
|
||||
"""
|
||||
CREATE TABLE interactive_sessions_old (
|
||||
id TEXT NOT NULL,
|
||||
persona_id TEXT NOT NULL,
|
||||
persona_hash TEXT NOT NULL,
|
||||
started_at TEXT,
|
||||
ended_at TEXT,
|
||||
last_message_at TEXT,
|
||||
state TEXT NOT NULL,
|
||||
PRIMARY KEY (id)
|
||||
)
|
||||
"""
|
||||
)
|
||||
op.execute(
|
||||
"INSERT INTO interactive_sessions_old SELECT id, persona_id, persona_hash, "
|
||||
"started_at, ended_at, last_message_at, state FROM interactive_sessions"
|
||||
)
|
||||
op.execute("DROP TABLE interactive_sessions")
|
||||
op.execute("ALTER TABLE interactive_sessions_old RENAME TO interactive_sessions")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Revert run_bindings
|
||||
# ------------------------------------------------------------------
|
||||
op.execute(
|
||||
"""
|
||||
CREATE TABLE run_bindings_old (
|
||||
id TEXT NOT NULL,
|
||||
run_id TEXT NOT NULL
|
||||
REFERENCES runs (id) ON DELETE CASCADE,
|
||||
role_id TEXT NOT NULL,
|
||||
persona_id TEXT NOT NULL,
|
||||
persona_hash TEXT NOT NULL,
|
||||
backend TEXT NOT NULL,
|
||||
binding_hash TEXT NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE (run_id, role_id)
|
||||
)
|
||||
"""
|
||||
)
|
||||
op.execute(
|
||||
"INSERT INTO run_bindings_old SELECT id, run_id, role_id, persona_id, "
|
||||
"persona_hash, backend, binding_hash FROM run_bindings"
|
||||
)
|
||||
op.execute("DROP TABLE run_bindings")
|
||||
op.execute("ALTER TABLE run_bindings_old RENAME TO run_bindings")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Revert runs (remove template_id FK)
|
||||
# ------------------------------------------------------------------
|
||||
op.execute("DROP INDEX IF EXISTS ux_active_run_repo_base")
|
||||
op.execute(
|
||||
"""
|
||||
CREATE TABLE runs_old (
|
||||
id TEXT NOT NULL,
|
||||
template_id TEXT NOT NULL,
|
||||
template_hash TEXT NOT NULL,
|
||||
state TEXT NOT NULL,
|
||||
repo_path TEXT NOT NULL,
|
||||
base_branch TEXT NOT NULL,
|
||||
worktree_root TEXT NOT NULL,
|
||||
current_phase_id TEXT,
|
||||
started_at TEXT,
|
||||
ended_at TEXT,
|
||||
final_report_path TEXT,
|
||||
paused_from_state TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
PRIMARY KEY (id)
|
||||
)
|
||||
"""
|
||||
)
|
||||
op.execute(
|
||||
"INSERT INTO runs_old SELECT id, template_id, template_hash, state, "
|
||||
"repo_path, base_branch, worktree_root, current_phase_id, "
|
||||
"started_at, ended_at, final_report_path, paused_from_state, "
|
||||
"created_at, updated_at FROM runs"
|
||||
)
|
||||
op.execute("DROP TABLE runs")
|
||||
op.execute("ALTER TABLE runs_old RENAME TO runs")
|
||||
|
||||
op.execute("PRAGMA foreign_keys=ON")
|
||||
Reference in New Issue
Block a user