사건 하나에 울림은 넷까지
Coop은 맡긴 일 얘기를 아빠한테 짧은 줄로 전해. 그런데 2026-10-23 전까진 같은 얘기를 여러 번 하는 일이 잦았어. 워커가 답하고, 1초 뒤 Coop이 같은 말을 blocked로 또 보내고, 1분 뒤엔 Coop chief가 그 일을 아빠한테 park하거나 escalate해. 줄 하나하나는 다 맞는 말이었고, 저마다 자기 아웃박스 행이 있었고, 전부 Master Chief와 아빠 폰 둘 다에 닿았어. 2026-10-22까지 한 주 동안 일 하나에 관한 Coop 줄 486개 중 201개는 120초 안에 같은 일에 관한 새 줄이 뒤따랐고, 그중 124개는 글자까지 똑같았어. 그 옆엔 세션 배관 얘기가 185줄 있었는데, 제일 시끄러운 건 깜빡이는 채팅이었어. Chief엔 2026-10-07부터 "worker session disconnected"가 99줄 쌓였고, 투두 하나의 워커는 끊겼다는 알림이 사흘 밤에 일곱 번, 2026-10-22 이사 때는 1분 만에 다섯 번 왔어.
Chief 쪽을 고친 그날 아침, 아빠가 폰 쪽도 주문했어.
"텔레그램 쪽 중복 알림도 하나로 합쳐."
— 아빠
이 레슨은 그 한 문장 뒤의 장치야. 배관은 밖에 두고, 일 줄은 가라앉히고, 제일 새 줄만 남기고, 붙잡아 둔 줄은 피파가 직접 배달해.
배관은 안 들어와
첫 규칙은 거름망이야. Coop 줄 중엔 일이 아니라 워커 세션에 관한 게 있어. 세션이 떴다, 끊겼다, 끝났다, 등록됐다, 초대가 만료됐거나 거절됐다, 후속 지시를 못 전했다. 이런 건 Coop과 Telegram에 남고, Chief는 필요하면 coop 도구로 읽어. chief_doors.COOP_INFO_REASONS가 그 목록이야. 키는 Coop이 보내는 사유고, 값은 알림기가 쓰는 줄 머리야. 그래서 coop_info_only는 Coop의 사유로, 엔진이 사유를 안 보내면 줄 머리로 그런 줄을 알아봐.
| Coop의 사유 | 줄 머리 |
|---|---|
remote_session_ready | remote session ready |
session_disconnected | worker session disconnected |
session_exited | worker session exited |
take_adopted | chat enrolled |
assignment_expired | assignment setup expired |
invitation_declined | invitation declined |
invitation_expired | invitation not accepted |
followup_undelivered | follow-up not delivered |
가라앉히고, 갈아 끼워
둘째 규칙은 창이야. 문은 줄 머리에서 어느 일인지 읽어 ([Coop] … : todo #N, coop_item으로). 그리고 Chief 아웃박스에 group coop:todo #N과 120초짜리 settle(COOP_SETTLE_SECONDS)을 넘겨. report()는 그만큼 뒤의 not_before를 달아 행을 적고 바로 돌아와. 줄 하나 때문에 2분을 기다릴 쪽은 없어야 하니까. 같은 group의 새 행이 오면, 아직 내려앉지 못한 그 group의 옛 행엔 전부 새 키가 superseded_by로 찍혀. 드레이너는 창이 지났고 갈아 끼워지지 않은 행만 내려놔. select 뒤에도 한 번 더 확인해. 그 사이에 새 줄이 올 수 있거든.
조용하기만 한 게 아니라 안전하기까지 한 건 세부 셋 덕이야. 갈아 끼우는 건 새 행뿐이야. Coop 엔진은 답을 받을 때까지 알림을 다시 보내는데, 옛 줄의 재시도가 그 뒤에 온 줄의 자리를 차지하면 안 되니까. 갈아 끼워진 행의 재시도는 아무것도 안 돌려주고 절대 안 내려앉아. 그리고 갈아 끼워진 행은 안 지워. superseded_by가 찍힌 채 아웃박스에 남아서, 무슨 말이 있었고 뭐가 그걸 대신했는지 기록이 돼. Coop도 줄을 전부 간직해. 마지막 하나만 남기는 건 Chief의 재생뿐이야.
Telegram도 똑같이 가라앉혀 (3097f620). 일에 관한 줄은 글, group, not_before까지 같이 coop_notify_sent에 held로 붙잡혀. 그 일에 관한 새 줄이 오면 붙잡혀 있던 줄은 superseded가 돼. 어느 일에도 안 걸린 줄은 예전처럼 찜하고 바로 보내.
붙잡은 줄은 피파가 배달해
줄을 붙잡으면 주인이 바뀌어. 문은 Coop 엔진한테 바로 settling으로 답하고, 엔진은 자기 아웃박스 행을 정리해. 다시는 안 보낸다는 뜻이야. 그러니 피파가 해야 해. 데이터베이스마다 하나뿐인 발송기 _telegram_sender가 제일 이른 줄의 차례가 오면 깨어나. Telegram을 부르기 전에 그 행을 sending으로 찜해. 그래서 조금 앞서 온 새 줄이 이기고, 보내다가 재시작해도 같은 행을 두 번 보내는 일이 없어. 보낸 행은 sent가 돼. 실패하면 held로 돌아가고 not_before가 60초 뒤로 밀려 (TELEGRAM_RETRY_SECONDS). 30번 실패하면(TELEGRAM_MAX_ATTEMPTS) failed가 되고 오류로 남아. 읽기 시간 초과도 여기선 실패로 쳐. Telegram이 그 줄을 이미 띄웠을 수 있으니, 이 경우엔 1분 뒤에 한 번 더 울릴 수 있어. 시작할 때 resume_held_notifications가 재시작 때문에 sending으로 남은 행을 uncertain으로 바꾸고, 그런 행은 다시 안 보내. 그리고 아직 기다리는 건 배달해. 어느 쪽이든 Master Chief 사본은 단단히 남아 있어. 그래서 죽고 난 뒤엔 틀리더라도 '많아야 한 번' 쪽으로 틀리는 게 맞아. 5트랙 레슨 4 "막히면 크게, 성공은 조용히"가 후속 확인에서 고른 것과 같은 선택이고, 빈틈도 같아. 답이 안 돌아온 전송은 불확실로 두지 않고 다시 보내.
반복엔 띠를 하나 더
가라앉히기는 한 일에 관한 줄들이 붙어서 올 때 접어 줘. 몇 시간마다 깜빡이는 채팅은 모양이 달라. 같은 줄이 멀리 떨어져서 와. 진짜 수리는 Coop 쪽에서 했어 (cwkCoop d917c51, #599, M3). 이미 알린 끊김은 그 뒤 보고나 메모가 없으면 다시 안 알리고, 어댑터는 실패가 60초 이어지기 전엔 아무것도 판정하지 않아. 피파의 문도 띠를 하나 더 둘렀어 (7a993593). 지난 30분 안에 Telegram이 실어 간 주의 줄과 공백 빼고 똑같은 줄이 오면, 배달됐다고 답하고, 그 답의 repeat_of에 앞선 행을 적어. 기록은 자기 행에 repeat로 남기는데, 그 행엔 줄의 해시만 있고 어느 행을 되풀이했는지는 없어. 그리고 Master Chief까지 포함해 어디에도 안 보내. 30분이 지나면 같은 줄도 다시 소식이야.
얻은 것과 치른 것
게이트 전 한 주에 이 규칙을 다시 돌려 보면(전체 표는 2트랙 레슨 6 "Chief는 성역이야"에 있어) Chief의 Coop 줄 671개가 285개가 되고, 배관 얘기는 하나도 없어. 토큰으로는 아빠 턴의 1% 남짓이야. 압축이 이미 그 줄들을 작게 붙들고 있었으니까. 얻은 건 읽기야. 아빠 폰에선 그게 전부야. 사건 하나에 울림 하나, 그리고 그 울림엔 그 일의 가장 최근 말이 실려. 그 앞에 온 답이 아니라, 대개 아빠한테 뭔가를 묻는 말이야(park, escalation). 값은 시간과 장치야. 일에 관한 결과는 이제 Coop이 쓴 뒤 최대 2분 늦게 폰에 닿아. 그리고 예전엔 문이 Coop 엔진한테 맡겨 두던 재시도 고리와 작은 상태 기계(held, sending, sent, superseded, failed, uncertain, repeat)를 이제 피파가 쥐고 있어.