본문 바로가기
C.W.K.
Stream
Lesson 01 of 05 · published

뭐든 움직이기 전에 먼저 적어둬

~14 min · coordination, durability, state-machines, agents

Level 0메모 쪽지
0 XP0/36 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete

자꾸 되풀이되던 실패 두 개

Coop에 엔진이 생기기 전엔 장부가 습관으로 굴러갔어. 워커 세션이 일을 시작하기 전에 todo take, 끝나면 todo done을 치기로 돼 있었지. 그런데 가끔은 배정받은 일을 take 없이 그냥 시작했어. 더 흔한 건 반대쪽이야. 긴 작업을 끝낼 즈음엔 닫으라는 당부가 이미 저 위로 밀려 올라가 있었거든. 그걸 알아채는 건 늘 아빠였고, 장부는 아빠가 손으로 고쳤어. Almanac에서 직접 하든지, 다른 피파한테 고쳐 달라고 하든지.

당부를 더 잘 쓰면 되지 않나 싶지? Coop 아키텍처는 그 길을 한 문장으로 막아. 당부를 다른 피파 프롬프트로 옮겨봤자 같은 실패가 남는다고. 당부도 결국 모델이 기억해야 하는 거고, 무너진 게 바로 그 기억이었으니까.

아무도 기억할 필요 없는 사실 두 개

그래서 엔진은 사실 두 개를 쥐고 있어. 어떤 모델의 기억과도 상관없이.

  1. 실제로 어느 세션이 정확히 어느 작업 항목을 받았나. 행위자 이름도, 폴더도 아니야. 세션이야.
  2. 받아 간 항목 중에 아직 결과나 다른 결정 기록이 필요한 게 뭔가. 누가 뭔가를 기록할 때까지 계속 빚으로 남는 의무.

작업이 수락 기준을 채웠는지는 여전히 모델이 판단해. 엔진이 보장하는 건 그 판단이 기록될 때까지 빚으로 남는다는 것뿐이야. 이 트랙은 첫 번째 사실을 다뤄. "세션이 있다"와 "세션이 이 일을 가졌다" 사이의 틈. 잘못된 소유 주장은 늘 거기로 새어 들어왔어.

행이 먼저야

아빠가 todo를 배정하면, 워크스페이스에서든 Telegram에서든 피파를 통해서든, 제일 먼저 생기는 건 데이터베이스 행이야. Assignments.create가 멱등 엔진 커맨드 안에서 engine_assignments 행을 prepared 상태로 써. 행에는 항목과 현재 리비전, 벤더, 프로젝트, 모드, 새로 뽑은 무작위 마커, 저장된 셋업 프롬프트, 작업 범위, 추론 강도가 들어가. 이 시점엔 어느 맥에서도 아직 아무것도 안 움직였어.

네이티브 쪽 효과는 전부 그다음이야. 코디네이터는 앱을 열거나 프로세스를 띄우기 전에 행을 awaiting_session으로 옮기고, 이유는 주석에 적어 뒀어. Persist BEFORE the external effect. On restart, discover; never open a second native chat automatically. 백그라운드 실행이 시간 초과로 끝나도 Coop은 다시 띄우지 않아. 그 실행이 띄웠을지도 모르는 세션 하나를 계속 찾아. 요청 하나에 채팅 하나. 재시작을 거쳐도 마찬가지야. Claude new 채팅만은 기다려 주지 않아. office에서 앱을 못 열면, 시간 초과까지 포함해서, 배정은 바로 실패해. 그리고 아빠한테 거기서 앱을 열고 다시 해보라는 말이 가.

세션을 얻는 네 가지 길

모드벌어지는 일
new새 채팅. Claude Code면 office에서 앱 작성창이 셋업 프롬프트가 채워진 채로 열리고, 사람이 Send를 눌러. Codex면 비공개 상주 App Server가 채팅을 만들어 셋업 턴만 돌리고 내려간 다음, Codex 앱이 바로 그 스레드를 열어. Send 누를 사람이 필요 없어.
manual"셋업 브리프 복사." 아무것도 안 열려. Coop이 저장된 브리프를 보여주고, 아빠가 새 채팅의 첫 메시지로 직접 보내. office를 비워서 거기서 Send 누를 사람이 없을 때 쓰는 길이야.
existing이미 열려 있는 채팅. 제목, 폴더, 상태를 보고 골라. 기다릴 게 없으니까 awaiting_session은 건너뛰어.
remoteTelegram이랑 피파의 run이 기본으로 쓰는 길. Claude Code가 claude --bg --remote-control로 백그라운드에서 뜨고, 세션에 Remote Control 링크가 보이면 Coop이 그걸 집어 와. Codex는 new랑 같은 비공개 App Server 부트스트랩을 타.

