세션 계약과 메모리 경계 보강
This commit is contained in:
parent
391639c1de
commit
2bb052f624
12 changed files with 836 additions and 116 deletions
|
|
@ -26,6 +26,12 @@ _SESSION_SHARE_TOKEN_INDEX: dict[str, str] = {}
|
|||
_LIVE_COACH_EVENT_CACHE: dict[str, list[dict[str, Any]]] = {}
|
||||
_SESSION_ARCHIVE_CACHE: dict[str, dict[str, Any]] = {}
|
||||
_SESSION_AUDIT_ROLES = {"teacher", "admin"}
|
||||
_WORKSHEET_REVIEW_STATUS_VALUES = {
|
||||
"pending",
|
||||
"approved",
|
||||
"changes_requested",
|
||||
"rejected",
|
||||
}
|
||||
_APPROPRIATENESS_SCORE = {
|
||||
"warn": 1.0,
|
||||
"neutral": 3.0,
|
||||
|
|
@ -39,6 +45,17 @@ class CaseContext:
|
|||
last_session_no: int
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class SessionSummaryWrite:
|
||||
session_id: str
|
||||
case_id: str
|
||||
session_no: int
|
||||
end_state: dict[str, Any]
|
||||
rapport_delta: float
|
||||
digest: str
|
||||
open_threads: list[str]
|
||||
|
||||
|
||||
_JOINED_CARD_COLUMNS = (
|
||||
"card_persona_id",
|
||||
"card_code",
|
||||
|
|
@ -935,11 +952,24 @@ async def ensure_review_tables() -> None:
|
|||
status TEXT NOT NULL DEFAULT 'pending'
|
||||
CHECK (status IN ('pending','viewed','closed')),
|
||||
note TEXT NOT NULL DEFAULT '',
|
||||
worksheet_status TEXT NOT NULL DEFAULT 'pending',
|
||||
worksheet_note TEXT NOT NULL DEFAULT '',
|
||||
worksheet_reviewed_at TIMESTAMPTZ,
|
||||
reviewed_at TIMESTAMPTZ,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
)
|
||||
"""
|
||||
)
|
||||
await conn.execute(
|
||||
"""
|
||||
ALTER TABLE app.session_review_status
|
||||
ADD COLUMN IF NOT EXISTS worksheet_status TEXT NOT NULL DEFAULT 'pending';
|
||||
ALTER TABLE app.session_review_status
|
||||
ADD COLUMN IF NOT EXISTS worksheet_note TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE app.session_review_status
|
||||
ADD COLUMN IF NOT EXISTS worksheet_reviewed_at TIMESTAMPTZ;
|
||||
"""
|
||||
)
|
||||
await conn.execute(
|
||||
"""
|
||||
ALTER TABLE app.session_review_status ENABLE ROW LEVEL SECURITY;
|
||||
|
|
@ -1324,17 +1354,29 @@ def _review_status_from_row(row: Any) -> dict[str, Any]:
|
|||
"reviewer_id": str(row["reviewer_id"] or ""),
|
||||
"status": str(row["status"] or "pending"),
|
||||
"note": str(row["note"] or ""),
|
||||
"worksheet_status": _worksheet_review_status(row["worksheet_status"]),
|
||||
"worksheet_note": str(row["worksheet_note"] or ""),
|
||||
"worksheet_reviewed_at": _iso_dt(row["worksheet_reviewed_at"]),
|
||||
"reviewed_at": _iso_dt(row["reviewed_at"]),
|
||||
"updated_at": _iso_dt(row["updated_at"]),
|
||||
}
|
||||
|
||||
|
||||
def _worksheet_review_status(value: object) -> str:
|
||||
raw = str(value or "pending")
|
||||
if raw in _WORKSHEET_REVIEW_STATUS_VALUES:
|
||||
return raw
|
||||
return "pending"
|
||||
|
||||
|
||||
def _review_status_cache_record(
|
||||
*,
|
||||
session_id: str,
|
||||
reviewer_id: str,
|
||||
status: str,
|
||||
note: str,
|
||||
worksheet_status: str | None = None,
|
||||
worksheet_note: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
now = datetime.now(timezone.utc)
|
||||
previous = _SESSION_REVIEW_STATUS_CACHE.get(session_id) or {}
|
||||
|
|
@ -1343,11 +1385,25 @@ def _review_status_cache_record(
|
|||
reviewed_at = now.isoformat().replace("+00:00", "Z")
|
||||
if status != "closed":
|
||||
reviewed_at = None
|
||||
worksheet_reviewed_at = previous.get("worksheet_reviewed_at")
|
||||
if worksheet_status is None:
|
||||
worksheet_status = _worksheet_review_status(previous.get("worksheet_status"))
|
||||
worksheet_note = str(previous.get("worksheet_note") or "")
|
||||
else:
|
||||
worksheet_status = _worksheet_review_status(worksheet_status)
|
||||
worksheet_note = str(worksheet_note or "").strip()
|
||||
if worksheet_status == "pending":
|
||||
worksheet_reviewed_at = None
|
||||
else:
|
||||
worksheet_reviewed_at = now.isoformat().replace("+00:00", "Z")
|
||||
return {
|
||||
"session_id": session_id,
|
||||
"reviewer_id": reviewer_id,
|
||||
"status": status,
|
||||
"note": note,
|
||||
"worksheet_status": worksheet_status,
|
||||
"worksheet_note": worksheet_note,
|
||||
"worksheet_reviewed_at": worksheet_reviewed_at,
|
||||
"reviewed_at": reviewed_at,
|
||||
"updated_at": now.isoformat().replace("+00:00", "Z"),
|
||||
}
|
||||
|
|
@ -1368,7 +1424,8 @@ async def list_session_review_statuses(
|
|||
) as conn:
|
||||
rows = await conn.fetch(
|
||||
"""
|
||||
SELECT session_id, reviewer_id, status, note, reviewed_at, updated_at
|
||||
SELECT session_id, reviewer_id, status, note, worksheet_status,
|
||||
worksheet_note, worksheet_reviewed_at, reviewed_at, updated_at
|
||||
FROM app.session_review_status
|
||||
WHERE session_id = ANY($1::uuid[])
|
||||
""",
|
||||
|
|
@ -1402,14 +1459,20 @@ async def save_session_review_status(
|
|||
status: str,
|
||||
note: str,
|
||||
principal: Principal,
|
||||
worksheet_status: str | None = None,
|
||||
worksheet_note: str | None = None,
|
||||
) -> tuple[dict[str, Any] | None, bool]:
|
||||
note = note.strip()
|
||||
if worksheet_status is not None:
|
||||
worksheet_note = str(worksheet_note or "").strip()
|
||||
if runtime_fallback_allowed():
|
||||
_SESSION_REVIEW_STATUS_CACHE[session_id] = _review_status_cache_record(
|
||||
session_id=session_id,
|
||||
reviewer_id=reviewer_id,
|
||||
status=status,
|
||||
note=note,
|
||||
worksheet_status=worksheet_status,
|
||||
worksheet_note=worksheet_note,
|
||||
)
|
||||
try:
|
||||
get_pool()
|
||||
|
|
@ -1421,10 +1484,17 @@ async def save_session_review_status(
|
|||
row = await conn.fetchrow(
|
||||
"""
|
||||
INSERT INTO app.session_review_status (
|
||||
session_id, reviewer_id, status, note, reviewed_at, updated_at
|
||||
session_id, reviewer_id, status, note, worksheet_status,
|
||||
worksheet_note, worksheet_reviewed_at, reviewed_at, updated_at
|
||||
)
|
||||
VALUES (
|
||||
$1::uuid, $2::uuid, $3, $4,
|
||||
COALESCE($5, 'pending'),
|
||||
CASE WHEN $5::text IS NULL THEN '' ELSE COALESCE($6, '') END,
|
||||
CASE
|
||||
WHEN $5::text IS NULL OR $5 = 'pending' THEN NULL
|
||||
ELSE now()
|
||||
END,
|
||||
CASE WHEN $3 = 'closed' THEN now() ELSE NULL END,
|
||||
now()
|
||||
)
|
||||
|
|
@ -1432,18 +1502,38 @@ async def save_session_review_status(
|
|||
reviewer_id = EXCLUDED.reviewer_id,
|
||||
status = EXCLUDED.status,
|
||||
note = EXCLUDED.note,
|
||||
worksheet_status = CASE
|
||||
WHEN $5::text IS NULL
|
||||
THEN app.session_review_status.worksheet_status
|
||||
ELSE $5
|
||||
END,
|
||||
worksheet_note = CASE
|
||||
WHEN $5::text IS NULL
|
||||
THEN app.session_review_status.worksheet_note
|
||||
ELSE COALESCE($6, '')
|
||||
END,
|
||||
worksheet_reviewed_at = CASE
|
||||
WHEN $5::text IS NULL
|
||||
THEN app.session_review_status.worksheet_reviewed_at
|
||||
WHEN $5 = 'pending'
|
||||
THEN NULL
|
||||
ELSE now()
|
||||
END,
|
||||
reviewed_at = CASE
|
||||
WHEN EXCLUDED.status = 'closed'
|
||||
THEN COALESCE(app.session_review_status.reviewed_at, now())
|
||||
ELSE NULL
|
||||
END,
|
||||
updated_at = now()
|
||||
RETURNING session_id, reviewer_id, status, note, reviewed_at, updated_at
|
||||
RETURNING session_id, reviewer_id, status, note, worksheet_status,
|
||||
worksheet_note, worksheet_reviewed_at, reviewed_at, updated_at
|
||||
""",
|
||||
session_id,
|
||||
reviewer_id,
|
||||
status,
|
||||
note,
|
||||
worksheet_status,
|
||||
worksheet_note,
|
||||
)
|
||||
return (_review_status_from_row(row) if row else None), True
|
||||
except Exception:
|
||||
|
|
@ -2165,14 +2255,31 @@ async def _upsert_pinned_fact_candidates(conn: Any, sess: InProcSession) -> None
|
|||
turn_id=fact.source_turn_id,
|
||||
)
|
||||
|
||||
|
||||
def _build_session_summary_write(sess: InProcSession, carry: memory.CarryOver) -> SessionSummaryWrite:
|
||||
digest_input = memory.build_session_digest_input(
|
||||
session_id=sess.session_id,
|
||||
case_id=sess.case_id,
|
||||
session_no=sess.session_no,
|
||||
masked_turns=sess.masked_turns(visible_to="client"),
|
||||
open_threads=carry.compression_job.open_threads if carry.compression_job else [],
|
||||
)
|
||||
digest_result = memory.build_fallback_digest_result(digest_input, end_state=carry.end_state)
|
||||
return SessionSummaryWrite(
|
||||
session_id=sess.session_id,
|
||||
case_id=sess.case_id,
|
||||
session_no=sess.session_no,
|
||||
end_state=carry.end_state,
|
||||
rapport_delta=carry.rapport_delta,
|
||||
digest=digest_result.digest,
|
||||
open_threads=list(digest_result.open_threads),
|
||||
)
|
||||
|
||||
|
||||
async def end_session(sess: InProcSession, carry: memory.CarryOver) -> bool:
|
||||
try:
|
||||
get_pool()
|
||||
digest = memory.build_fallback_session_digest(
|
||||
session_no=sess.session_no,
|
||||
masked_turns=sess.masked_turns(visible_to="client"),
|
||||
end_state=carry.end_state,
|
||||
)
|
||||
summary_write = _build_session_summary_write(sess, carry)
|
||||
async with acquire(role="learner", user_id=sess.learner_id) as conn:
|
||||
await conn.execute(
|
||||
"""
|
||||
|
|
@ -2196,13 +2303,13 @@ async def end_session(sess: InProcSession, carry: memory.CarryOver) -> bool:
|
|||
digest = EXCLUDED.digest,
|
||||
open_threads = EXCLUDED.open_threads
|
||||
""",
|
||||
sess.session_id,
|
||||
sess.case_id,
|
||||
sess.session_no,
|
||||
carry.end_state,
|
||||
carry.rapport_delta,
|
||||
digest,
|
||||
list(carry.compression_job.open_threads if carry.compression_job else []),
|
||||
summary_write.session_id,
|
||||
summary_write.case_id,
|
||||
summary_write.session_no,
|
||||
summary_write.end_state,
|
||||
summary_write.rapport_delta,
|
||||
summary_write.digest,
|
||||
summary_write.open_threads,
|
||||
)
|
||||
case_row = await conn.fetchrow(
|
||||
"""
|
||||
|
|
@ -2222,7 +2329,7 @@ async def end_session(sess: InProcSession, carry: memory.CarryOver) -> bool:
|
|||
case_digest = memory.merge_case_digest(
|
||||
existing_digest=case_row["case_digest"],
|
||||
session_no=sess.session_no,
|
||||
session_digest=digest,
|
||||
session_digest=summary_write.digest,
|
||||
)
|
||||
rapport_trajectory = memory.merge_rapport_trajectory(
|
||||
case_row["rapport_trajectory"],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue