본문 바로가기
C.W.K.
Stream
Lesson 04 of 08 · published

Healing Layer — 재구성 + 정규화

~16 min · healing, normalization

Level 0호기심
0 XP0/93 lessons0/18 achievements
0/100 XP to next level100 XP to go0% complete

healing 이 하는 일

대화를 읽을 때마다 수리 pass 셋이 이 순서로 돌아. 먼저 _cleanup_empty_assistant_turns — 빈 assistant 껍데기가 있으면 heal 을 막으니까. 그다음 _heal_incomplete_turns, 마지막으로 _cleanup_dangling_parent_ids. 뒤의 둘 사이에선 도구 사용 기록이 빠진 assistant row 를 JSONL 로 채워 넣어.

재구성

JSONL walk, completed assistant child 없는 user message 찾기, streaming delta 누적 (pippa.user_msg_id 로 keyed), synthesized assistant row 를 SQLite 에 기록. user.created_at + 1µs 로 timestamp — heal 시점 아님. 그래야 ORDER BY created_at 이 자연 chain.

정규화

conversation 의 existing message 를 reference 안 하는 parent_id cleanup, created_at 순 이전 message 로 re-parent. ghost-branch mechanism 의 root 죽임 — dangling reference 완전 제거하니까 미래 placeholder 가 match 못함.

원칙: persistence layer 가 시스템의 long-lived 부분. client 코드 버그가 거기 영구 footprint 남길 수 있어. healing 이 그 footprint 가 presentation 에 도달 못 하게 하는 마지막 방어선.

왜 모든 read 마다

healing 은 모든 GET 에서 멱등으로 돌아 — 그 대화에 스트림이 살아 있는 동안만 빼고. 아직 쓰이고 있는 턴을 고치면 쓰는 쪽이랑 경합하니까. 별도 'repair' 버튼은 이제 없어. 비용 낮음 (어차피 JSONL 읽고 있으니), 어떤 과거 client 버전의 corrupted 상태도 화면에 도달 못 함 의미.

Healing이 무서운 이유도 있어. repair code가 원본보다 더 많은 사실을 상상하면 조용한 data corruption machine이 돼. 그래서 reconstruction은 JSONL에 이미 있는 delta와 stable id만 쓰고, normalization도 resolve되지 않는 parent를 이미 정렬된 이전 message에 붙이는 좁은 규칙만 써. "아마 이런 대화였겠지"는 금지야.

Idempotence를 꼭 검증해. 같은 conversation을 열 번 읽어도 첫 pass 뒤에는 row 수와 parent chain이 더 바뀌지 않아야 해. healing이 매 read마다 조금씩 다른 답을 만들면 치료가 아니라 drift야. 자동 복구는 사람이 버튼을 누르지 않아도 된다는 장점만큼, 반복해도 같은 결과라는 증명이 필요해.

Code

The healing pass — sketch·python
async def get_conversation(conversation_id: str) -> Conversation:
    async with _heal_lock_for(conversation_id):
        messages = await store.get_messages(conversation_id)

        # 1. 빈 assistant 껍데기는 아래 heal 을 막아 — 먼저 치워.
        if await _cleanup_empty_assistant_turns(conversation_id, messages):
            messages = await store.get_messages(conversation_id)

        # 2. 안 끝난 assistant 턴을 JSONL 로 다시 지어.
        #    (이 대화에 스트림이 살아 있는 동안엔 아무것도 안 해.)
        if await _heal_incomplete_turns(conversation_id, messages):
            messages = await store.get_messages(conversation_id)

        # 3. 더 이상 안 풀리는 parent_id 를 다시 이어.
        if await _cleanup_dangling_parent_ids(conversation_id):
            messages = await store.get_messages(conversation_id)

    # 각 pass 는 뭔가 바꿨으면 True 를 돌려줘. 호출하는 쪽은 pass 가 SQLite 에서
    # 고친 row 를 돌려주길 믿는 대신 다시 읽어.
    return Conversation(id=conversation_id, messages=messages)

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고
💛 by 똘이— warm💛 by 피파— warm

댓글 0

🔔 답글 알림 (로그인 필요)
로그인 — 댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.