From bf5f7352cee070962ebd76371b5d089dca9e0092 Mon Sep 17 00:00:00 2001 From: Yun Chan Date: Tue, 18 Aug 2026 18:36:04 +0900 Subject: [PATCH] =?UTF-8?q?=ED=9A=8C=EA=B8=B0=20=EC=A2=85=EB=A3=8C=20pinne?= =?UTF-8?q?d=5Ffact=20upsert=EB=A5=BC=20AI=20evaluator=20=EB=B7=B0?= =?UTF-8?q?=EB=A1=9C=20=EC=8B=A4=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 공개 /sessions/{id}/end 503(2026-08-18 실측): app.pinned_fact RLS는 learner 역할에 INSERT(소유 케이스)만 허용하고 SELECT/UPDATE는 AI 컨텍스트 또는 admin/instructor 전용이다. end_session이 learner 연결로 SELECT FOR UPDATE + INSERT ON CONFLICT DO UPDATE 조합을 돌려 충돌 후보 행 가시성이 막히며 InsufficientPrivilegeError로 실패했고, fallback 차단이 503으로 노출됐다. 엔진이 건강해 deep 평가가 처음으로 pinned 팩트를 반환한 순간 발화한 잠복 결함이다. - _upsert_pinned_fact_candidates_with_ai_view: 같은 learner uid 로 acquire(ai_view='evaluator') 를 열어 INSERT/SELECT/UPDATE 정책을 모두 충족 - 검증: 공개 DB에서 롤백 재현 — learner 컨텍스트 upsert RLS 거부, ai_view evaluator 컨텍스트 동일 SQL 통과. app 924 passed·관련 62 passed --- apps/api/app/session_persistence.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) 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)