Files
opensquilla--opensquilla/migrations/V015__meta_skill_step_usage.py
2026-07-13 13:12:33 +08:00

141 lines
4.7 KiB
Python

"""V015 - persist per-step meta-skill usage summaries."""
from __future__ import annotations
from yoyo import step
__depends__: set[str] = {"V014__meta_skill_run_steps_allow_user_input"}
_STEP_COLUMNS_WITHOUT_USAGE = (
"run_id",
"step_id",
"step_kind",
"declared_skill",
"effective_skill",
"status",
"started_at_ms",
"ended_at_ms",
"rendered_inputs_json",
"output_text",
"error",
"substitute_step_id",
"truncated_fields",
)
def _create_steps_table_without_usage_sql(table_name: str) -> str:
return f"""
CREATE TABLE {table_name} (
run_id TEXT NOT NULL
REFERENCES meta_skill_runs(run_id) ON DELETE CASCADE,
step_id TEXT NOT NULL,
step_kind TEXT NOT NULL
CHECK(step_kind IN (
'agent', 'llm_classify', 'llm_chat',
'tool_call', 'skill_exec', 'user_input'
)),
declared_skill TEXT NOT NULL,
effective_skill TEXT NOT NULL,
status TEXT NOT NULL
CHECK(status IN ('running','ok','failed','substituted')),
started_at_ms INTEGER NOT NULL,
ended_at_ms INTEGER,
rendered_inputs_json TEXT NOT NULL,
output_text TEXT,
error TEXT,
substitute_step_id TEXT,
truncated_fields TEXT NOT NULL DEFAULT '',
PRIMARY KEY (run_id, step_id)
)
"""
def _table_exists(conn, table: str) -> bool:
cur = conn.cursor()
cur.execute(
"SELECT 1 FROM sqlite_master WHERE type='table' AND name=?",
(table,),
)
return cur.fetchone() is not None
def _column_exists(conn, table: str, column: str) -> bool:
cur = conn.cursor()
cur.execute(f"PRAGMA table_info({table})")
return any(row[1] == column for row in cur.fetchall())
def _assert_fk_enforcement_off(conn) -> None:
"""Fail loudly if foreign-key enforcement is live on this connection.
yoyo wraps every Python step in an explicit transaction, and
``PRAGMA foreign_keys`` is a documented no-op inside a transaction, so
the OFF/ON bracket in the rollback recreate below cannot take effect
there. The rebuild has only ever been validated with SQLite's default
foreign_keys=OFF; with enforcement live the drop-and-copy would run
under FK semantics this migration was not designed for. Refuse loudly.
"""
cur = conn.cursor()
cur.execute("PRAGMA foreign_keys")
row = cur.fetchone()
if row is not None and row[0]:
raise RuntimeError(
"V015: PRAGMA foreign_keys is enabled on the migration "
"connection; the meta_skill_run_steps rebuild would run with "
"foreign-key enforcement live (the OFF pragma below is a no-op "
"inside yoyo's step transaction). Refusing to proceed — run "
"migrations on a connection with foreign-key enforcement "
"disabled."
)
def apply_step(conn) -> None:
if not _table_exists(conn, "meta_skill_run_steps"):
return
if _column_exists(conn, "meta_skill_run_steps", "usage_json"):
return
conn.cursor().execute(
"ALTER TABLE meta_skill_run_steps "
"ADD COLUMN usage_json TEXT NOT NULL DEFAULT '{}'"
)
def rollback_step(conn) -> None:
if not _table_exists(conn, "meta_skill_run_steps"):
return
if not _column_exists(conn, "meta_skill_run_steps", "usage_json"):
return
_assert_fk_enforcement_off(conn)
cur = conn.cursor()
# Inert inside yoyo's step transaction (see _assert_fk_enforcement_off);
# kept because it is harmless and correct outside a transaction.
cur.execute("PRAGMA foreign_keys = OFF")
try:
cur.execute(
_create_steps_table_without_usage_sql("meta_skill_run_steps__new")
)
columns = ", ".join(_STEP_COLUMNS_WITHOUT_USAGE)
cur.execute(
f"INSERT INTO meta_skill_run_steps__new ({columns}) "
f"SELECT {columns} FROM meta_skill_run_steps"
)
cur.execute("DROP TABLE meta_skill_run_steps")
cur.execute(
"ALTER TABLE meta_skill_run_steps__new RENAME TO meta_skill_run_steps"
)
cur.execute(
"CREATE INDEX idx_meta_run_steps_status"
" ON meta_skill_run_steps(status)"
)
cur.execute("PRAGMA foreign_key_check")
bad = cur.fetchall()
if bad:
raise RuntimeError(
f"V015 foreign_key_check found orphans after rollback: {bad}"
)
finally:
cur.execute("PRAGMA foreign_keys = ON")
steps = [step(apply_step, rollback_step)]