diff --git a/apps/api/src/services/ai_agent_autonomous_runtime_control.py b/apps/api/src/services/ai_agent_autonomous_runtime_control.py index 241aae4b..cea6f3a8 100644 --- a/apps/api/src/services/ai_agent_autonomous_runtime_control.py +++ b/apps/api/src/services/ai_agent_autonomous_runtime_control.py @@ -2041,7 +2041,11 @@ async def load_ai_agent_autonomous_runtime_receipt_readback( async with get_db_context(project_id) as db: await db.execute(text("SET LOCAL statement_timeout = '5000ms'")) - async def _safe_aux_rows(query_name: str, sql: str) -> list[Mapping[str, Any]]: + async def _safe_aux_rows( + query_name: str, + sql: str, + fallback_sql: str | None = None, + ) -> list[Mapping[str, Any]]: try: return (await db.execute(text(sql), params)).mappings().all() except Exception as exc: # pragma: no cover - depends on live schema drift @@ -2051,6 +2055,16 @@ async def load_ai_agent_autonomous_runtime_receipt_readback( query_name=query_name, error_type=type(exc).__name__, ) + if fallback_sql: + try: + return (await db.execute(text(fallback_sql), params)).mappings().all() + except Exception as fallback_exc: # pragma: no cover - live schema drift + logger.warning( + "ai_agent_autonomous_runtime_trace_aux_fallback_failed", + project_id=project_id, + query_name=query_name, + error_type=type(fallback_exc).__name__, + ) return [] operation_counts = ( @@ -2102,10 +2116,12 @@ async def load_ai_agent_autonomous_runtime_receipt_readback( timeline_counts = await _safe_aux_rows( "timeline_counts", _RUNTIME_TIMELINE_COUNTS_SQL, + _RUNTIME_TIMELINE_COUNTS_FALLBACK_SQL, ) playbook_trust_counts = await _safe_aux_rows( "playbook_trust_counts", _RUNTIME_PLAYBOOK_TRUST_COUNTS_SQL, + _RUNTIME_PLAYBOOK_TRUST_COUNTS_FALLBACK_SQL, ) except Exception as exc: logger.warning( @@ -2440,6 +2456,17 @@ _RUNTIME_TIMELINE_COUNTS_SQL = """ WHERE event_type IS NOT NULL OR actor IS NOT NULL OR actor_role IS NOT NULL + GROUP BY coalesce(status, 'unknown') + ORDER BY status +""" + + +_RUNTIME_TIMELINE_COUNTS_FALLBACK_SQL = """ + SELECT + 'timeline_event' AS status, + count(*) AS total, + 0 AS recent + FROM timeline_events """ @@ -2469,6 +2496,15 @@ _RUNTIME_PLAYBOOK_TRUST_COUNTS_SQL = """ """ +_RUNTIME_PLAYBOOK_TRUST_COUNTS_FALLBACK_SQL = """ + SELECT + 'cataloged' AS status, + count(*) AS total, + 0 AS recent + FROM playbooks +""" + + def _validate_payload(payload: dict[str, Any]) -> None: if payload.get("schema_version") != _SCHEMA_VERSION: raise ValueError(f"schema_version must be {_SCHEMA_VERSION}") diff --git a/apps/api/tests/test_ai_agent_autonomous_runtime_control.py b/apps/api/tests/test_ai_agent_autonomous_runtime_control.py index b20d7bb1..091a73a6 100644 --- a/apps/api/tests/test_ai_agent_autonomous_runtime_control.py +++ b/apps/api/tests/test_ai_agent_autonomous_runtime_control.py @@ -1,10 +1,21 @@ from src.services.ai_agent_autonomous_runtime_control import ( + _RUNTIME_PLAYBOOK_TRUST_COUNTS_FALLBACK_SQL, + _RUNTIME_TIMELINE_COUNTS_SQL, build_ai_agent_autonomous_runtime_control, build_runtime_receipt_readback_from_rows, classify_deploy_control_plane_observation, ) +def test_runtime_receipt_auxiliary_sql_keeps_source_family_counts_schema_safe(): + assert "GROUP BY coalesce(status, 'unknown')" in _RUNTIME_TIMELINE_COUNTS_SQL + assert "FROM timeline_events" in _RUNTIME_TIMELINE_COUNTS_SQL + assert "count(*) AS total" in _RUNTIME_PLAYBOOK_TRUST_COUNTS_FALLBACK_SQL + assert "FROM playbooks" in _RUNTIME_PLAYBOOK_TRUST_COUNTS_FALLBACK_SQL + assert "updated_at" not in _RUNTIME_PLAYBOOK_TRUST_COUNTS_FALLBACK_SQL + assert "trust_score" not in _RUNTIME_PLAYBOOK_TRUST_COUNTS_FALLBACK_SQL + + def test_ai_agent_autonomous_runtime_control_uses_current_owner_directive(): data = build_ai_agent_autonomous_runtime_control()