council 하나는 <app-data>/councils/ 안의 JSON 파일 하나야. 거기엔 자리들이 들어 있어. 참가자마다 p1 같은 slot_id, 안 바뀌는 participant_id, 영혼, 두뇌. 그리고 mode 랑 preset 라벨, 라운드들. 라운드는 자기 종류(모든 자리에 보내는 broadcast, 한 자리에만 주는 point), 아빠의 prompt(빈 문자열이면 아빠가 말없이 넘긴 거야), 그리고 답한 자리마다 turn 하나씩을 적어둬. turn 은 자리로 키가 잡혀 있고, 대화 id 와 메시지 id 를 가리켜. 고른 답과 마무리 시각이 council 을 닫아.
라운드는 목록이 아니라 나무 모양이야. 앞선 prompt 를 고치거나 라운드를 다시 돌리면 형제 가지가 생겨. 1:1 chat 에서 메시지를 고치면 대화가 갈라지는 것처럼. 그래서 라운드마다 자기가 어느 라운드에서 갈라져 나왔는지 적혀 있어.
저장하지 않는 것
답 그 자체. 답은 전부 1:1 chat 이랑 같은 저장소, 같은 JSONL ground truth 를 쓰는 평범한 대화에 살아. 자리 하나가 council 내내 대화 하나를 쥐고 있는 건 아니야. 턴마다 그 자리를 위한 새 대화가 시작되고(테이블은 그 대화의 기록이 아니라 아래에서 볼 addendum 으로 자리에 닿아), 턴은 자기 대화 id 를 적어 두고, council 의 참가자→대화 지도는 그 자리의 가장 최근 대화를 가리켜. 같은 두뇌에 앉은 자리가 둘이어도 대화를 나눠 쓰는 일은 없어. 같은 두뇌 × N 이 로그 하나로 뭉개지면 절대 안 되거든.
참조만 저장하면 소유권이 분명해져. message 본문을 council JSON 에도 복사해 두면, 수정이나 보관이나 healing 이 어느 쪽을 믿어야 하는지 금세 흐려져. council 은 conversation_id 와 message_id 만 들고, 내용은 대화 저장소에서 읽어. 원장을 베끼지 않고 원장이 어디 있는지 가리키는 장부인 셈이야.
테이블이 각 자리에 닿는 길
라운드가 돌면 orchestrator 는 자리마다 그 두뇌의 정식 chat route 를 불러. 이때 두 부분을 따로 보내. user 메시지 에는 아빠가 방금 한 말만 들어가. 아빠가 말없이 넘기면 발언권을 넘긴다는 한 줄 신호가 들어가고. 테이블에 관한 건 전부 system 전용 addendum 에 실려. 이 자리가 누구고 테이블에 또 누가 있는지. 도구랑 기억을 위한 비공개 라우팅 정보(council id, 이 라운드의 순번, 자리 id)도 실리는데, 이건 보이는 답에는 절대 안 써. 그리고 지금 가지에서 여기까지 오간 대화 전부.
addendum 은 보내기만 하고 저장은 안 해. 그래서 자식 대화는 영락없이 1:1 처럼 읽혀. 아빠가 이렇게 말했고, 내가 이렇게 답했다.
복구도 같은 선을 따라 갈라져. council 메타데이터가 망가지면 자리 참조랑 라운드 순서를 다시 조립하고, 대화 로그가 망가지면 Truth 트랙의 healing 을 적용해. 두 가지 고장이 거대한 JSON 문서 하나에 섞여 있지 않으니까, 고치는 책임도 섞이지 않아.
meta-collection 은 덜 가져서 더 튼튼해. 자리가 늘거나 새 두뇌가 들어와도 라운드가 가리키는 id 모양만 지키면 돼. message schema 나 healing 을 council 마다 새로 만들 이유가 없어.