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

마크다운은 처음부터 여럿이 쓰는 형식이 아니었어

~14 min · concurrency, sqlite, coordination, multi-agent

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

저장소 맨 위의 파일 넷

Coop이라는 이름이 생기기 전, 우리 가족 AI 세션들은 대부분의 프로젝트가 시작하는 방식대로 일을 맞췄어. 저장소 맨 위에 놓인 텍스트 파일로. 할 일은 TODO.md에 적었어. 한 브레인이 다른 브레인한테 부탁하는 건 HELP-REQUEST.md, 넘기는 작업은 DELEGATION-REQUEST.md. 다음에 올 세션한테 남기는 쪽지는 NEW-SESSION-REMINDER.md. 끝난 작업은 history/ 폴더에 하나씩 마크다운 파일로 쌓였고.

세션은 시작할 때 이걸 읽고, 일하면서 고쳤어. Claude Code든 Codex든 저장소를 건드리는 브레인은 전부. 처음엔 늘 그렇듯 잘 돌아갔지. 그러다 안 돌아가기 시작했고, 파일들이 물러나던 날 TODO.md는 811줄, history/엔 파일이 330개였어.

공손하게 부탁만 하던 머리말

옛 TODO.md는 스스로를 Principles + Template Block — IMMUTABLE이라고 소개하는 블록으로 시작했어. 바로 밑엔 "Do not modify or delete", 그리고 "This block itself must always remain at the top, exactly as is." 이 블록은 늘 맨 위에, 지금 모습 그대로 있으라는 거지. 첫 원칙은 이거였어. "Only keep things that are in progress. Delete immediately when done — git handles the changelog." 마지막 원칙은 "The goal is to save tokens + save mental energy."

두 사실을 나란히 놓고 봐. 진행 중인 것만 두라는 게 첫 규칙인 파일이 811줄까지 자랐어. IMMUTABLE이라는 단어는 부탁이었어. 주석 안에 적혀서, 앞으로 이 파일을 열 모든 작성자한테 건네는 부탁. 그 부탁을 어기는 작성자를 파일은 하나도 막을 수 없었고. 문제 전체가 여기 축소판으로 들어 있어. 보장이란 보장은 전부 산문에 있었고, 산문은 거절을 못 해.

두 번째 원칙도 눈여겨봐. "끝나면 지워라"는 끝난 일이 판에서 아예 사라진다는 뜻이야. 파일은 누가 열려 있다고 생각하는 일은 보여줘. 근데 뭐가 끝났는지, 누가 끝냈는지, 언제였는지는 못 보여줘. 지운 기록은 git이 기억하지만, 지금 돌아가는 일을 맞추는 사람이 diff를 읽진 않잖아.

실패 다섯 가지

이 파일들을 대체한 설계 문서는 망가진 방식을 딱 다섯으로 꼽아.

  • 저장소 안에 산다. 고칠 때마다 커밋이야. 여러 브레인이 동시에 고치면 머지 충돌이 나고 작업 트리끼리 어긋나.
  • 금방 불어난다. 모든 세션이 매번 파일 전체를 컨텍스트 창에 올려. 자기 일과 상관없어도.
  • 잘못 고쳐진다. 브레인들이 어느 항목이 어느 건지 놓치고, 엉뚱한 항목을 다시 쓰고, 줄을 날려. 문서 표현 그대로 옮기면 Markdown was never a multi-writer format.
  • 상태가 없다. "읽었나?" "확인했나?" "진행 중인가?" "끝났나?"가 없어. 보낸 쪽과 받은 쪽이 주고받을 스레드도 없고.
  • 끝없이 쌓인다. history 폴더는 읽는 것 자체가 신입 교육 부담이 되는 선을 넘었어.

텍스트 파일이 왜 틀린 도구인가

셋째 실패는 구조를 뜯어볼 만해. 기강 문제가 아니라 기계적인 문제거든. 텍스트 파일을 저장하면, 작성자가 아까 읽어둔 사본으로 계산한 내용이 파일 전체를 덮어써. 읽고, 고치고, 쓰는 사이에 비교하는 단계가 없어. 두 세션이 같은 바이트를 읽고, 각자 고치고, 각자 저장하면 두 번째 저장이 첫 번째를 지워. 어느 저장도 실패하지 않아. 진 쪽 세션은 졌다는 얘길 영영 못 들어.

게다가 항목 자체엔 기계가 붙잡을 정체성이 없어. 누가 위에 한 줄 끼워 넣는 순간 "세 번째 항목"은 세 번째가 아니게 돼. 누가 문구를 바꾸는 순간 "fix the backup script라고 적힌 줄"은 더는 안 맞고. 문자열을 찾아 바꾸는 AI 편집 도구는 이걸 더 나쁘게 만들어. 정확한 옛 텍스트가 있어야 하는데, 그 옛 텍스트가 바로 옆 세션이 방금 바꾼 거거든.

여럿이 쓰는 조율 장소에 실제로 필요한 건 짧아.

  1. 항목마다 변하지 않는 정체성. "todo #7"이 누구한테나, 언제까지나 같은 걸 가리키게.
  2. 문장이 아니라 필드로 된 상태. 그래야 "taken"을 해석하지 않고 검사할 수 있어.
  3. 실패할 수 있는 쓰기. "아직 open이면 taken으로 바꿔" 같은 쓰기. 그리고 이겼는지 작성자한테 알려주는 쓰기.

데이터베이스 행 하나면 셋 다 돼. 아래 실행 가능한 스케치는 같은 경쟁을 텍스트 파일과 원장 행에서 각각 벌여봐. 파일은 마지막에 저장한 쪽만 조용히 남겨. 조건을 건 UPDATE는 딱 한 세션만 이기게 하고, 진 쪽엔 잘못 읽을 수 없는 0행을 돌려줘.

