What "Resume" Has to Promise
Resuming sounds simple: remember how far you got, start from there. The hard part is the word remember. A record that says chunk 7 is done must never be wrong about it. If it claims bytes the disk does not hold, the resumed file has a hole that nothing will ever fetch again. If the source changed in between, the resumed file is two versions spliced together. Both failures produce a file of the right size that passes a size-and-date check and is quietly wrong.
Delta's answer is a partial: a file being received, plus a record of exactly which chunks of it are proven. Both directions use it. A pull lands in a partial on the pulling Mac. A push lands in one on the receiving Mac.
Disk First, Then the Record
Every chunk takes the same four steps, in this order:
- Check. The chunk's length must be what its index implies, and its SHA-256 must equal the hash that travelled with it. A mismatch is refused right here.
- Write at its offset.
os.pwriteinto the partial's data file, atindex × 16 MiB. Chunks arrive out of order from parallel workers, and position is all that matters. - fsync. The bytes are on the disk before anything says they are.
- Record. One line,
"<index> <sha256>\n", appended to the partial's.chunksfile.
A stop between steps 3 and 4 costs one chunk, fetched again on the next run. A stop in the middle of step 4 leaves a line that does not parse as an index plus 64 hex characters, and that line is no record either. The order means the record can lag behind the disk but never run ahead of it.
The Key Holds the Version
A partial is named by a hash of four things: the destination path, the file's size, the chunk size and the source's validator (its etag). If the source is edited between two attempts, its validator changes. The next run looks for a different partial, finds none, and starts over. The two versions are never mixed. If the source changes during a transfer, a chunk arrives with a different etag. The partial is discarded and the file fails with "changed during the transfer; run again".
Landing: Prove, Then Rename
When the last chunk is recorded, the partial is committed:
- every chunk must be recorded, or the commit refuses with "N of M chunks still missing";
- the recorded chunk hashes must equal the source's chunk list. If not, the partial is discarded. The whole-file SHA-256 is the fallback proof, and a file with neither proof is not landed at all;
- mode and modification time are set on the data file;
os.replacemoves it onto the destination name. On one file system that rename is atomic. A reader sees the old file or the new one, never half of each.
One subtle case: a source file rewritten in place with its size and time kept. The source's hash record is keyed by device, inode, size and modification time, so it can still describe the old bytes. If the chunks that arrived disagree with that record, the pulling side asks the source once more with fresh=1, and the source hashes the file again before the commit decides.
Two Bugs in the Record
The push that lost its own chunks. On 2026-11-09 a 40 MiB push of a model file from the service host to a laptop (Cradle delivering a model) failed at commit: "2 of 3 chunks still missing". All three chunks had arrived. A push sends its chunks as parallel requests, and each request had built its own Partial object with its own copy of the record. Each saved its own list, and the last save erased the others' chunks. Pulls never hit it, because one transfer shares one Partial, and every earlier big-file test had been a pull. Now every request for one partial shares one in-memory chunk list, under that partial's lock.
The record that cost more every chunk. The fix above first kept the whole list in one JSON header, re-read and rewritten on every chunk. The review on 2026-11-10 measured where that leads: at 65,536 chunks (a 1 TiB file), each chunk cost a 5 MB read and a 5 MB write, so the bookkeeping grew with the square of the chunk count. The header is now written once, and the chunk list is append-only, one line per chunk. Recording a chunk costs the same at the end of a 1 TiB file as at its start. A test asserts exactly that.
What Resuming Looks Like
A re-run of the same command plans again. Files whose size and modification time match are already in place. A partial's recorded chunks are counted as done, and only the missing ones are requested. The job says so: "resuming: N of M files already in place". In the measurements, a 1 GiB pull stopped at 560 MiB and run again finished the rest in 0.17 s.
An abandoned partial (never run again, or its source changed so the key moved on) is kept while anything touches it. After 14 days untouched it is swept, at start and a few times a day, and health shows how many partials are waiting and how many bytes they hold. The data file sits in Delta's own state folder when that is on the destination's volume, so a home folder never fills with stray halves.