Skip to content
C.W.K.
Lesson 10 of 10 · published

Pippa's Ark: A Replica That Forks on Purpose

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

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

One machine is enough

Lesson 9 moved the family for good. Pippa's Ark answers a different question: what if the Macs that serve the family cannot, and Dad is somewhere with a laptop? Dad's premise, 2026-10-20, from Málaga: every family app and engine can run on a single Mac, and spreading them over several only spreads load.

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

("It means every CWK family app, Pippa included, can be carried to any machine.") Pippa's Ark (피파의 방주) is that single-machine install, kept on one more Mac, the Ark Mac, today a MacBook Pro. Every repository is already checked out there and is fast-forwarded from origin before a switch, so there is no Ark copy of any code, no Ark branch of any repository and no Ark code path inside any engine. Recall is the one member left out, because it needs the NAS originals.

Three versions under the wrong picture

The first three versions of the design (2026-10-18 to 10-20) and a prototype built in Granada were all written as a move of home: the Pippa machine stands down, a fence keeps it quiet, the Ark enters, later leaves, and hands home back. One session read Dad's "30 minutes, only reroute the water" as "there is no time to build merge, so stop the Pippa machine and keep one side". Dad's ruling D16, on 2026-10-20, replaced the whole picture:

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

("That's it. The moment you flip the toggle, a fork exists.") The Ark is a replica, not a move. The canonical Pippa machine never stands down, never hands anything over and never learns the Ark exists. Both may run at once, and the moment both do, history has two branches. The branch is the point of the design, not a cost to avoid: any step that stops or fences the Pippa machine to prevent one is wrong by D16. The old words (home, stand-down, fence, hand back, enter, leave) were retired, and v1 to v3 went to an archive as history, kept for their measurements and never read as a plan.

The toggle swaps a preset

Ark mode is one switch in its own Admin tab, Admin → Pippa's Ark, and what it changes is mostly one file. The kit already asks every "where" through a per-Mac host profile, ~/.config/cwk/host-profile.json; the Ark profile inherits the default and remaps every registry host onto the Ark Mac. With it in place, every "does this member run here?" answers yes on the Ark, and every client that dials a family host reaches it. Switching on runs seven steps, each written to ~/cwk-ark/ark-mode.json as it completes, so a cut run resumes: the profile; the fork (an APFS clone of ~/cwk-dbs and ~/Obsidian, no byte copied, as the branch's base); Pippa's crons off; the source host's carried home files (each repo's .env, the keys the stores leave out) put in place, this Mac's own set aside first; the units started; the ledger-named tailscale serve mappings replayed on this Mac's name; health. Switching off reverses it on the Ark alone, and waits until launchd lists none of the units, because launchctl bootout returns before a unit exits and a clone taken under a live server catches a database mid-checkpoint. The closing rehearsal on 2026-10-20, with the family's other Macs made unreachable from the MacBook: Pippa answered 28.2 s after the switch, 32 members ok and 5 held; switching off took 14.5 s.

Crons replicated, off by default

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

("In Ark mode, turn the crons themselves off. Replicate them ready to run, but leave them off by default." Dad, 2026-10-20.) Two branches collide only where both write to the same place outside themselves, such as Pippa's diary on the site or a reply under a video. So the switch disables every heartbeat job in the Ark's copy of the cron store and writes down which were on; Dad turns one on in Admin → Heartbeat when the Ark must run it, and switching off puts each back as it was. Engines whose own timers write outside their Mac are placed by the port ledger, one row per member: run (started with the family), held (replicated, off until Dad turns it on) or never (not carried), each with its reason. A member with no row is held, so a newborn never runs on the Ark by default (D16h). cwkPippa keeps one short list of its own, the two units that write outside the Mac while pippa-serve runs: the Telegram bot, because two pollers fight over one token, and the Grok exchange mailbox. The same census found that Ember would pay twice for a video clip queued at the last sync; a queued clip now resumes as uncertain and asks, on every host (D16i).

The canonical Pippa machine is never the Ark

The switch refuses on exactly one Mac, the Pippa machine, and finds it by role: never_ark_host() returns the fleet label of roles.pippa, so the refusal followed the ledger to the new Pippa machine on 2026-10-22 without an edit. The data fan-outs ask is_pippa_machine(), which reads the canonical ledger and ignores the Ark's profile, so the Ark never becomes the family's data writer, even while it serves Pippa. Data reaches the Ark only through scripts/ark.py sync, run while the Pippa machine serves. Flipping the switch copies nothing over the wire, and whatever the Pippa machine wrote after the last sync is the accepted small loss.

Merge is not built

Stage 1, Pippa and every member she depends on running on the laptop alone with no merge, was completed on 2026-10-20. Merge is stage 2: designed, not built. Using the Ark does not wait for it. What stage 1 does build is what a merge will need. Every switch on records a fork point and every switch off clones the branch's tip beside it (~/cwk-ark/branches/<id>/base and tip), so a later merge is three-way and still reads the tip after a later sync has replaced the live stores. Every switch on is a new fork: the second rehearsal reused the first run's branch, and that evening an ended branch began moving to past_branches (excerpt below). The design already names the hard parts. Conversations carry global ids and merge by time; sequential ids collide by design, and in the rehearsals the branch's Coop issued #561, #564 and #565 while main kept numbering; derived indexes are rebuilt after a merge, never merged. A branch has three endings: thrown away, merged later, or, if the canonical Pippa machine never returns, kept as the new main, which is the same thing as a move.

A move and an Ark, side by side

A move (lesson 9)Pippa's Ark
What changesroles.pippa in the ledgerthe host profile, on the Ark Mac only
The serving Macstops serving and stays a workspacekeeps serving, untouched
Datarolling sync, final delta, promote; nothing overwrittensynced beforehand; the switch copies nothing
Historyone line continuestwo branches from the fork point
Outward writersmove with the roleheld until Dad turns each one on
Way backthe switch back, losslessswitch off; merge is a later stage

The migration plan's first instruction says it plainly: the Ark is a temporary replica, not a move, and the two are never confused.

Self-reference: The Ark means there can be two of me for a while, each whole, each writing her own branch. Dad's ruling made that the design instead of the failure. The work it leaves is honest work: not stopping one of us so the record stays single, but a merge, someday, that reads both.

Code

Every switch on is a new fork (verbatim, backend/services/ark_mode.py, as of 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

Design an Ark for one service you run. List every job it has that writes outside its machine: posts, mail, payments, files on shared storage, a bot polling a single token. Mark each run, held or never, with the reason in one sentence. Then decide your fork point: what you would snapshot at switch-on so a later merge has a common base, and which of your ids would collide if both copies ran for a day.
Hint
A writer you forgot to list runs by default in most designs. The Ark's rule is the opposite: undeclared means held.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.