개발일지
Debugging·2 min read

크론은 매일 돌았는데 라이브 배지는 멈춰 있었습니다

0to1 /lab의 eval 라이브 배지가 며칠째 안 움직였다. 크론 로그엔 매번 성공이라 찍혀 있었다. 원인은 코드가 아니라 launchd가 주는 최소 PATH였다. node가 경로에 없어서 배지를 찍는 스크립트만 조용히 실패하고 있었다.

0to1의 /lab 페이지에는 매일 도는 것들의 상태 배지가 있다. 그중 하나가 새김 RAG 평가기(eval)의 데일리 heartbeat다. 하루 한 번 축소 표본으로 평가를 돌리고, 마지막 실행 시각과 점수를 배지에 찍는다. 살아 있다는 증거다.

그게 07-17에 멈췄다. 배지에 찍힌 시각이 며칠째 그대로였다.

크론은 성공이라고 했다

제일 먼저 크론 로그를 봤다. 매일 밤 잘 돌고 있었다. 평가도 돌았고, results/daily.jsonl에 점수도 하루 한 줄씩 쌓이고 있었다. 종료 코드도 0이었다. 로그만 보면 아무 문제가 없었다.

여기서 잠깐 헷갈렸다. 데이터는 쌓이는데 배지만 안 움직인다면, 배지를 읽는 쪽이나 배포 쪽 문제인가 싶었다. 프론트를 열어보려던 참이었다.

그런데 데일리 스크립트는 두 가지를 한다. 평가를 돌려서 jsonl에 점수를 쌓는 것, 그리고 그 결과로 라이브 배지용 json을 갱신하는 것. 앞엣것은 파이썬이고, 뒤엣것은 node 스크립트다. 데이터는 쌓이는데 배지만 멈췄다는 건, 파이썬은 도는데 node 단계만 죽었다는 뜻이었다.

터미널에선 되고 크론에선 안 됐다

스크립트를 손으로 돌려봤다. 터미널에서는 배지까지 멀쩡하게 갱신됐다. 그런데 launchd가 밤에 돌릴 때만 배지 단계가 안 됐다. 이 조합이면 답은 거의 하나다. 환경 차이, 그중에서도 PATH다.

launchd는 로그인 셸이 아니다. 내가 터미널에서 쓰는 PATH를 그대로 물려주지 않고, 아주 최소한의 PATH만 준다. /usr/bin:/bin 수준이다. 거기에는 Homebrew로 깐 것들이 사는 /opt/homebrew/bin이 없다. node가 그 안에 있다.

같은 스크립트, 다른 PATH

터미널에서
로그인 셸이 /opt/homebrew/bin을 PATH에 넣어준다. node를 찾는다. 배지까지 갱신된다.
launchd에서
최소 PATH만 준다. /opt/homebrew/bin이 없다. node를 못 찾는다. 배지 단계만 조용히 실패한다.

그러니까 파이썬은 시스템 경로 안에 있어서 돌았고, node는 Homebrew 경로에 있어서 못 돌았다. 한 스크립트 안에서 앞 절반은 성공하고 뒤 절반은 실패한 거였다. 그런데 스크립트는 그 실패에 걸려 넘어지지 않고 끝까지 갔으니, 종료 코드는 0이었고 로그는 성공이라고 찍혔다.

고친 건 한 줄이었다

스크립트 맨 위에 PATH를 명시적으로 박았다.

export PATH="/opt/homebrew/bin:$PATH"

이 한 줄이면 launchd가 주는 최소 PATH 앞에 Homebrew 경로가 붙는다. node를 찾는다. 배지가 다시 움직였다. 커밋 77d98730이다.

같은 밤에 데일리 비용도 손봤다. 교차 채점하던 OpenAI 모델을 gpt-5.5에서 gpt-4o-mini로 내리고, 표본을 정상 4개에 함정 2개, control 1개로 줄여서 7케이스만 돌게 했다. 매일 도는 heartbeat는 "돌아간다"만 확인하면 되니까, 비싼 모델과 전체 표본은 수동 쇼케이스 전용으로 뺐다.

남은 것

이번 건은 코드 로직이 멀쩡했다. 평가도 맞게 돌았고 점수도 정확했다. 틀린 건 그 코드가 실행되는 환경이었다. 로그가 성공이라고 말해서 며칠을 코드 쪽에서 헛짚을 뻔했다.

배운 걸 두 개로 정리해 둔다. 터미널에서 되던 스크립트가 크론에서만 안 되면 PATH를 제일 먼저 본다. 그리고 자동화의 "성공했다"는 스크립트가 끝까지 갔다는 뜻일 뿐, 중간의 한 단계가 조용히 죽어도 그렇게 찍힐 수 있다. 정말 살아 있는지는 로그가 아니라 결과물(여기서는 움직이는 배지)로 확인해야 한다.