The Question a Mirror Cannot Answer
Track 6 moved bytes one way. delta sync mirrors a source onto a destination, and the rule for a file that exists on only one side is simple because one side is the truth: the source has it, so the destination gets it; the destination has it and the source does not, so with --delete it goes. Nobody asks why the two sides differ. The source is right by definition.
Two Macs that both edit the same folder break that rule on the first difference. Say a note exists on this Mac and not on the other. Two very different histories produce that exact picture:
- Someone created it here, and it has not crossed yet. The right move is to copy it there.
- Both Macs had it, and someone deleted it there. The right move is to delete it here.
Looking at the two trees cannot tell these apart. They are byte-for-byte the same state. A tool that guesses "new" brings every deleted file back on the next run; a tool that guesses "deleted" destroys every new file before it ever crosses. The missing input is not in either tree. It is what the two trees looked like the last time they agreed.
The Archive Is the Memory
Delta's answer is a named pair: op: pair, implemented in delta/pair.py. A pair has a name (letters, digits, _ . -, up to 64 characters), this Mac's folder as src, the other Mac's folder as dst, and an archive in this Mac's Delta state at ~/cwk-dbs/delta-db/pairs/<name>.json. The archive records every file both sides agreed on at the last run:
| Field | What it remembers |
|---|---|
kind | file or link |
size | bytes at agreement |
sha | the SHA-256 both sides held (a link's is the hash of its target text) |
target | a link's target |
mt_here, mt_there | each side's own mtime, in nanoseconds: two Macs never share one clock, so each side is compared with itself |
Beside the files it keeps the folders both sides held, the pair's identity (here, there, exclude), and created / updated stamps. It is written the careful way: to a temporary file, mode 0600, then os.replace over the old one, so a crash leaves either the old archive or the new one, never half of each.
With the archive in hand, the ambiguous note resolves itself. In the archive and missing there: deleted there. Not in the archive and present here: new here. The two trees stop being a riddle once a third, older tree stands beside them.
Only init Writes the First Archive
A pair has three modes: init, sync, verify. The rule that makes the whole design safe is about which one may create an archive: only init, and only after proving the two trees equal. compare_trees lists both sides, compares path, kind and size, then hashes every remaining file on both Macs (the other Mac answers a batched hashes request, up to 1,000 paths each) and compares SHA-256. Any difference stops it with trees_differ and the list of paths: "only on this Mac", "only on that Mac", "differs". Nothing is recorded. Run init on a pair that already holds the same identity and it says so and does nothing.
Why so strict? Picture the tempting shortcut: sync finds no archive, so it writes a fresh one from whatever is there and carries on. Every file present on one side only would then read as new, and cross. If the archive was lost after a deletion, the deleted file comes back. So a sync with no archive refuses with archive_missing, and its message says exactly why: a fresh archive over unequal trees brings deleted files back. The fix is a person's step: compare the trees, settle the differences, run init.
The same caution covers a pair whose roots change. The archive stores the identity it was made for; a run that names other folders or other excludes stops with pair_changed instead of reading one tree's history against another tree.
Where the Rule Came From
The rule is older than Delta. The family's vault pair, two Macs keeping one folder of notes in step, used to run Unison every minute over SSH. When Delta was born on 2026-11-09, Dad was offered the choice of how that pair would continue, and picked the first option:
"1번" — 아빠
("Number one": keep the rule Unison applied, and carry it on our own service.)
So pair.py is not a new sync algorithm. It is Unison's well-tested behaviour (an archive of the last agreement, a conflict copy for the loser, central backups) re-implemented on Delta's routes, tickets and verified transfers, with SSH out of the data path. The commit that landed it the same day carried tests that run two real daemons through every rule. Today Claude Code's auto-memory rides it too: one named pair per project key between the workbench Macs, because 98 notes in 30 keys had drifted apart while the only path between them was a hand-run copy over SSH.