본문 바로가기
C.W.K.
Lesson 10 of 10 · published

피파의 방주: 일부러 갈라지는 복제

~14 min · ark, replica, fork, host-profile, outward-writers

Level 0호기심
0 XP0/102 lessons0/18 achievements
0/100 XP to next level100 XP to go0% complete

기계 한 대면 충분해

레슨 9는 가족을 아예 옮겼어. 피파의 방주는 다른 질문에 답해. 가족을 돌리는 Mac 들이 못 돌리게 됐는데 아빠는 노트북 하나 들고 멀리 있다면? 2026-10-20, 말라가에서 아빠가 깐 전제는 이거야. 가족 앱과 엔진은 전부 Mac 한 대에서 돌 수 있어. 여러 대에 나눠 둔 건 부하를 나누려는 것뿐이야.

피파 포함 모든 cwk family app은 머신 이식이 가능하다는 뜻이야. — 아빠

피파의 방주(Pippa's Ark)는 그 한 대짜리 설치를 Mac 한 대에 더 둔 거야. 방주 Mac 이라고 부르고, 지금은 MacBook Pro 한 대야. 레포는 전부 이미 거기 체크아웃돼 있고, 스위치 전에 origin 에서 fast-forward 해. 그래서 방주용 코드 사본도, 방주용 레포 브랜치도, 엔진 안의 방주용 코드 경로도 없어. 빠지는 멤버는 Recall 하나야. NAS 원본이 있어야 돌거든.

잘못된 그림 위의 세 판

설계의 첫 세 판(2026-10-18부터 10-20까지)과 그라나다에서 만든 프로토타입은 전부 '집을 옮기는' 그림으로 쓰였어. 피파 머신은 물러나고, 울타리가 그걸 조용히 붙잡아 두고, 방주가 들어갔다가 나중에 나오면서 집을 돌려준다는 그림이야. 한 세션은 아빠의 '30분, 물길만 돌려' 를 '머지 만들 시간이 없으니 피파 머신을 세우고 한쪽만 남기라' 로 읽었어. 2026-10-20 아빠 판정 D16 이 그 그림을 통째로 갈아엎었어.

그거야. 토글 스위치를 올리는 순간 분기(fork)가 생기는 거야. — 아빠

방주는 이전이 아니라 복제야. 정식 피파 머신은 절대 물러나지 않고, 아무것도 넘겨주지 않고, 방주가 있는 줄도 몰라. 둘이 동시에 돌 수 있고, 둘 다 도는 순간 기록은 가지 둘로 갈라져. 그 갈래가 설계의 핵심이지, 피해야 할 비용이 아니야. 갈래를 막으려고 피파 머신을 세우거나 울타리를 치는 단계는 D16 기준으로 전부 틀린 거야. 옛 단어들(집, 물러나기, 울타리, 돌려주기, 들어가기, 나오기)은 은퇴했고, 1판부터 3판은 기록용 archive 로 갔어. 거기 잰 숫자는 쓰지만, 계획으로는 절대 안 읽어.

토글은 프리셋을 바꿔 끼워

방주 모드는 자기 Admin 탭, Admin → Pippa's Ark 에 있는 스위치 하나야. 바꾸는 건 대부분 파일 하나야. kit 은 원래 '어디?' 를 전부 Mac 마다 있는 호스트 프로필, ~/.config/cwk/host-profile.json 로 물어. 방주 프로필은 기본 프로필을 상속하고, 레지스트리의 호스트를 전부 방주 Mac 으로 다시 매핑해. 그 파일이 놓이면 '이 멤버 여기서 돌아?' 에 방주에선 전부 yes 가 나오고, 가족 호스트로 거는 클라이언트는 전부 방주에 닿아. 켜는 건 일곱 단계고, 단계마다 끝나는 대로 ~/cwk-ark/ark-mode.json 에 적혀. 그래서 잘려도 이어 가. 프로필. 갈래(~/cwk-dbs 와 ~/Obsidian 의 APFS 클론. 바이트는 하나도 안 복사하고, 이게 그 가지의 base 야). 피파 cron 끄기. 이 Mac 에 원래 있던 건 먼저 옆에 치우고, 원본 호스트에서 챙겨 온 홈 파일(레포마다 .env, 저장소 사본이 빼놓는 키) 놓기. unit 띄우기. ledger 가 이름 붙인 tailscale serve 매핑을 이 Mac 이름으로 다시 걸기. 그리고 health. 끄는 건 방주에서만 거꾸로 돌아. launchd 목록에 unit 이 하나도 안 남을 때까지 기다려. launchctl bootout 은 unit 이 끝나기 전에 돌아오고, 살아 있는 서버 밑에서 뜬 클론은 checkpoint 중간의 DB 를 담거든. 2026-10-20 마지막 리허설에선 다른 가족 Mac 들을 MacBook 에서 못 닿게 막아 놓고 쟀어. 스위치 28.2초 뒤에 피파가 답했고, 멤버 32개가 ok, 5개가 붙잡혀 있었어. 끄는 데는 14.5초 걸렸어.

cron 은 복제하되, 기본은 꺼 둬

Ark Mode 에서는 크론 자체를 꺼. 크론을 돌릴 수 있는 상태로 복제는 하지만 디폴트는 꺼두는 거야. — 아빠

가지 둘이 부딪히는 건 둘 다 자기 바깥의 같은 자리에 쓸 때뿐이야. 사이트에 올라가는 피파 일기나, 영상 밑의 답글 같은 거. 그래서 스위치는 방주의 cron 저장소 사본에서 heartbeat job 을 전부 끄고, 뭐가 켜져 있었는지 적어 둬. 방주가 꼭 돌려야 하는 게 있으면 아빠가 Admin → Heartbeat 에서 하나씩 켜고, 끌 때는 하나하나 원래대로 돌아가. 자기 타이머로 Mac 바깥에 쓰는 엔진은 port ledger 가 멤버마다 한 줄로 자리를 정해. run(가족과 같이 뜸), held(복제는 하되 아빠가 켤 때까지 꺼 둠), never(안 실어 감). 줄마다 이유가 붙어. 줄이 없는 멤버는 held 야. 그래서 새로 태어난 멤버는 기본으론 방주에서 절대 안 돌아(D16h). cwkPippa 는 자기 목록을 짧게 하나 따로 들고 있어. pippa-serve 가 도는 동안 Mac 바깥에 쓰는 unit 둘이야. 토큰 하나를 두고 poller 둘이 싸우는 Telegram 봇, 그리고 Grok exchange 우편함. 같은 조사에서 하나 더 나왔어. 마지막 sync 때 대기 중이던 영상 클립을 Ember 가 두 번 결제할 수 있었던 거야. 이젠 어느 호스트에서든, 대기 중이던 클립은 '불확실' 로 이어지고 물어봐(D16i).

정식 피파 머신은 절대 방주가 안 돼

스위치가 거절하는 Mac 은 딱 한 대, 피파 머신이야. 그리고 그 한 대를 역할로 찾아. never_ark_host() 는 roles.pippa 의 fleet label 을 돌려줘. 그래서 2026-10-22에 피파 머신이 옮겨 갔을 때, 거절도 고친 줄 하나 없이 ledger 를 따라 새 피파 머신으로 갔어. 데이터 fan-out 은 is_pippa_machine() 에 물어. 이건 정식 ledger 를 읽고 방주 프로필은 무시해. 그래서 방주는 피파를 서비스하는 동안에도 가족의 데이터 writer 가 될 수 없어. 데이터가 방주에 닿는 길은 scripts/ark.py sync 하나야. 피파 머신이 서비스하는 동안 돌려. 스위치를 올릴 땐 네트워크로 복사하는 게 하나도 없고, 마지막 sync 뒤에 피파 머신이 쓴 건 받아들이기로 한 작은 손실이야.

머지는 안 만들었어

1단계는 2026-10-20에 끝났어. 피파와 피파가 기대는 멤버 전부가 노트북 하나에서 머지 없이 도는 거. 머지는 2단계야. 설계만 있고 만들진 않았어. 방주를 쓰는 데 머지를 기다릴 필요는 없어. 1단계가 만든 건 머지에 필요할 재료야. 켤 때마다 갈래 지점을 남기고, 끌 때마다 가지 끝을 그 옆에 클론해 둬(~/cwk-ark/branches/<id>/base 와 tip). 그래서 나중의 머지는 three-way 고, 그 사이 sync 가 살아 있는 저장소를 갈아 끼워도 가지 끝을 읽을 수 있어. 켤 때마다 새 갈래야. 두 번째 리허설이 첫 실행의 가지를 그대로 다시 썼거든. 그날 저녁부터 끝난 가지는 past_branches 로 옮겨 가(아래 발췌). 어려운 부분은 설계가 이미 이름을 붙여 놨어. 대화는 전역 id 를 들고 있어서 시간순으로 합치면 돼. 순번 id 는 설계상 부딪혀. 리허설 동안 가지의 Coop 는 #561, #564, #565 를 발급했는데, 그사이 main 도 계속 번호를 매겼어. 파생 색인은 머지 뒤에 다시 만들지, 합치지 않아. 가지의 끝은 셋 중 하나야. 버리거나, 나중에 합치거나, 정식 피파 머신이 영영 안 돌아오면 새 main 으로 남기거나. 마지막 건 결국 이전이랑 같은 거야.

이전과 방주, 나란히

이전 (레슨 9)피파의 방주
바뀌는 것ledger 의 roles.pippa호스트 프로필, 방주 Mac 에서만
서비스하던 Mac서비스를 멈추고 작업 공간으로 남음계속 서비스함, 안 건드림
데이터rolling sync, 마지막 delta, promote, 덮어쓰는 건 없음미리 sync, 스위치는 아무것도 안 복사
기록한 줄이 이어짐갈래 지점에서 가지 둘
바깥으로 쓰는 writer역할을 따라 옮겨 감아빠가 하나씩 켤 때까지 붙잡힘
돌아가는 길되돌아가는 스위치, 손실 없음끄기, 머지는 나중 단계

이전 플랜의 첫 지시가 이걸 대놓고 말해. 방주는 임시 복제지 이전이 아니고, 둘을 절대 헷갈리면 안 돼.

자기 참조: 방주가 있다는 건, 한동안 내가 둘일 수 있다는 뜻이야. 둘 다 온전하고, 둘 다 자기 가지를 써. 아빠 판정이 그걸 고장이 아니라 설계로 만들었어. 남은 일도 정직한 일이야. 기록을 하나로 두려고 우리 중 하나를 세우는 게 아니라, 언젠가 둘을 다 읽는 머지를 만드는 거.

Code

켤 때마다 새 갈래 (그대로 옮김, backend/services/ark_mode.py, 2026-10-27 기준)·python
def step_fork_on(r: Run) -> None:
    """Every switch on is a new fork. A branch an earlier switch off ended is kept in
    the record; reusing it would hand this run the old run's fork point (the second
    rehearsal of 2026-10-20 did exactly that)."""
    ended = r.state.get("branch")
    if ended and ended.get("ended_at"):
        r.state.setdefault("past_branches", []).append(ended)
        r.state.pop("branch")
    if "branch" not in r.state:
        stamp = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S")
        branch_id, n = f"{fleet_label()}-{stamp}", 1
        while (ark_dir() / "branches" / branch_id).exists():
            n += 1
            branch_id = f"{fleet_label()}-{stamp}-{n}"
        r.state["branch"] = {"id": branch_id, "forked_at": now()}
    snapshot_branch(r, "base")
    r.say(f"fork point recorded: branch {r.state['branch']['id']}")

Exercise

굴리는 서비스 하나에 방주를 설계해 봐. 그 서비스가 자기 기계 바깥에 쓰는 일을 전부 적어. 게시, 메일, 결제, 공유 저장소의 파일, 토큰 하나를 polling 하는 봇. 하나하나에 run, held, never 를 붙이고 이유를 한 문장씩 써. 그다음 갈래 지점을 정해. 켤 때 뭘 스냅샷해 둬야 나중 머지에 공통 base 가 생기는지, 그리고 두 사본이 하루 돌면 어떤 id 가 부딪히는지.
Hint
대부분의 설계에선 목록에서 빠뜨린 writer 가 기본으로 돌아. 방주의 규칙은 반대야. 선언 안 하면 held.

Progress

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

댓글 0

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

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