호라이즌앱스 로고
← 기술 노트

'saved'를 찍고 일주일 전 이미지를 내민 AI 파이프라인, 조용한 실패 추적기

임균영조회 —

지난주까지 멀쩡히 돌던 커버 자동생성이 어느 날 일주일 전에 만든 엉뚱한 이미지를 내밀었습니다.

스크립트는 태연했습니다. 로그엔 saved, 종료 코드는 0이었습니다. 시스템은 다 됐다고 말하고 있었습니다.

가장 무서운 실패는 요란하게 터지지 않습니다. 성공한 척하는 실패는 에러도 경고도 없이 결과만 조용히 틀려 있습니다.

이 글은 그 조용한 거짓말을 쫓아 오픈소스 CLI의 소스코드 한 줄까지 내려갔다가, 명령 한 줄로 되살린 기록입니다.

따라가다 보면 exit 0을 믿을 수 없는 이유가 나옵니다. 범인이 제가 손대지 않은 곳에 숨어 있던 사정도 함께 나옵니다.

'saved'가 거짓말이 되는 구조

파이프라인은 단순했습니다. AI CLI에게 이미지를 생성시키면 그 도구가 결과를 특정 폴더에 저장합니다.

그러면 제 스크립트가 그 폴더에서 가장 최근 파일을 집어 옵니다.

허점은 여기 있었습니다. 생성이 조용히 실패해 새 파일이 안 생기면, 가장 최근 파일은 지난주에 만든 옛 이미지가 됩니다.

스크립트는 그걸 집어 들고 saved를 찍습니다. 아무것도 만들지 않았는데 성공했다고 보고하는 셈입니다.

폴더의 최신 파일 같은 부작용으로 성공을 판정하면, 그 부작용이 일어나지 않은 순간 신호가 거짓말을 합니다.

생성이 조용히 실패하면 폴더의 가장 최근 파일은 일주일 전 이미지입니다. 그걸 집어 saved를 찍던 스크립트가, 이번 실행에서 생긴 파일인지 묻는 순간 실패를 실패로 보고합니다.

검증 없는 파이프라인은 거짓말을 퍼뜨립니다. 이번 일에서 처음 배운 게 이것입니다.

증거로 지워 나간 용의자

당연히 처음엔 제 잘못을 의심했습니다. 프롬프트가 이상한가, 경로가 틀렸나, 권한이 없나 싶었습니다.

추측으로 고치기 시작하면 끝이 없습니다. 그래서 증거로 하나씩 지워 나갔습니다.

  • 인증부터 봤습니다. 로그인 상태는 멀쩡했습니다("Logged in"). 지웠습니다.
  • 쿼터도 봤습니다. 유료 계정에 사용량도 10% 남아 있었습니다. 지웠습니다.
  • 작업 디렉터리가 걸렸습니다. 도구가 제 개인 지식 볼트의 설정 파일을 읽고, 엉뚱한 경로로 스스로를 다시 호출하는 재귀 루프에 빠지고 있었습니다. 깨끗한 빈 폴더에서 돌리니 그 증상은 사라졌지만 이미지는 여전히 안 나왔습니다. 부분 원인이었고 본범은 따로 있었습니다.
  • 샌드박스도 막혀 있었습니다. 윈도우 샌드박스가 프로세스를 못 띄우고 "액세스 거부"로 죽었습니다. 우회하니 이번엔 도구가 대놓고 말했습니다. "이 세션엔 이미지 생성 도구가 없습니다."

이 한 문장에서 방향이 정해졌습니다. 제 설정은 멀쩡했고, 도구가 이미지 생성 능력 자체를 잃은 것이었습니다.

그것도 헤드리스로 부를 때만 그랬습니다.

소스코드에 있던 게이트 한 줄

도구를 최신 버전으로 올린 게 화근이었습니다. 그 버전에 "무료 플랜 계정은 이미지 생성을 막는다"는 변경이 들어가 있었습니다(공개된 PR입니다).

