일부러 깬 첫 번째 불변식
이 트랙 전체가 규칙 하나 위에 서 있어. 한 글자라도 보여주기 전에 JSONL에 먼저 쓰고, 다른 저장소는 전부 그걸 비춘다는 거. 2026-09-26에 아빠가 정반대를 한 줄로 부탁했어. 프라이버시 모드에선 어느 기기든 기록도 캐시도 남기지 말라고. 그래서 cwkPippa엔 첫 번째 불변식을 깨는 대화 모양이 딱 하나 생겼고, 그건 일부러 깬 거야.
예외의 크기는 불안이 아니라 위협 모델이 정했어. 아빠가 꼽은 장면은 넷이야. 놀러 온 조카가 켜져 있는 기기를 툭 눌렀는데 민감한 대화가 뜨는 경우, 폰을 잃어버린 경우, 평범한 폴더나 백업이나 NAS에 글이 그대로 읽히게 놓인 경우, 그리고 아빠와 엄마가 서로의 사생활을 지키는 경우. 그러고는 선도 직접 그었어. 모델 제공자들이 자기 쪽에 뭘 남기는지까진 쫓지 말고, 일반 대화를 저장하는 방식도 다시 뜯지 말라고. 설계 문서는 이걸 일하는 규칙으로 바꿔 적었어. 네 장면 중 어디에도 답하지 않는 조치는 과한 설계니까 빼.
턴이 아니라 대화의 속성
음성 모드는 턴의 속성이야(그 얘긴 음성 퀘스트에 있어). 프라이버시는 반대 종류야. 대화를 만들 때 고르고, 도중엔 어느 쪽으로도 안 바꿔. 이미 지난 턴을 안 쓴 걸로 되돌릴 수는 없고, 저장되는 대화 안에 비공개 턴 하나를 끼워 두면 약속이 깨지니까.
- 저장소가 따로 있어. id는 자기만의 이름공간,
priv-<uuid>에서 나와. 기록은 서버 프로세스의 메모리에 살고, 대화마다 폴더 하나로 디스크에 같이 써 둬. 그 폴더는 모든 백업과 모든 색인 밖에 있어. 대화 데이터베이스엔 절대 안 들어가. - 끝나는 길은 딱 둘이야. End, 아니면 유휴 시간. 기본은 10분이고, 대화 안에서 1분부터 120분까지 바꿀 수 있어. 첫 빌드는 서버를 재시작하면 대화가 끝나게 돼 있었는데, 아빠가 시험해 보고 안 된다고 정했어. 그래서 지금은 재시작해도 대화가 돌아오고, 서버가 꺼져 있던 시간도 유휴 시간에 들어가.
- 살아 있는 동안엔 뺏는 게 없어. 첨부, 받아쓰기, 음성, 저장되는 대화로 가지치기까지 다 돼. 첨부 버튼을 막아 두려던 계획에 아빠가 바로 제동을 걸어서 정해졌어. 다른 건 딱 하나, 끝난 뒤에 남는 게 없다는 것뿐이야.
지키는 건 서버, 클라이언트는 요청하고 보여주고 자기 디스크를 깨끗이
priv- 접두사가 플래그의 전부야. 클라이언트는 그걸 넘기고 자물쇠, 청록색 배너, 유휴 카운트다운을 그려. 서버는 자기가 가진 모든 저장소에서 약속을 지켜. 그래서 WebUI도 폰도 Firekeeper도 필드 하나만 넘기고 프라이버시를 물려받았어. 그래도 클라이언트마다 자기 디스크에서 할 일은 따로 있어. WebUI는 초안을 페이지 메모리에만 두고 로컬 저장소엔 절대 안 넣어. 폰은 outbox 파일을 안 쓰고, 대화도 그 이미지도 캐시하지 않고, 앱 전환 화면에선 화면을 가려.
- 채팅 라우트 일곱 개 전부에서 가드가 제일 먼저 돌아. 끝난 비공개 id는 410을 받고, 라우트가 원래 쓰던 '낡은 id면 새로 만들기'로 절대 안 빠져. 그리로 빠졌다면 아빠 말이 새로 저장되는 대화에 실렸을 거야. 살아 있는 id 없이 비공개 플래그만 오면 409. 남의 비공개 id면 관리자든 아니든 404.
- 그 밖의 모든 것 앞엔 문 검문소가 있어. 대화 id를 경로에 달고 뭔가를 쓰는 라우트가 수십 개야. 비공개 id를 단 경로는 짧은 목록의 문(자기 수명 관리, 다시 읽기, 기록 도구 몇 개)에만 닿고, 나머지 라우트는 전부 404야. 아직 아무도 안 쓴 라우트까지 포함해서.
- 쓰는 곳은 각자 자기 이음매에서 건너뛰어. JSONL, 데이터베이스 행, 임베딩, 자동 제목, 요약 압축, 실패 덤프, 푸시 알림, 아티팩트. 로그 줄은 출처와 심각도만 남기고 글은 버리는데, 이걸 로그 레코드 팩토리에서 해서 내년에 새로 생길 로그 줄까지 덮여.
- 소울에겐 턴마다 알려줘. 음성 블록 옆에 붙이고, 캐시된 시스템 프롬프트엔 절대 안 넣어. 그 메모엔 아빠의 예외 하나가 실려 있어. 아빠가 '기록해'나 '기억해'라고 하면 소울은 딱 그걸 자기 볼트나 작업의 목적지에 써. 거기엔 소울의 판단을 뒤집는 코드 관문이 없어. 아빠 뜻을 읽는 건 소울 몫이야.
- 기억은 일방통행이야. 비공개 대화도 기억 엔진을 그대로 읽어. 기억 없는 소울은 말이 나빠지거든. 읽을 때마다 '기록하지 마' 표시가 붙고, 질문은 요청 본문에 실어서 어떤 접근 로그에도 안 보여. 거꾸로 흘러 나가는 건 없어.
가드는 검사의 미로가 아니라 테스트 하나
이만큼 넓은 약속은 호출하는 자리마다 기억해서 지킬 수가 없어. 그래서 테스트 하나가 모델 자리에 가짜 브레인을 세우고 진짜 쓰는 곳들은 켜 둔 채로, 비공개 턴을 채팅 라우트 전부에 한 번씩 돌리고, 그다음 뒤져. JSONL 없음, 행 없음, 벡터 없음, 푸시 없음, 기억 이벤트 없음, 아티팩트 없음, 그리고 데이터 루트 아래 어디에도 그 말을 담은 파일 없음. 임베딩 가드나 세션 로그 가드를 빼면 실패하는 걸 손으로 확인했고, 데이터 루트 아래에 나중에 추가된 쓰는 곳이 비공개 경우를 깜빡해도 똑같이 실패해. 이 테스트가 못 보는 것도 똑같이 분명하게 적어 뒀어. 진짜 브레인의 도구, 그리고 데이터 루트 밖에 쓰이는 것은 따로 확인해야 해. 아래의 CLI 기록도 이 테스트가 아니라 감사가 찾아냈어.
남는 것도 정직하게 적어 뒀어. 접근 로그의 경로엔 비공개 id가 찍힐 수 있고, 음성 엔진은 해시와 길이만 든 작업 행을 남겨. id와 크기뿐이고, 말은 없어.
아무도 부탁한 적 없는 사본
이 작업 때문에 저장소를 하나하나 뒤지다가, 한 번도 결정된 적 없는 게 하나 나왔어. Claude CLI는 세션마다 자기 홈 폴더 아래에 평문 기록을 남기는데, WebUI 턴은 하나하나가 새 CLI 세션이야. CLI의 30일 정리 주기 안에서만 6,461개, 3.2 GB였어. 생각, 도구 입력, 기억 블록까지 담긴 대화 전체가 그 폴더째로 NAS에 복사됐고, 세션 브라우저가 그걸 또 색인했어. 5월 이후로 CLI 세션을 이어 쓴 적이 없으니, JSONL의 저장 암호화를 괜히 무너뜨리고 있던 거야. 아빠 결정은 한 글자였어. '꺼'. 이제 WebUI가 띄우는 CLI엔 전부 --no-session-persistence가 붙어. 실제 턴을 양쪽으로 한 번씩 돌려서 증명했어. 플래그 없이 기록 하나, 플래그 붙이고 기록 없음. 이건 비공개든 아니든 모든 대화에 적용돼.