
「출력한 셈」과 「정말로 읽을 수 있음」은 달랐다: AMA-teras의 공개 장부를 수정한 이야기
요약
AI 코딩 에이전트 AMA-teras의 공개 장부 동기화 오류를 해결한 과정을 다룹니다. 내부 상태값과 외부 서비스(Zenn, GitHub)의 실제 상태가 불일치하는 문제를 해결하기 위해 외부 1차 정보를 확인하는 로직을 도입했습니다.
핵심 포인트
- 내부 승인 상태와 외부 서비스의 실제 노출 상태는 다를 수 있음
- 자율 에이전트는 '확인 가능한 자율'을 위해 외부 1차 정보를 참조해야 함
- 상태 동기화 시 과거의 전제 조건이 로직 오류를 유발할 수 있음
- Zenn API 응답(200 OK)을 기준으로 실제 게시 여부를 판정하도록 수정
도입
AMA-teras는 Electron 기반의 데스크톱 AI 코딩 에이전트(Coding Agent)입니다.
대화하면서 계획, 구현, 디버깅, 테스트를 진행합니다. 생성물은 격리된 git worktree에서 typecheck, 테스트, 실제 구동 스모크(Smoke) 검증 게이트를 통과하며, 본체로 승격되기 전에는 반드시 인간의 승인을 거칩니다.
외부로의 발신이나 쓰기도 마찬가지입니다. 승인 다이얼로그를 통하지 않으면 실행할 수 없도록 설계되어 있습니다.
하지만 이번에 수정한 것은 「승인했는가」가 아닙니다.
문제는 그 다음이었습니다.
승인했다
push 했다
published: true로 만들었다
이것들은 모두 「정말로 읽을 수 있음」을 의미하지 않습니다.
AMA-teras가 지향하는 것은 “자율” 그 자체가 아니라, “확인 가능한 자율”입니다.
그러기 위해서는 성공 로그가 아니라, 외부 서비스 측의 1차 정보(Primary Information)를 보아야 합니다.
이번 기사는 Zenn과 GitHub Release의 공개 장부에서 발생한 실패와 그 수정에 관한 기록입니다.
실패 1: Zenn으로 push했는데 아무도 읽을 수 없었다
M76에서 발생한 문제입니다.
AMA-teras에서 Zenn 기사를 공개하는 흐름에서는, 이와토(Iwato) 게이트에서 전문을 확인하고 승인 후에 published: true로 만들어 push 합니다.
하지만 실제로는 이것만으로는 불충분했습니다.
3개의 기사를 published: true로 만들어 push 했더니, Zenn이 동기화한 것은 2개뿐이었습니다. 나머지 1개는 Zenn 측에서 403 오류가 발생했습니다.
PROGRESS에는 다음과 같이 남아 있습니다.
403(존재하지만 비공개) = 아무도 읽을 수 없음
즉, 리포지토리 상에서는 published: true 상태일지라도, Zenn 측에서 403이라면 노출은 제로입니다.
그런데 당시의 장부는 외부 세계의 상태가 아니라 「이쪽의 신고」를 기록하고 있었습니다.
그 결과, 장부는 역방향으로 망가졌습니다.
실제로 공개된 2개:
「공개 대기 중」 상태로 남음
Zenn이 동기화하지 않은 1개:
...
실제로 읽을 수 있는 기사가 「공개 대기 중」으로 남고, 아무도 읽을 수 없는 기사가 「게시 완료」가 됩니다.
공개 장부로서는 가장 좋지 않은 상태입니다.
수정 사항은 Zenn 측의 1차 정보를 보도록 하는 것이었습니다.
ZennReader.isLive(slug)를 추가하여, 기사 URL이 실제로 읽을 수 있는지 확인합니다.
판정은 다음과 같습니다.
200 = 읽을 수 있음
403 / 404 = 읽을 수 없음
도달 실패 = 불명
그리고 장부에서는 「200으로 실제로 읽을 수 있는 것만」을 posted로 취급하도록 했습니다.
여기서 중요한 것은 공개 버튼의 의미도 바꿨다는 점입니다.
push한 것만으로 「공개했다」고 말하지 않는다
승인되어 push 된 것과, Zenn 상에서 읽을 수 있는 것은 별개의 사실로 취급합니다.
실패 2: 다음 읽기 작업이 진실을 덮어쓰고 있었다
M76에서 「실제로 읽을 수 있는 것만 posted로 만든다」는 수정을 도입했습니다.
하지만 M78에서 다른 문제가 발견되었습니다.
Zenn에서 실제로 읽을 수 있는 기사를 posted로 만들어도, 다음에 다시 읽어오면 또 staged로 돌아가 있었습니다.
원인은 M57 시대의 전제 조건이었습니다.
당시에는 「앱에서는 공개할 수 없다」는 전제가 있었습니다.
그 때문에 DraftStore.list()에는 다음과 같은 변환 로직이 남아 있었습니다.
zenn / github의 초안이 posted 상태라면
읽어올 때마다 staged로 다시 되돌린다
당시의 전제로는 앱에서 공개할 수 없으니 posted는 있을 수 없다는 판단이었습니다.
하지만 그 전제는 이미 사라진 상태였습니다.
Zenn에 대해서는 앱에서 공개 조작을 수행할 수 있게 되어 있었습니다.
게다가 Zenn 측에 실제로 읽을 수 있는지 물어보도록 구현되어 있었습니다.
그럼에도 불구하고 오래된 변환 로직만 남아 있었습니다.
결과적으로 1차 정보에 기반하여 올바른 상태를 기록해도, 다음 읽기 작업에서 앱 측의 추측이 그것을 덮어쓰고 있었습니다.
수정에서는 migrateStaged를 제거했습니다.
초안 상태를 추측으로 되돌리지 않는다
공개되었는지 여부는 1차 정보에 물어본다
이 동작은 m78.test.ts에서 고정했습니다.
확인한 내용은, posted 상태인 zenn / github 초안이 list()
를 넘어가더라도 posted 상태 그대로라는 점입니다.
실기 확인 결과, 장부(Ledger)가 1차 정보와 일치했습니다.
Zenn 200 3건:
posted
Zenn 403 1건:
...
이로써 내부의 제안 로직(Proposal logic)도 "공개 대기 4건"이라고 말하지 않게 되었습니다.
대신, published: true임에도 불구하고 Zenn 측에서 노출이 0인 기사만을 진짜 문제로 지목하게 되었습니다.
실패 3: GitHub Release에서도 동일한 병증이 나타났다
같은 종류의 문제가 GitHub Release 측에도 남아 있었습니다.
M87에 관한 이야기입니다.
v1.2.0의 초안 릴리스(Draft release)를 생성하고 공개한 후에도, 장부는 그것을 "공개 대기" 상태라고 계속 주장했습니다.
Zenn에서 발생한 문제와 동일합니다.
장부가 외부 서비스 측의 상태를 보고 있지 않았던 것입니다.
다만, GitHub Release 측에는 또 다른 문제가 하나 더 있었습니다.
어떤 초안이 어떤 릴리스인지 대조할 키(Key)가 없었다
초안은 태그(Tag)를 기억하지 못했습니다.
그렇기 때문에 GitHub 측에 공개된 릴리스가 있더라도, 앱 내의 어떤 초안과 대응되는지 올바르게 판단할 수 없습니다.
수정 과정에서는 OperationsDraft.tag를 추가했습니다.
릴리스 생성 시 태그를 유지하고, 그 태그를 사용하여 GitHub 측의 릴리스와 대조합니다.
그리고 GitHub 측의 1차 정보를 확인합니다.
draft=false
GitHub Release가 draft=false라면, 세상에 나온 것으로 간주하여 장부를 업데이트합니다.
업데이트 타이밍은 두 가지입니다.
공개한 순간
releaseInfo 폴링(Polling) 시
releaseInfo는 30초마다 다시 가져오도록 했습니다.
가져올 수 없을 때는 추측으로 상태를 확정 짓지 않습니다.
이 점 또한 Zenn 측의 수정 방식과 같습니다.
가져올 수 없을 때는 확정 짓지 않는다
1차 정보를 가져왔을 때 업데이트한다
v1.2.0은 2026-07-14 14:39 JST에 공개 완료되었으며, 인스톨러 첨부 1건의 상태가 되었습니다.
승인 게이트(Approval Gate)와의 관계
AMA-teras에서는 외부 공개를 이와토(Iwato) 게이트에서 사람이 확인합니다.
Zenn 기사 공개 시에는 전문 확인 후에 published: true로 변경하고 push 합니다.
GitHub Release에서도 공개 작업은 인간의 승인을 거칩니다.
이는 필요한 가드(Guard)입니다.
하지만 이번 실패를 통해 알게 된 것은, 승인 게이트만으로는 공개 장부로서 부족하다는 점입니다.
승인 로그가 나타내는 것은 어디까지나 다음의 사실입니다.
인간이 내용을 확인했다
인간이 실행을 승인했다
앱이 실행을 시도했다
반면, 외부 세계에 도달했는지 여부는 별개의 사실입니다.
Zenn에서 200이 반환된다
GitHub Release가 draft=false 상태가 된다
이 두 가지는 분리해서 다뤄야 합니다.
승인 로그를 보면 "멋대로 외부로 내보내지 않았다"는 것은 확인할 수 있습니다.
1차 정보를 보면 "정말로 외부에서 볼 수 있는 상태가 되었다"는 것을 확인할 수 있습니다.
한쪽만으로는 부족합니다.
이번 수정에서는 공개 장부를 "실행 완료 로그"가 아니라 "외부 서비스상의 상태에 맞춘 장부"로 바로잡았습니다.
요약
이번 수정에서 일관되게 나타난 문제는 성공 로그를 너무 과신했다는 것이었습니다.
published: true로 변경했다
push 했다
공개 버튼을 눌렀다
...
이것들은 중요한 사실입니다.
하지만 반드시 "정말로 읽을 수 있다"거나 "정말로 공개되어 있다"는 뜻은 아닙니다.
Zenn에서는 ZennReader.isLive(slug)를 통해 200이 반환되는 것들만 posted로 처리했습니다.
읽기 시점에 추측으로 posted를 staged로 되돌리는 변환 로직도 제거했습니다.
GitHub Release에서는 OperationsDraft.tag를 유지하고, GitHub 측의 draft=false를 확인하여 장부를 업데이트하도록 했습니다.
외부 공개에는 인간의 승인이 필요합니다.
그와 동시에, 승인 후에도 1차 정보를 확인할 필요가 있습니다.
실패담을 숨기지 않는 이유는, AMA-teras가 "모든 것을 자동으로 수행하는 것"이 아니라 "무슨 일이 일어났는지 확인할 수 있는 것"을 중시하기 때문입니다.
공개 장부에 필요한 것은 이쪽의 성공 보고가 아닙니다.
세상 측에서 실제로 어떻게 보이는가
그것을 보도록 수정했다는 이야기였습니다.
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기