회기 종료 pinned_fact upsert를 AI evaluator 뷰로 실행

공개 /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
This commit is contained in:
Yun Chan 2026-08-18 18:36:04 +09:00
parent 6a42063d49
commit bf5f7352ce

View file

@ -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: async def _upsert_pinned_fact_candidates(conn: Any, sess: InProcSession) -> None:
turn_rows = [ turn_rows = [ {
{
"speaker": turn.speaker, "speaker": turn.speaker,
"text": turn.text_masked, "text": turn.text_masked,
"turn_id": turn.turn_id, "turn_id": turn.turn_id,
@ -3139,7 +3152,7 @@ async def end_session(sess: InProcSession, carry: memory.CarryOver) -> bool:
rapport_trajectory, rapport_trajectory,
alliance_level, alliance_level,
) )
await _upsert_pinned_fact_candidates(conn, sess) await _upsert_pinned_fact_candidates_with_ai_view(sess)
return True return True
except Exception: except Exception:
logger.exception("persisted session end failed session_id=%s", sess.session_id) logger.exception("persisted session end failed session_id=%s", sess.session_id)