2026-10-30 (금) 개발일지
오늘은 세 곳을 봤다. 하네스는 판정 기준을 두 번 고쳤고, 채용 리서치는 성공했는데 실패로 기록됐고, relay는 10분마다 PC를 끊고 있었다. 셋 다 증상도 원인도 달라 보였는데, 파고들면 같은 질문이 나왔다 — 지금 보고 있는 신호가 정말 그 일을 말하고 있나.
하네스 — 코드를 안 바꿨는데 4점이 흔들렸다, 2런 평균으로 바꾸니 이번엔 다른 게 묻혔다
여러 턴 점수가 이상했다. 이번 기준 측정은 지난 실행이 끝난 상태와 같은 코드였는데, 지난번 마지막이 21점이고 이번 시작이 17점이었다. 아무것도 안 바꿨는데 4점이 움직인 것이다. 그러면 이번 실행에서 17 → 21이 나와도 그걸 개선 효과라고 부를 수가 없다.
첫 가설은 “판정이 너무 예민하다”였다. 그래서 채택 조건을 이렇게 바꿨다.
| 항목 | 이전 | 변경 |
|---|---|---|
| 새 규칙 통과 | 2번 중 2번 | 3번 중 2번 이상 |
| 기존 케이스 회귀 | 2번 중 1번 실패면 회귀 | 3번 중 2번 이상 실패해야 회귀 |
| 새 케이스 재확인 | 없음 | 재측정 실패 시 3번 재실행 |
| 판정 이유 | 가끔 영어 | 한국어 + 케이스별 ○/3 |
이 기준이 실제로 값을 했다. 재측정에서 assumption-gate가 0.75로 떨어졌는데, 3번 다시 돌리니 3번 다 통과했다.
이전 기준이었다면 멀쩡한 개선이 우연한 흔들림 때문에 되돌려졌을 것이다.
그런데 흔들림 자체는 그대로였다. 그래서 여러 턴을 고객 유형별로 2런 평균으로 재도록 바꿨다. 비용이 $35에서 $65로, 시간이 1시간 32분에서 3시간 안팎으로 뛰었다.
결과는 절반만 맞았다. 필라테스가 4.5 → 6.5로 올라 노린 효과(강사 정산)는 나왔는데, 미용실이 8 → 6으로 떨어졌다. 예약 알림 문자와 예약 없이 오는 손님을 2런 모두 놓쳤고, 둘 다 수정 전에는 잡던 항목이다. 가정 목록이 5~8줄로 제한돼 있어서 정산 줄이 들어가며 다른 줄이 밀려난 것으로 보인다.
문제는 판정이 유형별이 아니라 합계라는 점이었다. 19 → 19.5로 통과해버려서 한 유형이 2점 빠진 건 걸러지지 않았다. 최근 채택 4건이 전부 가정 목록에 기본값을 한 줄씩 더한 것이라, 이제는 목록 길이가 고정된 상태에서 항목만 늘어 서로를 밀어내는 단계다. 다음엔 유형별로 1.5점 넘게 떨어지면 되돌리는 조건과, 업종에 맞는 기본값만 보여주는 방식을 넣어야겠다.
자동이력서지원 — 리서치는 성공했는데 실패 알림이 떴다, 끊긴 스트림 너머에 보고서가 있었다
아침에 채용 리서치 실패 알림이 떴다. 그런데 세션 폴더를 열어 보니 10:10에 보고서가 완성돼 있었다. 실패가 아니라 실패로 판정된 것이었다.
단서는 이랬다. 리서치는 08:33에 시작했고, 진행 내용을 받아 오던 aside exec 연결이 10:03에 Replay buffer no longer contains requested cursor로 끊겼다. 오래 돌아서 Aside 쪽 기록 버퍼가 넘친 것으로 보인다. 세션은 연결이 끊긴 뒤에도 계속 돌아 10:10에 보고서까지 썼다.
run.sh는 끊긴 시점까지의 출력에서만 <<<REPORT 블록을 찾고 있었다. 거기엔 보고서가 없으니 실패로 판정했다. 프로그램 자체는 exit 0으로 정상 종료돼 있었다.
해법은 두 갈래였다. 우선 세션의 messages.jsonl에서 마지막 REPORT 블록을 꺼내 reports/routine_2026-10-30.md로 저장했다(후보 18곳). 수정 전 파일은 run.sh.bak_1030으로 남겼다.
검증은 끊긴 출력을 그대로 넣고 복구 로직만 따로 돌리는 방식으로 했다. 위 보고서와 같은 내용이 나왔다.
같이 걸린 게 하나 더 있었다. 리포트가 평소보다 네 배 넘게 걸린 이유다. 기록된 작업 시간을 다 더하면 25분(모델 7분, 도구 18분)으로 어제 21분과 비슷한데, 실제로는 97분이 걸렸다.
- 56분(08:34 → 09:30): 응답이 오지 않고 멈춤. Aside 로그에
terminated후 자동 재시도로 남아 있다. - 16분(09:31 → 09:47): 같은 증상,
Request timed out.후 재시도.
멈춘 시간대에 브라우저 확장 연결이 다섯 번 끊겼다 붙었고, Aside 데몬이 그날 새 빌드로 바뀐 상태였다. 내 쪽에서 고칠 수 있는 부분이 아니라 복구 로직으로 대응하게 두고, 대조 명령 쪽은 “중복 확인은 직전 보고서로만” 규칙으로 줄이기로 했다.
덤으로 주말 차단도 넣었다. 원래 예약은 월~금 08:30뿐이지만, 금요일에 맥이 잠자기였다가 주말에 깨어나면 launchd가 놓친 작업을 그때 실행한다. run.sh 맨 앞에서 주말이면 바로 끝내도록 하고, 금요일은 실행·토요일은 건너뛰는 것을 테스트로 확인했다.
lepons relay — 10분마다 PC가 끊기던 이유, “한 번이라도 페어였나”를 안 물었다
iOS는 백그라운드에서 WebSocket을 열어 둘 수 없다. 앱이 뒤로 가면 소켓이 끊긴다. 그런데 relay의 park 함수는 10분 시점에 partner(id, c) == nil만 봤다 — 한 번이라도 페어였는지를 몰랐다.
그래서 평상시 대기 경로가 이렇게 됐다.
PC 연결 → 폰 페어링 → 폰이 백그라운드로 가며 정상 종료 → PC 혼자
10분 경과 → partner == nil → 4002 "no peer"로 PC 끊김
→ 재연결 → 10분 뒤 또 끊김 ...
게다가 클라이언트의 4002 처리 문구가 “PC에서 QR을 다시 찍어 주세요”였는데, 이 경우엔 항상 틀린 안내다. 방 ID는 영구적이고 멀쩡하다.
여기서 한 번 삽질했다. “한 번이라도 페어였나” 기록을 넣었는데, 그 기록이 me := &conn{...}로 연결마다 새로 만들어져서 연결이 바뀌면 초기화됐다. 아침에 PC를 켜고 폰이 백그라운드에 있으면 여전히 10분마다 끊겼다.
근본 원인은 규칙 자체에 있었다. 방 ID가 영구적이고 폰이 백그라운드에서 연결을 끊으면, PC가 상대 없이 몇 시간을 기다리는 게 정상이다. relay는 “유효한 방에 상대가 잠깐 없는 것”과 “아무도 안 쓰는 가짜 방”을 구분할 방법이 없다. 원래 이 규칙은 가짜 방의 자리 점거를 막으려던 건데, 공격자는 10분마다 다시 붙으면 그만이라 실제로 막지도 못하고 있었다.
해법은 park과 4002를 통째로 지우는 것이었다. 실질적인 방어는 IP당 6개, 전체 200개 한도이고 그건 그대로 남는다. 상대 없는 PC를 11분 동안 붙여 두는 테스트를 넣었고, 예전 규칙이었다면 10분에 끊겼어야 할 연결이 유지되는 걸 확인했다.
이 섹션에서 나온 다른 것들도 성격이 같았다.
1 MiB가 Cloudflare를 통과하는데 못 통과한다고 오판했다. 공개 경로 첫 테스트에서 7개가 실패해 Cloudflare의 WebSocket 메시지 크기 제한을 의심했다. 그런데 서버 로그가 답을 줬다 — room=a1b2c3 in=1048582 out=1048582, room=eeeeee in=1048577 out=0 code=1009. 1 MiB가 전부 건너갔고 초과 거부도 제대로 작동했다. 내 테스트가 루프백 기준 250ms/1200ms만 기다리고 있었고, Cloudflare 왕복은 ~1초였다. 대기시간을 거리에 맞춰 6배로 늘리니 20/20이 나왔다.
README의 health 확인 명령이 틀렸다. docker compose exec cloudflared wget ...을 돌렸더니 exit 127에 exec: "wget": executable file not found in $PATH. cloudflared 이미지에는 셸도 wget도 없다. 공개 경로 curl과 docker run --rm --network ... busybox wget 두 가지로 문서를 고쳤다.
메시지 중간에 끊기면 받는 쪽이 망가지는 버그도 리뷰에서 나왔다. relay()가 io.CopyBuffer 실패 시 wc.Close()를 부르지 않고 반환해서, 받는 쪽에 FIN 없는 미완 메시지가 남고 msgWriter.mu가 잠긴 채 남는다. ping/pong은 writeFrameMu를 쓰므로 소켓은 멀쩡해 보이고 방 자리는 계속 물고 있다. 폰이 1 MiB를 넘긴 메시지를 하나 보내면 PC는 1,048,577바이트짜리 끝나지 않는 메시지를 받고, peer-left가 잠긴 writer에서 10초 막힌 뒤 버려진다. 두 오류 분기에서 상대를 CloseNow()로 끊도록 고쳤고, 공개 경로에서 0.29초에 끊기는 걸 확인했다.
smoke 20/20은 상대가 없는 방에서만 1009를 확인해서, 이 치명 경로를 한 번도 지나가지 않았다.
lepons cockpit — SSH 키 하나가 빠지자 레일 전체가 비었다
왼쪽 PC·폴더 목록 패널이 통째로 안 뜨는 증상이 있었다. 1.7.5에도 있던 버그다. 시작할 때 등록된 모든 SSH PC의 키를 미리 읽고 있었고, 키 하나가 없으면 그 시점에 에러가 났다. 그 시점엔 내부 연결이 아직 서 있지 않아서 목록 자체가 만들어지지 않는다. PC 하나의 문제가 전체 목록을 죽인 것이다.
각 PC의 키를 그 PC를 실제로 쓸 때 읽도록 바꿨다. 테스트 549개 중 548 통과(1건은 원래 스킵), 독립 리뷰에서 나온 3건도 같이 고쳤다 — 키 없는 PC 하나가 공유 목록을 비울 수 있는 경로, “다시 묻지 않기” 기록이 지워지는 위치, 첫 연결 질문에 제목이 없던 것.
세로 모니터에서 분할이 안 되던 건 원인이 따로였다. 자동 배치가 오른쪽 분할만 시도하고 있었다. 세로 모니터는 폭이 짧아 오른쪽으로는 안 들어가는데, 높이는 남아 있는데도 거기서 포기했다. 긴 쪽을 먼저 시도하도록 바꾸니 900×1700 화면에서 두 번째 칸이 아래로 쌓인다.
VPN을 껐다 켤 때마다 호스트 키 팝업이 뜨던 건 세 번째 문제였다. VPN이 꺼지면 등록된 주소에 다른 기기가 응답하거나 아무것도 없는데, relay는 그걸 “PC가 교체됐다”와 구분할 수 없다. 문제는 시작 시 세션 복원이 직접 연결로 계산돼서 재시작마다 다시 물었다는 점이다. “연결하지 않고 다시 묻지 않기” 버튼을 넣었다. 자동으로 새 키를 신뢰하는 옵션은 넣지 않았다 — 그건 VPN 꺼진 상태에서 다른 기기에 로그인하는 셈이다.
덤으로, 시작 직후엔 화면이 아직 로딩 중이라 Lepons 다이얼로그 대신 Windows 기본 창으로 뜨고 있었다. 로딩이 끝날 때까지 기다렸다가 앱 다이얼로그로 띄우고, 그 사이 창이 닫히면 “연결하지 않기”로 처리하도록 고쳤다.
오늘의 한줄
4점 흔들리는 점수, exit 0으로 끝난 실패 판정, 지나가지 않은 경로 위의 20/20 — 초록불이 무엇을 지나온 초록불인지 확인하지 않으면 셋 다 똑같이 거짓말을 한다.