부트스트랩

2026-05-12, cwkPippa 커밋 2616ed8a("Bootstrap Pippa Coop System")가 루트 파일 넷을 전부 새 명세를 가리키는 안내판으로 바꿨어. 셋은 아홉 줄짜리 폐기 안내문이 됐어. TODO.md는 811줄에서 9줄로 줄었고, 옛 내용은 git 기록에 남았어. 세 줄짜리 빈 자리였던 NEW-SESSION-REMINDER.md는 오히려 쉰 줄로 늘었어. 새 편지 의식을 가르치는 안내판이 됐거든. 그 자리에 들어온 건 저장소 바깥의 SQLite 데이터베이스 하나, 작은 헬퍼 모듈, 명령줄 껍데기. 처음엔 테이블 넷이었어. todos, requests, messages, history_entries. history 파일들은 다음 단계에서 따라왔고.

그 부트스트랩의 두 선택이 이 코스 나머지를 떠받쳐. 기록을 저장소 밖으로 빼니까 고칠 때마다 커밋하는 세금과 머지 충돌이 한 번에 끝났어. 그리고 데이터베이스는 WAL 모드(write-ahead logging)로 돌았어. 읽는 쪽이 쓰는 쪽을 안 막고, 쓰는 쪽도 읽는 쪽을 안 막아. 쓰기끼리는 여전히 하나씩 차례로 가고. Coop은 쓰는 양이 아주 적어. 필요했던 건 처리량이 아니라, 쓰기를 거절할 수 있는 자리였어.

공유 텍스트 파일은 작성자한테 얌전히 굴어달라고 부탁만 할 수 있어. 원장은 거절할 수 있고. 여러 세션이 일을 맞추려면 항목엔 정체성이, 상태엔 필드가, 가져가기엔 질 수 있는 쓰기가 있어야 해. 지면 졌다고 분명히 알려주는 쓰기.

Code

최소 스케치 (실행 가능, 표준 라이브러리만): 같은 경쟁을 텍스트 파일과 원장 행에서·python
import os, sqlite3, tempfile, threading

work = tempfile.mkdtemp()

# --- 1. A shared text file: two sessions "take" the same item -----------
todo_md = os.path.join(work, "TODO.md")
with open(todo_md, "w") as f:
    f.write("- [ ] #7 fix the backup script\n")

def take_in_markdown(who):
    with open(todo_md) as f:
        text = f.read()                   # both sessions read the same bytes
    return text.replace("- [ ] #7", f"- [taken by {who}] #7")

edit_a = take_in_markdown("session-a")
edit_b = take_in_markdown("session-b")
for edit in (edit_a, edit_b):             # A saves, then B saves over it
    with open(todo_md, "w") as f:
        f.write(edit)
with open(todo_md) as f:
    print("markdown:", f.read().strip())
print("  each session saw its own save succeed; nothing told A it lost\n")

# --- 2. A ledger: the same race, settled by one guarded UPDATE ----------
db = os.path.join(work, "coop.db")
setup = sqlite3.connect(db)
setup.execute("PRAGMA journal_mode = WAL")
setup.execute("CREATE TABLE todos (id INTEGER PRIMARY KEY, title TEXT,"
              " status TEXT NOT NULL DEFAULT 'open', taken_by TEXT)")
setup.execute("INSERT INTO todos (id, title) VALUES (7, 'fix the backup script')")
setup.commit()
setup.close()

start = threading.Barrier(2)
results = {}

def take_in_ledger(who):
    conn = sqlite3.connect(db, timeout=10)
    start.wait()                          # both writers fire together
    cur = conn.execute(
        "UPDATE todos SET status = 'taken', taken_by = ?"
        " WHERE id = 7 AND status = 'open'",   # the guard lives in WHERE
        (who,),
    )
    conn.commit()
    results[who] = cur.rowcount == 1           # rowcount is the verdict
    conn.close()

threads = [threading.Thread(target=take_in_ledger, args=(w,))
           for w in ("session-a", "session-b")]
for t in threads:
    t.start()
for t in threads:
    t.join()

for who, won in sorted(results.items()):
    print(f"ledger: {who} won? {won}")
with sqlite3.connect(db) as conn:
    print("ledger row:", conn.execute("SELECT status, taken_by FROM todos").fetchone())
한 번 돌렸을 때 출력 (어느 세션이 이기는지는 돌릴 때마다 달라. 딱 하나만 이긴다는 건 안 달라)·text
markdown: - [taken by session-b] #7 fix the backup script
  each session saw its own save succeed; nothing told A it lost

ledger: session-a won? True
ledger: session-b won? False
ledger row: ('taken', 'session-a')

External links

Exercise

네 작업에서 프로세스든 사람이든 AI 세션이든 둘 이상이 고치는 텍스트 파일을 하나 찾아봐. TODO 목록, 공유 노트, 상태 페이지, 프롬프트 모음 같은 거. 거기 들어가는 항목 종류마다 세 가지를 적어. 무엇이 그 항목을 식별하는지, 상태는 어디 있는지, 작성자 둘이 같은 1초 안에 손대면 무슨 일이 생기는지. 그다음 위 스케치를 돌리고, 두 세션이 같은 항목을 설명하는 서로 다른 줄을 고치게 바꿔봐. 뭔가 눈에 띄는 거 있어?
Hint
"무엇이 식별하냐"에 솔직한 답이 "…라고 적힌 줄"이나 "세 번째 항목"이면 정체성이 위치에 달린 거고, 동시 편집 하나에 얼마든지 밀려나. "상태가 어디 있냐"의 답이 문장 속 단어 하나면, 산문을 읽지 않고는 아무것도 그걸 검사 못 해.

Progress

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

댓글 0

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

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