"""On-session-end async aggregation hook (TASK-07-02, D-054). Fire-and-forget: designed to be chained as an `asyncio.create_task` after the mastery flow. Failures log + the nightly job reconciles (no exception propagation to the caller — the session-end response returns immediately). If `pg_store` is None (no Postgres), no-op + log WARNING. """ from __future__ import annotations import logging from typing import Any from db.pg_store import PgStore log = logging.getLogger(__name__) async def on_session_end(pg_store: PgStore | None, session_outcome: dict[str, Any]) -> None: """Aggregate one session outcome. Non-blocking, fire-and-forget (D-054). Failures are logged but never raised — the caller (session_recorder) has already returned its response; aggregation is off the voice path. The nightly job (nightly.py) reconciles any missed/hook-failed sessions. """ if pg_store is None: log.warning( "cohort aggregation skipped (no Postgres) for session %s", session_outcome.get("scenario_id"), ) return try: from server.cohort.aggregator import aggregate_session await aggregate_session(pg_store, session_outcome) except Exception: log.exception( "cohort aggregation hook failed for session %s — nightly job will reconcile", session_outcome.get("scenario_id"), ) __all__ = ["on_session_end"]