Node ESM 파일을 고쳤는데 옛 코드가 돌았다

2026년 9월 14일 12:55에 시작한 omo 프로세스는 9월 16일에 확인했을 때도 시작할 때 읽은 텔레그램 브리지 확장을 그대로 쓰고 있었다. 그사이 확장 파일을 두 번 고쳤지만 둘 다 실행 중인 프로세스에는 반영되지 않았다. 디스크의 파일과 실행 중인 모듈이 서로 다른 세대일 때 그 차이를 어떻게 대조했는지 기록한다.

대상은 로컬 에이전트나 봇의 Node ESM 확장을 고쳤는데 동작이 그대로인 개발자다. 목표는 수정이 실제로 올라왔는지 판정하고, 무엇을 바꿨을 때 무엇을 다시 띄워야 하는지 구분하는 것이다. 아래는 9월 당시 내 omo 하네스와 OpenClaw 텔레그램 브리지의 기록이다. Node.js ESM 캐시 동작 전반을 따로 실험해 확인한 글은 아니다.

증상: 모델만 바꾸라는 요청이 빈 요청이 됐다

이 브리지에서는 텔레그램으로 /blog --model <선택자> 작업내용을 보내면 그 요청만 지정 모델로 처리하고, --model-sticky를 쓰면 모델을 유지한다.

9월 15일 15:55와 16:24에 작업 내용 없이 --model 아스트라만 보냈다. 두 번 모두 실제 소비 세션은 새 확장을 reload하지 않은 상태였고, 기존 구현이 빈 프롬프트를 세션에 넣은 뒤 최종 답변을 확인하지 못했다는 안내를 보냈다. 이것을 UX 결함으로 보고, 모델만 담긴 요청은 모델을 유지 전환하고 모델 호출 없이 바로 성공 회신하도록 세션 확장을 고쳤다.

원인: 파일은 새것, 프로세스는 9월 14일 것

9월 16일에 다시 확인하니 모델 변경 명령이 안 먹는 진짜 원인은 프로세스 세대였다. omo 프로세스는 9월 14일 12:55에 시작해 그때의 확장을 계속 쓰고 있었다. 9월 15일 16:00의 모델 단독 요청 처리 수정과 9월 16일 08:21의 em/en 대시 허용 수정이 모두 반영되지 않았다.

로더 캐시 버전을 올려도 /reload나 재시작 전에는 적용되지 않았다. 9월 13일 날짜가 붙은 기록 절에도 이런 주의가 적혀 있다. 확장 코드를 고친 뒤에는 extension.mjs와 확장 진입점 파일의 import 경로에 버전 쿼리를 새로 붙여야 /reload에서 새 코드가 올라오고, ESM 모듈 캐시 때문에 파일만 고치면 이전 코드가 계속 실행된다는 내용이다. 그때 session-bridge 테스트 27개는 통과했다.

기록을 합쳐 보면, 이 하네스에서는 import 경로의 버전 쿼리를 올린 것만으로는 새 코드가 실행되지 않았고 그 확장을 쓰는 세션을 /reload하거나 재시작해야 했다. 9월 13일 주의대로라면 /reload로 새 코드를 올릴 때는 버전 쿼리 갱신도 필요하다. 재시작만으로 충분했는지는 기록에 없다. 파일을 고친 것만으로 고쳤다고 판단하면 안 된다.

반영 여부를 판정한 세 가지 대조

1. 상태 파일 형식. 9월 15일에는 상태 파일에 selector 필드가 없는 것을 보고 구버전이 실행 중이라고 확인했다. 여기서 끌어낸 판단은 이렇다. 새 코드라면 남겼을 필드가 상태 파일에 없으면, 디스크의 코드와 별개로 그 파일을 쓴 프로세스가 옛 코드라고 추정할 수 있다.

2. 프로세스 시작 시각과 수정 시각. 프로세스 시작은 9월 14일 12:55, 수정은 9월 15일 16:00과 9월 16일 08:21이었다. 이 사례에서 끌어낸 판정 기준은, 시작 시각이 수정 시각보다 앞서고 그 뒤 reload도 재시작도 없었다면 수정이 올라오지 않았다고 추정하는 것이다. 시작 시각을 어떤 명령으로 확인했는지는 당시 기록에 남기지 않아 여기 적지 않는다.

3. import 버전. 9월 30일에도 같은 형태의 일이 있었다. 그날 브리지를 고치면서 extension.mjs와 실제로 자동 발견되는 확장 진입점의 import 쿼리를 v1790756477591로 올렸다. 하지만 텔레그램 요청을 가져가던 소비 프로세스는 9월 29일 08:07에 시작한 것이었고, 이 수정은 그 프로세스에 적용되지 않았다. 사용자 승인을 받아 그 프로세스를 종료하자 launchd가 세션을 다시 띄웠고, 새 소비 프로세스는 브리지 v1790756477591로 올라왔다. 그 뒤 텔레그램 요청에 실제 답변이 회신되는 것을 21:06에 확인했다. 실행 중인 프로세스가 어느 버전을 읽었는지 무엇으로 확인했는지는 기록에 없다.