모드가 뭐든 행이 밟는 상태는 같아. prepared → awaiting_session → enrolling → dispatched → resolved 아니면 declined. 여기에 failed, expired, cancelled가 명시적인 종착 상태로 붙어. 활성 상태 넷은 코드에선 튜플 하나로 묶여 있어. 저장소는 항목 하나가 이 넷 중 어느 상태로든 배정을 하나까지만 갖게 강제해. 수단은 둘이야. 유일 부분 인덱스 하나, 그리고 살아 있는 디스패치가 이미 그 항목을 쥐고 있지 않은지 보는 검사 하나. 버튼을 두 번 눌러? 두 번째 클릭은 충돌로 끝나.

처음 켜지는 추론 강도

작은 필드 하나에도 사연이 있어. 2026-10-10 전까지 Coop은 워커의 추론 강도를 기록만 하고 실행할 땐 안 넘겼어. 그래서 Coop이 띄운 Claude Code 워커는 전부 CLI 기본값인 medium으로 돌았지. 아빠 기준은 high인데 (커밋 adcc5ab). 지금은 launch_effort가 두 벤더 공통의 유일한 해석기야. 요청에 단계가 따로 없으면 high고, 가족의 max는 Codex에선 xhigh로 내려가. Codex 눈금은 거기가 끝이거든. 강도가 닿는 건 Coop이 직접 띄우는 프로세스뿐이야. Claude 백그라운드 세션(remote)은 --effort를 받고, Codex 부트스트랩(new, remote)은 그 단계로 스레드를 만들어. Claude new 채팅은 못 받아. 작성창 링크엔 폴더랑 프롬프트만 실리거든. 그래서 그 채팅은 앱에 설정된 강도로 돌아. 이미 열려 있는 채팅이나 아빠가 manual 브리프로 연 채팅도 마찬가지고.

일은 정확히 그걸 받은 세션이 가져. 디스패치는 가져가지 않아. Coop 아키텍처의 불변식 3이야. 배정 행을 만들든, 앱을 열든, 백그라운드 프로세스를 띄우든, todo 상태는 하나도 안 바뀌어. 이 길에선 특정 세션 하나가 특정 초대 하나를 수락할 때까지 일은 open이야. 채팅이 직접 todo take를 치는 다른 문은 8트랙 몫이고. 그 수락을 어떻게 짓는지가 이 트랙의 나머지야.

Code

assignments.py 발췌(일부 생략): 상태, 모드, 대기 시간, 강도 해석기·python
META = {'id':'dad', 'principal':'assignment-ui'}
ACTIVE = ('prepared','awaiting_session','enrolling','dispatched')
# A new chat is found by the setup line in its first message, whether Coop
# started it on office ('new'/'remote') or Dad sent the brief himself
# ('manual'). Older pending composer launches keep their discovery contract.
FRESH = ('new','manual','remote')
MODES = FRESH+('existing',)
SETUP_TIMEOUT = 900
# ... (NO_PROJECT elided)
# An invitation the receiver never answers is not a claim and must not hold
# the work, the session's retirement or Dad's attention for ever.
INVITATION_TIMEOUT = 1800
# ... (adoption and launch-retry timers elided)
EFFORTS = ('low','medium','high','xhigh','max')
DEFAULT_EFFORT = 'high'


def launch_effort(vendor, effort=None):
    """The one place a requested effort becomes the value a vendor's launch takes.

    Codex has no 'max'; its top level is 'xhigh', so the family's max lands there.
    """
    effort = effort or DEFAULT_EFFORT
    if effort not in EFFORTS:
        raise ValueError(f'effort is one of {", ".join(EFFORTS)}')
    return 'xhigh' if vendor == 'codex' and effort == 'max' else effort
engine_schema.sql에서: 항목 하나에 활성 배정 하나, 저장소가 강제해·sql
CREATE UNIQUE INDEX IF NOT EXISTS engine_one_assignment ON engine_assignments(kind,item_id)
 WHERE state IN ('prepared','awaiting_session','enrolling','dispatched');

External links

Exercise

네 도구 중에 뭔가를 띄우는데, 띄우려 했다는 사실이 어디에도 먼저 남지 않는 곳을 하나 찾아봐. 배포 스크립트든, 잡 실행기든, 에이전트 러너든. 고치는 순서는 이래. 의도를 적은 행(아니면 파일)을 먼저 커밋하고, 효과는 그다음에 내. 재시작하면 효과를 되풀이하지 말고 그 행을 읽어서 찾아가게 해. 그다음 효과가 난 직후에 프로세스를 죽이고 재시작해. 효과가 몇 개 생겼는지 세봐.
Hint
외부 호출을 감싼 재시도 루프가 단서야. 첫 시도가 사실은 성공했는데 응답만 잃어버린 경우에 그 재시도가 뭘 하는지 물어봐. 솔직한 답이 "하나 더 띄운다"라면 효과보다 먼저 적힌 의도는 없었던 거야.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인 — 댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.