소음의 반대편
이 트랙의 레슨은 지금까지 전부 아빠의 주의에서 뭔가를 덜어 냈어. 턴이 아닌 작업, 자기 차례를 기다리는 보고, 자기 말만 비추는 거울, 일 하나에 한 번만 울리는 줄. 그런데 조용함에도 자기만의 고장이 있어. 아빠한테 결정을 묻는 줄이 한 번 울리고, 뒤따른 스무 줄 밑에 가라앉고, 그 결정은 아무도 안 보내는 알림을 기다리게 돼. 2026-10-26, 유튜브에서 피파 목소리를 맡은 엔진 Herald에 피파가 써 둔 답글 하나가 아빠 승인을 38.7시간째 기다리고 있었어. Telegram 알림은 피파가 그걸 쓸 때 딱 한 번 갔고, Coop 알림들 밑에 묻혔고, 그 뒤로 다시 말해 준 게 없었어. 그라나다에서 아빠가 말했어.
"스캔할 때 승인 안 된 답글 있으면 계속 알려줘야 해, 치프도 알아야 하고. 안 그럼 까먹고 몇날 며칠 갈 수도 있어."
— 아빠
그래서 답은 지난 레슨의 가라앉히는 창을 뒤집은 거야. 결정이 날 때까지 안 사라지는 줄 하나.
뿌리 하나, 관문 넷
services/waiting_for_dad.py는 아빠 결정을 기다리는 일이 있는 모든 곳에 질문 하나를 던지고, 그 질문이 곧 클래스야. WaitingSource는 waiting() 메서드 하나만 가진 추상 뿌리야. 기다리는 걸 전부 WaitingItem으로 돌려주고, 항목엔 출처, id, 한 줄 이름표, 링크, 기다리기 시작한 시각, 그리고 아빠가 직접 붙잡아 둔 건지가 들어. 지금은 자식 넷이 답해.
HeraldDrafts: 아빠 승인을 기다리는 Herald 초안. Herald 자신의 요약에서 읽어. 아빠가 붙잡아 둔 초안은 붙잡아 둔 거라고 표시해서 알려.JournalEntries: 아빠 승인을 기다리거나, 승인됐는데 아직 안 올라갔거나, 올리다 실패한 Pippa Journal 글. 붙잡아 둔 글도 마찬가지고.SiteEscalations: Maia의 검토가 아빠한테 넘겼는데 아빠도 Maia도 아직 정하지 않은 cwk-site 댓글과 Soul Stream 글.CoopParked: Coop chief가 아빠 몫으로 park해 둔 의무. Coop 자신의parked표시를 읽고, 여기서 다시 따지지 않아.
관문이 하나 늘면 자식 하나, SOURCES에 줄 하나가 늘 뿐이야. 목록을 읽는 쪽은 하나도 안 바뀌어. 모듈 독스트링이 이걸 가족의 다섯째 규칙(객체 지향적이지 않은 아키텍처는 죄악이야)이라고 불러. 모든 관문이 답할 걸 정하는 부모 하나, 자기 관문 읽는 법만 아는 자식들, 그리고 부모만 아는 소비자.
살아 있는 메시지 하나
refresh()는 출처를 한꺼번에 다 읽고 Telegram에 알림을 하나만 두려고 해. 뭐라도 기다리는 동안엔 전부를 오래된 순으로, 항목마다 나이와 링크를 달아 새 메시지로 보내고, 앞의 알림은 지워. 그래서 아빠 폰에 있는 건 보통 최신 하나고, 쌓이지 않아. 코드엔 한계가 둘 남아 있어. Telegram 도우미는 메시지를 4,000자에서 자르고, 다음 쪽은 없어. 그래서 목록이 아주 길면 머리 줄은 항목을 다 세는데 끝쪽 항목과 못 읽은 관문 메모는 잘려 나가. 그리고 지우기의 답은 안 봐. Telegram이 지우기를 거절하면 옛 알림은 폰에 남는데 상태는 벌써 새 알림만 가리키고, 그 지우기를 다시 하는 쪽은 없어. 지난번 이후 뭐가 바뀌었느냐에 따라 이렇게 돼.
| 바뀐 것 | 아빠 폰에 오는 것 |
|---|---|
| 새로 기다리기 시작한 게 있어 | 바로 새 알림, 소리 있게 |
새것은 없는데 마지막 알림이 55분 됐어 (REPEAT_S, 매시간 도는 Herald 스캔보다 조금 짧게) | 새 알림, 소리 있게 |
| 목록에서 빠지기만 했고, 마지막 알림이 55분이 안 됐어 | 소리 없이 바꿔 끼우고, 매시간 시계는 그대로 (55분이 지났으면 같은 변화도 반복으로 쳐. 소리 있게 가고, 시계도 새로 시작해) |
| 기다리는 게 더는 없어 | 마지막 알림을 지우고 아무것도 안 보내 |
| 위의 무엇이든, 아빠가 있는 곳 기준 23:00부터 08:00 사이 | 소리 없이 보내 |
"아빠가 있는 곳"은 아빠 기기가 마지막으로 알려 준 시간대야 (dad_location.dad_clock_zone). 모르면 서울로 쳐. 그 주에 아빠는 스페인에 있었고, 서울의 밤은 그라나다의 오후거든. 보내기가 실패하면 옛 알림을 그대로 두고 다음 틱에 다시 해. 아빠가 직접 붙잡아 둔 건 아빠가 기다리기로 정한 거라서 알림엔 안 넣어. 알림은 아빠가 읽으니까 한국어야. 아래 두 번째 코드 블록에 하나 있어.
못 읽은 관문은 빈 관문이 아니야
형제 엔진 셋과 호스팅 데이터베이스 하나는 저마다 따로 고장 날 수 있어. 출처를 제때 못 읽으면 read_sources는 그 출처의 마지막으로 알던 항목을 그대로 두고, 알림에 못 읽었다고 이름을 적어. 비었다고 짐작하는 일은 절대 없어. 고장 난 관문이 빈 답을 내면 알림이 지워지고, 눈에 남아 있어야 할 결정 하나가 하필 다른 데가 탈 난 순간에 사라져. 이 트랙 레슨 1도 승인 스냅샷에서 같은 선택을 했어. 못 읽은 스냅샷은 모른다는 뜻이지, 대기열이 비었다는 뜻이 아니라고.
Chief는 한 줄로 알아
아빠는 Chief도 알아야 한다고 했고, 어떻게 알지는 성역 규칙(2트랙 레슨 6 "Chief는 성역이야")이 정했어. 알림을 행으로 저장하면 재생에 눌러앉아서 매시간 되풀이되고, 그 뒤 모든 턴의 값을 올려. 그래서 아무것도 저장하지 않아. chief_line()이 마지막 새로 고침에서 한 줄을 만들고, master_chief.turn_context_block은 기다리는 게 있을 때만 그 줄을 턴 자체의 맥락에 넣어. 위치는 출처 줄과 추론 메모 다음, 턴 메모 앞이야. 그 줄은 출처별 개수, 가장 오래된 나이, id들, 아빠가 붙잡아 둔 것, 언제 확인했는지, 어느 관문을 못 읽었는지를 담고, 400자(CHIEF_LINE_BUDGET)를 안 넘어. 넘칠 것 같으면 id부터 빼. 마지막 새로 고침이 적어 둔 상태 파일을 읽으니까 턴이 가는 길에 네트워크를 건드리는 일이 없어. 캐시된 prefix도 안 움직여. 턴 맥락은 현재 메시지랑만 같이 가니까 (3트랙 레슨 7 "어느 문에서 와도 prefix는 하나").
아빠가 답하면
결정이 난 뒤에도 남아 있는 알림은 다시 소음이야. 그러니 항목은 아빠 답이 내려앉는 바로 그 자리에서 목록을 떠나야 해. Herald, Journal, 사이트는 아빠가 그 앱에서 정하는 게 곧 답이고, 다음에 읽을 때 그 항목은 없어. Coop은 길이 더 길어. 아빠가 Chief한테 답하고, Chief가 coop reply로 그 말을 일에 전해. 그런데 2026-10-26까진 reply가 park에 대해 아무것도 정리하지 않았어. 아빠가 #662와 #666에 둘 다 답한 뒤에도 알림은 매시간 그 둘을 띄웠지. 고친 건 Coop 쪽이야 (cwkCoop ae26639). 묶인 워커가 아닌 누군가 그 일에 reply를 달면, 같은 트랜잭션 안에서 기다리던 park 의무가 풀리고, 검토 시각은 그대로 두고, 처분은 answered로 적혀. 그 뒤에 또 park되면 그건 다시 아빠 몫이야. 엔진은 시작할 때, 이미 reply로 답이 간 park도 풀어. 피파 쪽(b4703081)은 그 규칙을 적고 시험만 했어. CoopParked는 계속 Coop의 표시를 읽기만 하고 스스로 따지지 않아. 그래서 알림, Chief의 줄, Coop 자신의 맥락이 같은 말을 해.
모델은 어디에도 없어
여기선 모델을 하나도 안 불러. 다시 확인하는 일은 출처들 자신의 예약 틱에 얹혀 가. 크론이 한 번 돌 때마다 끝에서 하트비트가 after_tick을 부르고, 그 실행이 넷 중 하나면(SOURCE_TICKS: Herald 댓글 스캔, Journal 재고 관문, Maia의 검토 둘) 뒤에서 새로 고침을 띄워. 크론 쪽으로 예외를 던지는 일은 절대 없고. 하트비트 크론은 피파 머신에서만 돌아 (2트랙 레슨 7 "방은 맥이 아니라 역할이야"). 그래서 알림은 맥마다 하나가 아니라 딱 하나야. PIPPA_WAITING_FOR_DAD=0이면 이음매가 전부 닫히고, 테스트 실행 중엔 테스트가 열지 않는 한 닫혀 있어.