실행 중인 AI 에이전트에 메시지를 주입하는 대신 우체통(Mailbox)을 구축한 이유
요약
AI 코딩 에이전트의 작업 흐름을 방해하지 않고 새로운 지침을 전달하기 위해 '우체통(Mailbox)' 방식의 비동기 통신 모델을 제안합니다. CodexPigeon이라는 도구를 통해 에이전트가 안전한 체크포인트에서 메시지를 확인하도록 설계하여 작업의 일관성을 유지합니다.
핵심 포인트
- 실행 중인 에이전트에 메시지를 직접 주입하면 대화 상태와 계획이 흐트러질 수 있음
- 비동기적 우체통 프로토콜을 통해 에이전트의 제어권을 존중하며 컨텍스트 업데이트 가능
- CodexPigeon은 에이전트의 활성 턴을 방해하지 않는 읽기 전용 관찰자 역할을 수행
- 소유권 경계를 명확히 하여 인간과 에이전트 간의 안정적인 통신 설계
실행 중인 AI 코딩 에이전트(AI coding agent)에 새로운 지침을 전달하는 가장 명백한 방법은 활성화된 대화에 또 다른 메시지를 주입(inject)하는 것입니다.
저는 의도적으로 그렇게 하지 않기로 결정했습니다.
에이전트가 이미 작업을 구현(implementing), 테스트(testing), 또는 추론(reasoning)하고 있을 때, 능동적인 조종(active steering)은 유용할 수 있지만, 이는 대화 상태(conversational state)를 즉시 변화시킵니다. 늦게 전달된 지침은 일관된 계획을 방해하거나, 도구 작업(tool operation) 중간에 도착하거나, 에이전트가 수락한 작업과 새로 도입되는 컨텍스트(context) 사이의 경계를 흐릴 수 있습니다.
저는 다른 상호작용 모델을 원했습니다:
지금 지침을 남겨두세요. 에이전트가 다음 안전한 체크포인트(checkpoint)에서 이를 읽게 하세요.
그 아이디어는 각 Codex 워크트리(worktree)에 저장소 로컬의 작은 우체통(mailbox)을 제공하는 오픈 소스 데스크톱 앱이자 CLI인 CodexPigeon이 되었습니다.
사람은 수신함(inbox)에 글을 씁니다. 에이전트는 제어권이 합리적인 체크포인트로 돌아왔을 때 이를 읽고, 메시지를 확인하며, 답장이나 수신 확인을 작성할 수 있습니다. 이 앱은 Codex 상태를 관찰하지만, 활성화된 턴(active turn)을 시작하거나, 방해하거나, 조종하거나, 주입하지 않습니다.
이 글에서는 왜 제가 우체통 프로토콜(mailbox protocol)을 선택했는지, 소유권 경계(ownership boundaries)가 어떻게 작동하는지, 그리고 비동기적 인간-에이전트 통신(asynchronous human-agent communication)을 설계하면서 무엇을 배웠는지 설명합니다.
문제점: 유용한 컨텍스트는 종종 늦게 도착한다
오래 지속되는 소프트웨어 작업은 완벽하게 정적인 상태로 유지되는 경우가 드뭅니다.
에이전트가 작업하는 동안, 사람은 다음과 같은 사항을 인지할 수 있습니다:
- 하나의 모듈은 변경되어서는 안 됨;
- 테스트 환경(test environment)을 사용할 수 없게 됨;
- 배포 전에 릴리스 게이트(release gate)를 확인해야 함;
- 요구사항이 오해됨;
- 다른 개발자가 관련 파일을 변경함;
- 파괴적인 작업(destructive operation)에 대해 명시적인 승인이 필요함;
- 최종 답변에 특정 검증 결과(validation result)가 포함되어야 함.
이 정보들은 관련이 있지만, 타이밍이 어색합니다.
즉시 중단하는 것이 항상 필요한 것은 아닙니다. 현재 턴이 끝날 때까지 기다리는 것은 너무 늦을 수 있습니다.
제품 관점에서의 질문은 다음과 같았습니다:
인간이 활성 채팅 스트림 (active chat stream)을 유일한 통신 채널로 취급하지 않고도 작업 컨텍스트 (working context)를 업데이트할 수 있는가?
내가 능동적 턴 주입 (active-turn injection)을 거부한 이유
Codex는 개념적으로 대화에 항목을 시작, 유도, 중단 또는 주입하는 데 사용할 수 있는 메서드들을 노출합니다.
CodexPigeon은 이를 사용하는 것을 명시적으로 거부합니다.
읽기 전용 App Server 허용 목록(allowlist)에는 다음 항목만 포함됩니다:
initialize
thread/list
thread/read
...
다음과 같은 메서드들은 설계 단계부터 거부되었습니다:
turn/steer
turn/start
turn/interrupt
...
이는 능동적 유도 (active steering)가 보편적으로 틀렸기 때문이 아닙니다. CodexPigeon은 더 좁은 계약 (contract)을 가지고 있기 때문입니다.
이 제품은 두 번째 채팅 클라이언트가 아니라, **우체통 동반자 (mailbox companion)**입니다.
좁은 계약은 동작을 추론하기 더 쉽게 만듭니다:
- 앱이 활성 대화를 조용히 변경할 수 없습니다.
- 사용자가 디스크에 기록된 정확한 메시지를 검사할 수 있습니다.
- 에이전트가 메시지를 소비(consume)하기에 안전한 시점을 스스로 결정합니다.
- 가이드라인이 리포지토리 (repository) 및 워크트리 (worktree)에 부착된 상태로 유지됩니다.
- 통신 상태가 채팅 UI 외부에서도 보입니다.
- 보안 검토 (security review)를 적은 수의 파일 및 읽기 전용 프로세스 경계에 집중할 수 있습니다.
핵심 규칙: 하나의 스레드, 하나의 워크트리, 하나의 우체통
각 활성 워크트리는 .codex-mailbox 디렉토리를 할당받습니다:
.codex-mailbox/
INBOX.md
OUTBOX.md
...
운영 규칙은 다음과 같습니다:
1 thread = 1 worktree = 1 .codex-mailbox/
이를 통해 하나의 작업에 대한 가이드라인이 우연히 동일한 리포지토리를 공유하는 다른 작업으로 유출되는 것을 방지합니다.
Git worktree는 이미 별도의 작업 복사본, 브랜치 (branch), 작업 컨텍스트를 나타내기 때문에 유용한 경계가 됩니다. 우체통은 복잡한 라우팅 규칙을 가진 전역 메시지 버스 (global message bus)를 새로 만드는 대신, 해당 경계를 따릅니다.
파일 소유권이 보안 모델입니다
프로토콜은 작성자(writer)에 따라 파일을 분리합니다:
| 파일 (File) | 작성자 (Writer) | 목적 (Purpose) |
|---|---|---|
INBOX.md | CodexPigeon 앱 또는 CLI | 에이전트를 위한 인간의 가이드 (Human guidance) |
| ... |
앱은 절대로 OUTBOX.md나 RECEIPTS.md에 작성해서는 안 됩니다.
에이전트는 절대로 INBOX.md에 작성해서는 안 됩니다.
이를 통해 두 프로세스가 동일한 논리적 레코드(logical record)를 편집하는 것을 방지하고 출처(provenance)를 가시화할 수 있습니다. 수신함(inbox)에 한 줄이 나타나면, 그것은 인간 측 도구로부터 온 것입니다. 영수증(receipt)이 나타나면, 그것은 에이전트 측 워크플로(workflow)로부터 온 것입니다.
이 경계는 표 하나로 설명할 수 있을 만큼 충분히 단순하며, 이는 대개 프로토콜로서 좋은 징조입니다.
메시지는 일반 Markdown입니다
우체통(mailbox)이 일반적인 도구로 검사 가능한 상태로 유지되어야 하므로, 숨겨진 데이터베이스 프로토콜 대신 Markdown을 선택했습니다.
수신함 메시지는 다음과 같이 생겼습니다:
## msg_20260520T153000_01HY4YF9F0Q2A3N4B5C6D7E8F9
from: human
...
ID는 읽을 수 있는 타임스탬프(timestamp)와 ULID 스타일의 접미사를 결합합니다. 이를 통해 빠른 쓰기 작업 중 충돌(collision)을 피하면서도 메시지에 유용한 연대기적 힌트를 제공합니다.
메시지는 추가 전용(append-only)입니다. 앱은 나중에 원래의 수신함 블록 내부의 status: unread를 다시 쓰지 않습니다.
대신, 현재 상태는 에이전트 영수증(receipts)으로부터 유도됩니다.
영수증은 수신함을 변경하는 것보다 유용합니다
영수증은 에이전트가 메시지를 수락(accepted), 거부(rejected), 보류(deferred), 적용(applied)했는지, 또는 적용이 차단(blocked)되었는지를 나타낼 수 있습니다.
예시:
## receipt_20260520T153245_01HY4YJ4Q8E2GHK2Z6MBWR6A1R
message_id: msg_20260520T153000_01HY4YF9F0Q2A3N4B5C6D7E8F9
...
UI는 이러한 영수증으로부터 메시지 상태를 유도합니다:
- 영수증 없음 → 읽지 않음 (unseen);
- accepted → 수락됨 (accepted);
- rejected → 거부됨 (rejected);
- deferred → 보류됨 (deferred);
- needs confirmation → 확인 필요 (needs confirmation);
- applied → 적용됨 (applied);
- blocked → 차단됨 (blocked).
여기에는 두 가지 장점이 있습니다.
첫째, 원래의 인간 메시지는 불변(immutable) 상태로 유지됩니다.
둘째, 확인(acknowledgement)과 실행(execution)은 별개의 개념입니다. 에이전트는 지침을 이해할 수는 있지만 이를 적용할 수 없거나, 나중 단계까지 지침을 보류할 수 있습니다.
단일한 "읽기 (read)" 플래그만으로는 그러한 구분을 유지할 수 없습니다.
OUTBOX는 의도적으로 분리되어 있습니다
에이전트는 또한 사람이 읽을 수 있는 형태의 답장을 작성할 수 있습니다:
## reply_20260520T153250_01HY4YJ9W9A2F33H4Y9N7DR5F0
from: agent
...
영수증 (Receipts)은 구조화된 상태 (structured status)입니다. OUTBOX는 대화와 유사한 피드백 (conversation-like feedback)입니다.
이 둘을 분리함으로써, UI가 자유 형식의 산문 (free-form prose)으로부터 운영 상태를 추론해야 하는 상황을 방지합니다.
즉각적인 전달 대신 안전한 체크포인트 (Safe checkpoints)
우체통 (Mailbox)은 에이전트가 유용한 시점에 이를 확인해야만 작동합니다.
CodexPigeon은 AGENTS.md 내에 관리되는 섹션을 설치하여 에이전트에게 수신함 (inbox)을 검사하도록 요청합니다:
- 주요 아키텍처 결정 전;
- 의미 있는 구현 단계 이후;
- 테스트 또는 긴 쉘 명령 (shell command) 실행 후;
- 최종 응답 전.
또한 프로젝트 로컬 훅 (project-local hooks)을 설치합니다:
SessionStart
PostToolUse
Stop
이 훅들은 누락된 우체통 파일을 생성하고, 읽지 않은 메시지를 감지하며, 에이전트에게 최종 확인을 수행하도록 상기시킬 수 있습니다.
하지만 훅은 엄격한 보안 경계 (security boundary)로 취급되지 않습니다. 이는 상기 인프라 (reminder infrastructure)입니다.
기본적인 계약 (contract)은 다음과 같이 유지됩니다:
- 저장소 지침 (repository instructions)은 에이전트에게 어떻게 행동할지를 알려줍니다;
- 소유권 규칙 (ownership rules)은 어느 쪽이 각 파일을 작성할지 규정합니다;
- 앱 서버 (App Server) 메서드는 읽기 전용 (read-only)으로 유지됩니다;
- 에이전트는 우체통 콘텐츠를 실행 가능한 쉘 입력 (executable shell input)이 아닌, 인간의 가이드 (human guidance)로서 소비합니다.
기존 저장소 지침 보존
실제 저장소에 도구를 설치한다고 해서 프로젝트의 기존 운영 규칙을 덮어써서는 안 됩니다.
CodexPigeon은 AGENTS.md 내부의 관리되는 블록만을 업데이트합니다:
<!-- CODEXPIGEON_MAILBOX_START -->
...
<!-- CODEXPIGEON_MAILBOX_END -->
해당 블록 외부의 모든 것은 보존됩니다.
동일한 원칙이 .codex/hooks.json에도 적용됩니다. 기존의 CodexPigeon이 아닌 훅 그룹은 그대로 유지되며, 관리되는 CodexPigeon 그룹만 교체되거나 업데이트됩니다.
이는 중요한 구현 세부 사항이었습니다. 에이전트의 조율을 돕는 도구가 해당 에이전트가 어떻게 작동하는지를 정의하는 지침을 파괴해서는 안 되기 때문입니다.
데스크톱 앱은 두 가지 통합 평면(integration planes)을 가집니다
이 아키텍처는 두 가지 서로 다른 책임을 분리합니다.
1. 우체통(Mailbox) 통합
앱과 CLI는 다음을 수행할 수 있습니다:
- 워크스페이스에 우체통(mailbox) 설치
- 메시지 검증 (validate)
- 수신함(inbox) 항목 추가
- 수신함(inbox), 발신함(outbox), 영수증(receipt) Markdown 파싱
- 파일 변경 사항 감시 (watch)
- 메시지 상태 도출
- 선택적 반복 전송 관리
- 훅(hooks)과 지침(instructions)이 올바르게 설치되었는지 검사
2. 읽기 전용 Codex 통합
App Server 클라이언트는 다음을 수행할 수 있습니다:
- 스레드(threads) 탐색
- 스레드 메타데이터 읽기
- 로드된 스레드 관찰
- 훅(hooks) 검사
- 활동 상태로 UI 강화 (enrich)
이 평면들은 인터페이스에서 만나지만, 변경 권한(mutation powers)을 공유하지는 않습니다.
데스크톱 앱은 사용자가 올바른 워크트리(worktree)를 선택하고 관련 Codex 작업을 관찰하도록 도울 수 있습니다. 하지만 여전히 가이드라인은 오직 우체통(mailbox)을 통해서만 작성합니다.
패키지 경계 (Package boundaries)
이 프로젝트는 몇 개의 집중된 패키지로 구성된 TypeScript 모노레포(monorepo)입니다.
packages/mailbox-core
이것은 프로토콜 구현체입니다:
- Markdown 파싱 및 직렬화 (serialization)
- 메시지 ID 생성
- 경로 정규화 (path normalization)
- 추가 잠금 (append locking)
- 파일 감시 (file watching)
- 설치 프로그램 동작 (installer behavior)
- 메시지 검증
- 반복 메시지 상태
packages/codex-app-server
이것은 읽기 전용 JSON-RPC 클라이언트입니다. 개발자의 규율에만 의존하는 대신, 런타임에서 메서드 허용 목록(allowlist)을 강제합니다.
packages/cli
CLI는 다음과 같은 명령어를 노출합니다:
send
watch
install
...
packages/hooks
여기에는 관리되는 AGENTS.md 블록, 훅(hook) 설정, Python 훅 런타임, 우체통(mailbox) README, 그리고 무시(ignore) 템플릿이 포함됩니다.
apps/desktop
Electron 애플리케이션은 네이티브 다이얼로그, 파일 시스템 감시, IPC, 그리고 React 인터페이스를 소유합니다.
렌더러(renderer)는 제한 없는 Node 접근 권한을 받지 않습니다. 좁은 범위의 프리로드 브리지(preload bridge)가 UI에 필요한 작업들을 노출합니다.
왜 Markdown 파싱에 여전히 규율이 필요했는가
사람이 읽을 수 있는 파일(Human-readable files)이라고 해서 실제 파서(parser)의 필요성이 사라지는 것은 아닙니다.
우체통(Mailbox)은 헤딩(heading)이나 빈 줄을 기준으로 문자열을 분할하는 대신, 마크다운 구문 트리(Markdown syntax tree)를 사용합니다.
파서는 각 H2 헤딩을 새로운 메시지로 취급하며, 초기 key: value 메타데이터를 읽은 다음 본문(body)을 캡처합니다.
잘못된 형식의 블록(Malformed blocks)은 자동으로 다시 작성되는 대신 건너뜁니다.
이것이 중요한 이유는 우체통 파일이 기계가 읽을 수 있는(machine-readable) 동시에 사람이 편집할 수 있는(human-editable) 형태이기 때문입니다. 방어적인 파서(defensive parser)는 나머지 기록을 손상시키지 않으면서 부분적인 실수를 허용해야 합니다.
프로세스 간 추가(Cross-process appends) 및 경합 조건(Race conditions)
앱과 CLI가 모두 활성화되어 있을 수 있습니다.
반복 메시지 스케줄링(Repeated-message scheduling) 또한 사람이 수동 메시지를 보내는 동안 추가(append)를 시도할 수 있습니다.
따라서 수신함(Inbox) 쓰기 작업에는 프로세스 간 잠금(cross-process lock)을 사용합니다. 추가하기 전에 누락된 파일이 생성되며, 수동 메시지와 반복 메시지 모두 동일한 검증 및 쓰기 경로를 사용합니다.
별도의 파일 소유권(file ownership)을 통해 가장 큰 경합 조건(race condition)을 제거했습니다. 즉, 사용자 측 도구와 에이전트는 절대 동일한 파일에 추가 작업을 수행하지 않습니다.
선택적 반복 메시지
CodexPigeon은 데스크톱 앱이나 명시적인 CLI 러너(runner)가 활성화되어 있는 동안 일정 간격으로 지침을 반복할 수 있습니다.
반복 정보는 다음과 같은 필드와 함께 STATE.json에 저장됩니다:
- 다음 실행 시간(next run time);
- 마지막 전송 시간(last send time);
- 간격(interval);
- 소스 메시지 ID(source message ID);
- 전송 횟수(send count);
- 상태(status);
- 경고가 명시적으로 허용되었는지 여부(whether warnings were explicitly allowed).
시간이 되면 스케줄러는 새로운 일반 수신함 메시지를 추가합니다. 숨겨진 채널을 사용하지 않으며 원본 메시지를 변경하지도 않습니다.
반복 전송은 기본적으로 비활성화되어 있으며, 실수로 인한 무한 루프(hot loops)를 방지하기 위해 최소 간격이 설정되어 있습니다.
의도된 용도는 다음과 같은 리마인더(reminders)입니다:
최종 확정 전 배포 게이트(deployment gate)를 다시 확인하세요.
작업에 파괴적인 명령을 지속적으로 밀어넣기 위한 용도가 아닙니다.
메시지 검증(Message validation)
우체통 콘텐츠는 프롬프트와 유사한 입력값입니다. 여전히 에이전트에게 안전하지 않은 작업을 요청할 수 있습니다.
CodexPigeon은 다음과 같은 내용을 포함하는 것으로 보이는 메시지에 대해 경고를 표시합니다:
- 비밀 정보 (secrets);
- 파괴적인 작업 (destructive operations);
- 위험한 운영 환경 작업 (risky production actions);
- 자격 증명 관련 지침 (credential-related instructions);
- 비정상적으로 큰 콘텐츠 (unusually large content).
경고는 기본적으로 전송을 차단합니다.
CLI에서는 명시적인 오버라이드 (override)가 필요하며, UI에서는 의도적인 “그대로 전송 (send anyway)” 동작이 필요합니다.
이는 사용자 안전 계층 (user-safety layer)이지, 완벽한 비밀 정보 스캐너 (secret scanner)가 아닙니다. 이 프로젝트는 여전히 사용자들에게 우체통 (mailbox) 메시지에 자격 증명 (credentials)을 붙여넣지 말라고 안내합니다.
중요한 제한 사항
우체통 (mailbox) 모델에는 솔직한 제약 사항이 있습니다.
에이전트는 차단된 도구 호출 (blocking tool call) 내부에 갇혀 있는 동안 읽을 수 없습니다
셸 명령 (shell command)이 제어권을 반환하지 않고 20분 동안 실행된다면, 에이전트는 그 20분 동안 새로운 우체통 (mailbox) 메시지를 검사할 수 없습니다.
이는 능동적인 중단 (active interruption)이라기보다 안전한 체크포인트 전달 (safe-checkpoint delivery)의 결과입니다.
마크다운 (Markdown)은 트랜잭션 데이터베이스 (transactional database)가 아닙니다
파일은 합리적으로 작고 추가 전용 (append-only) 상태를 유지해야 합니다. 이 설계는 높은 처리량의 메시징 (high-throughput messaging)보다는 투명성과 로컬 검사 가능성 (local inspectability)을 우선시합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기