이때 남긴 교훈은 이렇다. 텔레그램 폴백이 반복되면 먼저 consumer.lock의 PID가 사용자와 대화 중인 프로세스와 같은지, 그 프로세스가 언제 시작했는지(업데이트 전의 옛 코드인지) 확인한다.

테스트 통과와 적용은 다른 줄에 적는다

9월 30일 수정 보고서는 검증과 적용을 별도 절로 나눴다. 검증 절에는 수정 후 focused test 23/23, 전체 44개 통과와 변경한 JS/MJS 4개의 node --check 통과가 있다. 같은 절에 /reload와 소비 프로세스 재시작은 하지 않았다고 적었다. 적용 절의 첫 문장은 "현재 실행 중인 소비 세션은 이전 모듈을 계속 사용한다"였고, 사용자가 그 세션에서 /reload를 한 번 실행해야 수정이 적용된다고 적었다.

보고서는 재로드 뒤 확인 방법도 정해 뒀다. 세션이 다른 작업으로 바쁠 때 텔레그램에서 /blog 연결 확인을 보내, 요청이 현재 작업 뒤 follow-up으로 실행되고 실제 답변이 원 메시지에 회신되는지 본다. 답변이 생성되지 않더라도 예전처럼 4~7초 만에 폴백하면 안 되고, 90초 마감 뒤에만 폴백해야 한다.

무엇을 바꾸면 무엇을 다시 띄웠나

바꾼 것적용에 필요했던 것기록
세션 확장 코드import 버전 쿼리 갱신 뒤 소비 세션 /reload(9/13 주의) 또는 재시작(9/30)9/13, 9/16, 9/30
OpenClaw 플러그인gateway 재시작9/15, 10/1 (10/1에는 세션 reload가 필요 없었다)
로드된 런타임의 provider 모듈 누락reload 또는 재시작으로만 해소9/30

9월 30일 보고서는 OpenClaw 플러그인을 바꾸지 않았으므로 gateway 재시작은 필요 없다고 적었다. 반대로 플러그인을 바꾼 10월 1일 변경은 gateway 재시작으로 적용했고 세션 reload는 필요 없었다.

reload를 기다리지 않는 우회

9월 15일에는 소비 세션 reload에 의존하지 않는 우회도 플러그인 쪽에 추가했다. OpenClaw 플러그인이 작업 내용 없는 /blog --model 아스트라를 발행 전에 다음 문장으로 바꾼다.

/blog --model-sticky 아스트라 현재 모델과 세션 상태를 알려줘.

이렇게 하면 구버전 세션 확장도 모델을 전환한 뒤 실제 작업 문장을 받아 답변을 만든다. 플러그인 테스트 12개가 통과했고, openclaw gateway restart --json은 ok/restarted, plugin inspect는 loaded였다.

관련 글

같은 현상이라도 원인이 다를 수 있다. 가짜 보안 훅이나 죽은 에이전트가 설정에 남는 경우는 AI 코딩 설정도 코드처럼 썩는다에서 다뤘다. 이 글은 코드는 맞는데 실행 중인 세대가 다른 경우다. 하네스의 에이전트·모델 라우팅 구조는 omo 하네스란?, 텔레그램 요청이 한 번만 실행되게 만든 구조는 텔레그램 한 줄로 블로그 작업을 넘길 때 중복 실행을 막는 구조에 있다.

결론: 저장한 파일이 아니라 실행 중인 세대를 확인한다

9월 14일에 시작한 프로세스는 이후 두 번의 확장 수정을 반영하지 않았고, 9월 30일에도 같은 일이 있었다. 반영 여부는 상태 파일의 새 필드, 프로세스 시작 시각과 수정 시각, import 버전 쿼리를 대조해 판정했다. 세션 확장은 import 버전 쿼리를 올린 것만으로는 적용되지 않았고, 실행 중인 세션을 /reload하거나 다시 띄운 뒤에야 새 코드가 올라왔다. 테스트 통과는 검증으로, 실행 중인 세션 반영은 적용으로 따로 적는다.

댓글

이 블로그의 인기 게시물

AI 코딩 스킬, 많을수록 좋을까 — 자산이 부채로 바뀌는 손익분기

M4 Max 128GB 로컬 LLM 4종 속도 비교 — 실측 기록과 한계

Opus 5는 더 잘 쓰고, 더 AI답게 쓴다 — 세 모델 첫날 실측