diff --git a/apps/api/app/session_persistence.py b/apps/api/app/session_persistence.py index b024786..90d4a29 100644 --- a/apps/api/app/session_persistence.py +++ b/apps/api/app/session_persistence.py @@ -2896,9 +2896,22 @@ async def _insert_pinned_fact_history( ) +async def _upsert_pinned_fact_candidates_with_ai_view(sess: InProcSession) -> None: + """pinned_fact upsert를 AI evaluator 뷰 컨텍스트로 실행한다. + + app.pinned_fact의 RLS는 learner 역할에 INSERT(소유 케이스)만 허용하고 + SELECT/UPDATE는 AI 컨텍스트 또는 admin/instructor로 제한한다. 그래서 + learner 연결에서 SELECT … FOR UPDATE + INSERT … ON CONFLICT DO UPDATE 조합은 + 충돌 후보 행 가시성이 막혀 InsufficientPrivilegeError 로 실패한다 + (2026-08-18 공개 /sessions/{id}/end 503 실측). 같은 learner uid 로 + ai_view evaluator 컨텍스트를 열면 INSERT/SELECT/UPDATE 정책이 모두 통과한다. + """ + async with acquire(role="learner", user_id=sess.learner_id, ai_view="evaluator") as ai_conn: + await _upsert_pinned_fact_candidates(ai_conn, sess) + + async def _upsert_pinned_fact_candidates(conn: Any, sess: InProcSession) -> None: - turn_rows = [ - { + turn_rows = [ { "speaker": turn.speaker, "text": turn.text_masked, "turn_id": turn.turn_id, @@ -3139,7 +3152,7 @@ async def end_session(sess: InProcSession, carry: memory.CarryOver) -> bool: rapport_trajectory, alliance_level, ) - await _upsert_pinned_fact_candidates(conn, sess) + await _upsert_pinned_fact_candidates_with_ai_view(sess) return True except Exception: logger.exception("persisted session end failed session_id=%s", sess.session_id)