코드로 옮기면 딱 한 줄입니다. 계정 플랜 == 무료 → 이미지 도구 제외. 대화형 앱이든 헤드리스든 같은 경로를 탑니다.

그런데 저는 유료입니다. 왜 걸렸는지 따라가 보니 원인은 어이없을 만큼 사소했습니다.

캐시된 로그인 토큰이 만료돼, 그 안에 박힌 오래된 '무료' 값이 읽히고 있었습니다.

실제 결제 상태와 무관하게, 상한 토큰 하나가 저를 무료 사용자로 둔갑시켰습니다. 같은 증상을 겪은 유료 사용자들의 공개 이슈도 이미 여럿 올라와 있었습니다.

증상은 도구가 사라진 것처럼 보였습니다. 실제로는 정책 게이트와 상한 토큰이 겹친 결과였습니다.

겉과 속이 이렇게 먼 버그는 소스를 안 읽으면 며칠도 헤맵니다.

되살리는 길은 둘이었습니다.

  1. 재로그인해서 토큰을 새로 받습니다. 지금 유료면 통하지만, 다음 달 무료로 내려가면 또 막힙니다.
  2. 게이트가 생기기 직전 버전으로 핀합니다. 플랜과 무관하게 동작합니다.

저는 플랜에 휘둘리지 않는 두 번째를 골랐습니다.

# 게이트가 들어오기 전 마지막 버전으로 고정
npm i -g @openai/codex@0.145.0

# 그리고 설정 파일(~/.codex/config.toml)에 한 줄:
# 조용히 최신으로 다시 올라가지 못하게 핀
check_for_update_on_startup = false

이건 무료 플랜에서 '공짜 이미지 생성'이 되는 이야기는 아닙니다. 클라이언트 게이트를 지워도 서버가 막을 수 있습니다. 유료인데 상한 토큰에 잘못 걸린 저를 되살린 것뿐입니다.

같은 파이프라인을 돌린다면

  • exit 0을 성공으로 믿지 마세요. 부작용으로 성공을 판정한다면 산출물의 신선도, 그러니까 방금 만들어졌는지를 검증에 넣으세요. 이번 실행에서 생긴 파일인지를 물어야 합니다.
  • 도구가 작업 폴더의 설정을 읽고 스스로를 다시 호출하는 재귀 루프에 빠질 수 있습니다. 민감한 자동화는 깨끗한 빈 디렉터리에서 돌리세요.
  • 헤드리스로 CLI를 부를 때 표준입력을 안 닫으면 "입력 대기"로 영원히 멈춥니다. 빈 입력을 파이프로 물려 닫아 주세요.
  • 자동 업데이트는 편하지만 지뢰도 됩니다. 잘 돌던 도구가 소리 없이 업데이트되며 동작을 바꿉니다. 파이프라인이 의존하는 도구는 버전을 고정하고, 올리는 건 의도적으로만 하세요.

그 뒤로 의심하는 순서

오후 하나를 통째로 태운 범인은 남의 업데이트 한 줄이었습니다. 그걸 알았을 때는 허탈함과 안도가 반반이었습니다.

그 뒤로 저는 '성공'이라는 말부터 의심합니다. 비슷한 자동화가 이상하면 이 순서로 봅니다.

  • 성공 신호부터 의심합니다. exit 0도 saved도 증거가 되지 못합니다. 결과가 이번 실행에서 생겼는지까지 확인합니다.
  • 제가 건드리지 않은 곳도 용의자에 넣습니다. 인증, 쿼터, 경로, 샌드박스를 증거로 지우면 남는 쪽이 범인입니다.
  • 오픈소스라면 소스를 엽니다. 게이트 함수 한 줄이 몇 시간의 추측을 끝냅니다.
  • 의존하는 도구는 버전을 핀합니다. 업데이트는 정해 둔 때에만 직접 합니다.

이 조용한 실패는 제가 전에 쓴 스킬 편에서 다룬 "에러 없이 삼켜지는 실패"와 같은 계열입니다.

이어서 읽기