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 못함.
왜 모든 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야. 자동 복구는 사람이 버튼을 누르지 않아도 된다는 장점만큼, 반복해도 같은 결과라는 증명이 필요해.