Keyboard shortcuts

Press ← or β†’ to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Syncing the book to a Kindle

2026-08-25

David: ran /clear

David:

Can you write a script to update the copy of the book on my kindle (currently mounted) with the latest version of the epub?

I'll check what's mounted and where the epub lives.

12 tool calls

πŸ”§ Bash: ls /Volumes/; ls redacted/*.epub (List volumes and epubs)

βœ… 4 lines, 75 chars β€” Audacity / Kindle / Macintosh HD

πŸ”§ Bash: ls /Volumes/Kindle/; echo ---; ls /Volumes/Kindle/documents/ 2>/dev/null | head -50 (Inspect kindle layout)

βœ… 62 lines, 749 chars

πŸ”§ Bash: cd /Volumes/Kindle/documents; ls -la Autoroad "Autoroad (draft)" 2>&1 (Inspect Autoroad dirs)

βœ… 16 lines, 868 chars β€” Autoroad: / total 976 / drwx------@ 1 drmaciver staff 8192 25 Aug 10:34 .

πŸ”§ Bash: which ebook-convert calibredb kindlegen 2>&1; ls /Applications/calibre.app/Contents/MacOS… (Check for calibre CLI)

βœ… 13 lines, 218 chars

πŸ”§ Bash: mkdir -p redacted/scripts && cat > redacted/scripts/update-kindle.py <<'EOF' #!/usr/b… (Write and run kindle update script)

βœ… 59 lines, 2536 chars

πŸ”§ Bash: git add scripts/update-kindle.py && git commit -q -m "$(cat <<'EOF' Add script to update… (Commit script)

βœ… 1 lines, 57 chars β€” 39e7155 Add script to update the book on a mounted Kindle

scripts/update-kindle.py β€” run it with the Kindle mounted; already run, so the device now has the current book.

  • Converts book-1.epub to MOBI via calibre's ebook-convert into a temp dir, then overwrites /Volumes/Kindle/documents/Autoroad/Wrong Man, The - Autoroad.mobi.
  • Same filename deliberately, so the sibling .sdr folder (reading position, bookmarks) stays attached.
  • --epub and --kindle flags override the defaults; it exits with a clear message if the Kindle isn't mounted or calibre isn't installed.

Note there's also an empty Autoroad (draft) folder on the device holding only a stale .sdr β€” I left it alone.

David:

There's something wrong with the way this is being converted to kindle that is causing page turning to break. It sometimes just freezes and refuses to turn. I've seen this with large imported epubs before.

6 tool calls

πŸ”§ Bash: unzip -l book-1.epub | sort -k1 -n -r | head -20; echo ---; ls -la book-1.epub (Inspect epub contents)

βœ… 22 lines, 1094 chars

πŸ”§ Bash: cd /Volumes/Kindle && cat system/version.txt 2>/dev/null; ls system/ | head -30; echo ---… (Check kindle device info)

βœ… 32 lines, 692 chars

πŸ”§ Bash: cd "/Volumes/Kindle/documents/Autoroad" && ls -la *.sdr/ "The Wrong Man_c893d674-24ec-4b5… (Inspect sdr contents)

βœ… 20 lines, 1059 chars

Found the actual cause. Let me rewrite the script.

6 tool calls

πŸ”§ Bash: cat > redacted/scripts/update-kindle.py <<'PY' #!/usr/bin/env python3 """Update the cop… (Rewrite script for azw3 and run)

βœ… 20 lines, 1165 chars

πŸ”§ Bash: python3 - <<'PY' import pathlib p = pathlib.Path("scripts/update-kindle.py") s = p.read_t… (Fix AppleDouble handling and rerun)

βœ… 10 lines, 507 chars

πŸ”§ Bash: ls -la "/Volumes/Kindle/documents/Autoroad/"; git add -A scripts && git commit -q -m "$(c… (Verify device state and commit)

βœ… 5 lines, 263 chars β€” total 832 / drwx------@ 1 drmaciver staff 8192 25 Aug 15:31 . / drwx------ 1 drmaciver staff 16384 25 Aug 09:07 ..

The device folder is now just the one AZW3 file, nothing else. Two things were wrong, and the second was the real culprit:

Stale reader state. The .sdr sidecar held Wrong Man, The - Autoroad.apnx β€” the page index β€” dated 10:34, from before my 14:30 overwrite. Same filename, different content, old index. The reader paginates against an index that no longer matches the text, and hangs. There was also a .mbp1.bad_file sitting in there, which is the device's own record that it choked on that state. The script now deletes the .sdr on every update, so the Kindle rebuilds it.

MOBI6. Calibre's .mobi output is the legacy format; your firmware is 5.19.2, which handles AZW3 (KF8) properly. Output switched to .azw3, and the superseded .mobi is removed so you don't get a duplicate entry.

Also swept the macOS ._ AppleDouble files β€” those show up as phantom books in the library, though they weren't causing the freeze.

The tradeoff: you lose your reading position on each update. That's unavoidable if the page index has to be rebuilt, and the position was already corrupt. --keep-position skips the .sdr deletion if you ever want it, but it reopens the freeze.

Worth confirming the freeze is actually gone before trusting the fix β€” I've reasoned from the evidence on disk, not tested it on the device. If it recurs on the AZW3, the next suspect is the conversion itself and I'd look at splitting the larger chapters.

David:

Are you able to parse and regenerate the sdr file?

Good question β€” worth checking against real files rather than from memory. Other books on the device have .sdr folders I can inspect.

8 tool calls

πŸ”§ Bash: cd /Volumes/Kindle/documents && find . -name "*.sdr" -maxdepth 3 | head -5; echo ===; fin… (Find sdr sidecar files on device)

βœ… 7 lines, 275 chars

πŸ”§ Bash: cd /Volumes/Kindle/documents && find . -name "*.apnx" | head -5; echo "--- state files --… (Find apnx and reader state files)

βœ… 19 lines, 2144 chars

πŸ”§ Bash: cd "/Volumes/Kindle/documents/M, Alex/redacted Wizard, The - Alex M.sdr" && for f in *.… (Hexdump apnx and mbs)

βœ… 43 lines, 2905 chars

πŸ”§ Bash: /Applications/calibre.app/Contents/MacOS/calibre-debug -c " from calibre.devices.kindle.a… (Check calibre APNX builder API)

βœ… 2 lines, 154 chars

Yes for the page index; no, meaningfully, for the position. Let me wire up the half that works.

πŸ”§ Bash: cat > redacted/scripts/make_apnx.py <<'PY' """Generate an .apnx page index. Run under c… (Add apnx regeneration and run)

βœ… 8 lines, 472 chars

πŸ”§ Bash: ls -la "/Volumes/Kindle/documents/Autoroad/Wrong Man, The - Autoroad.sdr/" && git add -A… (Verify sdr and commit)

βœ… 5 lines, 249 chars β€” total 48 / drwx------ 1 drmaciver staff 8192 25 Aug 15:33 . / drwx------@ 1 drmaciver staff 8192 25 Aug 15:33 ..

Split answer, because .sdr is a folder with two very different things in it.

The .apnx β€” yes, fully. It's a documented format and I've now wired it in. Looking at a working one from another book on your device, it's a small binary header followed by two JSON blobs and then a flat array of byte offsets:

{"contentGuid":"36d615e3","asin":"a5318a33-…","cdeType":"EBOK","fileRevisionId":"1"}
{"asin":"a5318a33-…","pageMap":"(1,a,1)"}
0x000008fc 0x000011f8 0x00001af4 …   ← one offset per page

Calibre ships APNXBuilder, so rather than hand-rolling it I call that. The script now regenerates the index from the AZW3 it just wrote, meaning it's structurally incapable of being stale β€” which was the failure mode. You get real page numbers back, which you'd lost under the previous commit.

The .mbp1/.mbs β€” parseable, but regenerating them is the wrong goal. They're an undocumented Amazon key-value serialization. The readable strings show the shape (timer.model, timer.average.calculator.distribution.normal, and fpr β€” furthest page read β€” carrying 1511217). I could reverse enough to rewrite that number.

But that number is a byte offset into the book, and this is why it isn't worth doing: when you edit chapter 3, every offset after it shifts by an unknown amount. There's no honest mapping from an old offset to a new one. Restoring the position would mean confidently dropping you at the wrong paragraph β€” worse than the Kindle putting you at the start and letting you know it did.

The exception is append-only updates. If you're only adding chapters at the end, offsets into existing text don't move, and the position is genuinely valid β€” that's the case where --keep-position is correct rather than reckless, and it's now the only case where I'd use it. If you expect to update that way often, I can make the script diff the new epub against the last-shipped one and take the safe path automatically when nothing before the append point changed.

David:

Could you look at the text at that index in the file on disk and figure out the corresponding text in the new version? Possibly also looking at e.g. what chapter you're in etc?

Worth saying up front: your actual position is gone β€” I deleted the .sdr and the old .mobi two turns ago, so there's no old offset and no old text to match against. What I can do is build and validate the mechanism so it works from the next update on. Let me test it against a book on the device that still has a live position.

πŸ”§ Bash: cd "/Volumes/Kindle/documents/M, Alex" && ls -la; echo "--- fpr region of mbs ---"; xxd "… (Inspect redacted Wizard book and fpr)

βœ… 13 lines, 826 chars

πŸ”§ Bash: cd redacted: private && /Applications/calibre.app/Contents/MacOS/calibre-debug --inspec… (Unpack redacted Wizard mobi)

βœ… 7 lines, 272 chars

Understood β€” the lost position isn't a worry, making it not recur is the goal. Continuing.

8 tool calls

πŸ”§ Bash: /Applications/calibre.app/Contents/MacOS/calibre-debug --help 2>&1 | grep -iE "inspect-mo… (Check calibre-debug flags)

βœ… 5 lines, 307 chars

πŸ”§ Bash: cd redacted: private && cp "/Volumes/Kindle/documents/M, Alex/redacted Wizard, The -… (Inspect mobi correctly)

βœ… 5 lines, 117 chars

πŸ”§ Bash: cd redacted: private && find . -maxdepth 2 -type f | head -20; echo ---; find . -maxdep… (List decompiled output)

βœ… 27 lines, 468 chars

πŸ”§ Bash: cd redacted: private && stat -f%z text.html && python3 -c " d=open('text.html','rb').re… (Read text at fpr offset)

βœ… 3 lines, 670 chars

fpr is a byte offset into the raw MOBI markup, confirmed: 1511217 of 1517859 bytes lands exactly on that book's end-matter TOC β€” a finished book. So the mapping is real and the remap is feasible. Let me check the project's test setup.

πŸ”§ Bash: ls; echo ---; cat pyproject.toml 2>/dev/null | head -30; ls tests scripts 2>/dev/null (Check project python setup)

βœ… 16 lines, 144 chars

πŸ”§ Bash: ls tools build; echo ---; find . -maxdepth 2 -name "pyproject.toml" -o -maxdepth 2 -name… (Inspect tools and build dirs)

βœ… 7 lines, 45 chars

No Python infra here, so I'll add a self-contained module with tests. Tests first.

πŸ”§ Bash: cat > redacted/scripts/test_kindle_position.py <<'PY' """Tests for remapping a Kindle r… (Write tests)

βœ… 1 lines, 7 chars

πŸ”§ Bash: cat > redacted/scripts/kindle_position.py <<'PY' """Carry a Kindle reading position acr… (Implement module and run tests)

βœ… 19 lines, 861 chars

uv it is β€” that's what I'm running tests under. One real weakness surfaced: a long anchor breaks whenever anything is inserted just before the position.

πŸ”§ Bash: python3 - <<'PY' import pathlib p = pathlib.Path("kindle_position.py") s = p.read_text()… (Add shorter anchor tier and retest)

βœ… 2 lines, 98 chars

All 16 pass. Now validate against real prose rather than synthetic fixtures β€” git has older versions of the epub.

πŸ”§ Bash: cd redacted && git log --oneline -- book-1.epub | head (History of book epub)

βœ… 10 lines, 803 chars

Ideal test case: 5b2c6ef is a dialogue pass scattering small edits across chapters 003–031. That's the hard case.

8 tool calls

πŸ”§ Bash: cat >> redacted/scripts/kindle_position.py <<'PY' # --- getting the raw markup out of… (Add rawml extraction)

βœ… 2 lines, 98 chars

πŸ”§ Bash: cd redacted && SP=redacted: private && mkdir -p $SP && git show 7294aca:book-1.ep… (Build old and new azw3 for validation)

βœ… 11 lines, 543 chars

πŸ”§ Bash: cd redacted/scripts && cat > redacted: private <<'PY' """Sample positions… (Validate remapping on the real book)

βœ… 6 lines, 563 chars

πŸ”§ Bash: cd redacted: private && ls; /Applications/calibre.app/Contents/MacOS/calibre-debug -m o… (Debug rawml extraction for azw3)

βœ… 6 lines, 155 chars

KF8 names the file differently from MOBI6.

πŸ”§ Bash: cd redacted/scripts && python3 - <<'PY' import pathlib p=pathlib.Path("kindle_position.… (Fix extraction and validate)

βœ… 6 lines, 563 chars

πŸ”§ Bash: python3 - <<'PY' import pathlib p=pathlib.Path("kindle_position.py"); s=p.read_text() s=s… (Fix cwd and validate)

βœ… 8 lines, 345 chars

398/399 exact on a real edit. But my "drift" number measures content shift, not error β€” let me replace it with an actual accuracy check, and fix the mojibake (I'm decoding byte-by-byte).

πŸ”§ Bash: python3 - <<'PY' import pathlib p=pathlib.Path("kindle_position.py"); s=p.read_text() s=s… (Decode UTF-8 properly, retest)

βœ… 2 lines, 98 chars

πŸ”§ Bash: cat > redacted: private <<'PY' import bisect, collections, sys from difflib import Sequ… (Validate with a real accuracy metric)

βœ… 1 lines, 338 chars

Slow because the validator re-strips both books on every one of 399 samples. Fine for production (one call), so I'll let it finish in the background.

6 tool calls

πŸ”§ Bash: sleep 100; tail -25 redacted: private 2>/dev/null || echo "still running" (Wait and check validation output)

βœ… 1 lines, 431 chars

πŸ”§ ToolSearch ({"query": "select:Monitor", "max_results": 1})

βœ… 1 lines, 50 chars

πŸ”§ Bash: until [ -s redacted: private ] && grep -q "landings under" redacted: private (Wait for validation to finish)

βœ… 1 lines, 290 chars

While that runs, let me wire the remap into the update script.

πŸ”§ Bash: cd redacted && sed -n '1,60p' scripts/update-kindle.py (Review update script)

βœ… 60 lines, 2303 chars

πŸ”§ Bash: cat redacted: private (Read validation results)

βœ… 26 lines, 1252 chars

394 exact, 5 fuzzy, none wild. Note the exact rows at 0.77–0.87 aren't errors: the position is right, the next line is what the dialogue pass changed. Now, which sidecar file actually holds the position, and is there a checksum I'd break?

πŸ”§ Bash: cd /Volumes/Kindle/documents && for b in "M, Alex/redacted Wizard, The - Alex M" "Hungr… (Compare sidecar headers across books)

βœ… 7 lines, 542 chars

πŸ”§ Bash: cd redacted/scripts && uv run --quiet python -c " import sys, pathlib sys.path.insert(0… (Find which sidecar holds fpr)

βœ… 20 lines, 1091 chars

.mbs holds fpr, .mbp1 never does β€” consistent across all 20+ books, and the 8-byte header is a constant magic, not a checksum. Safe to rewrite. Wiring it in:

πŸ”§ Bash: cat > redacted/scripts/update-kindle.py <<'PY' #!/usr/bin/env python3 """Update the cop… (Rewrite update script with position carrying)

βœ… 2 lines, 98 chars

Now an end-to-end test on the device: stage the old build plus a synthetic position, then run the real script and see whether it lands on the same sentence.

6 tool calls

πŸ”§ Bash: uv run --quiet python - <<'PY' import shutil, sys from pathlib import Path sys.path.inser… (Stage old build with a real position)

βœ… 2 lines, 131 chars β€” redacted sentence: unpublished prose / staged fpr: 450274

πŸ”§ Bash: cd redacted && ./scripts/update-kindle.py 2>&1 | tail -10 (Run the real update with position carrying)

βœ… 5 lines, 308 chars

πŸ”§ Bash: cd redacted/scripts && uv run --quiet python -c " import sys; sys.path.insert(0,'.') fr… (Verify carried position on device)

βœ… 16 lines, 857 chars

Works end-to-end. But that staged .mbs was cloned from another book's template, so I'll clear it β€” the position was synthetic anyway, and I don't want redacted Wizard's reader state on your book.

πŸ”§ Bash: rm "/Volumes/Kindle/documents/Autoroad/Wrong Man, The - Autoroad.sdr/Wrong Man, The - Aut… (Clean up and commit)

βœ… 4 lines, 212 chars

Yes, both parts work. The device now has the current book, a fresh page index, and no stale state.

What the offset is. fpr in the .mbs is a byte offset into the book's raw markup. I confirmed it on a book of yours that still had a live position: redacted Wizard's fpr of 1511217, against a 1517859-byte markup, lands exactly on that book's end-matter TOC β€” a finished book. The container is trivial: a marker byte, a three-byte big-endian length, then the offset in ASCII.

Reading the text there and re-finding it in the new version. That's scripts/kindle_position.py. It strips markup to visible prose while keeping each character's byte offset, then works down four tiers:

  • exact β€” the prose ending at the position is still present, verbatim and unique. Anchors run 300 β†’ 25 characters; short ones are safe because uniqueness across the whole book is the acceptance test. The short tiers matter more than I first assumed: a 300-character anchor breaks if anything is inserted anywhere in the preceding paragraph, which is what a test caught.
  • fuzzy β€” that passage was edited. Find the enclosing chapter by heading, diff old chapter against new, carry the position through the diff.
  • chapter β€” chapter rewritten past recognition (similarity under 0.5); return its opening.
  • reset β€” chapter gone; back to the start rather than inventing a location.

Validated against a real edit, not fixtures: 5b2c6ef, the dialogue pass scattering changes across chapters 003–031, old epub pulled from git. Of 399 sampled positions, 394 exact, 5 fuzzy, none wild. redacted sentence: unpublished prose

Two things to be straight about. Your original position is genuinely gone β€” I deleted it earlier. And while the format reading is confirmed against every book on the device (position always in .mbs, never .mbp1; the leading 0x1ab126 is a constant, not a checksum I'd be breaking), whether the firmware accepts a sidecar I rewrote can only be settled by ejecting and opening the book. That caveat is written into the module.

I also drop .mbp1 rather than carry it. It holds annotations, presumably offset-keyed too, and I don't know its layout well enough to remap them honestly β€” so highlights won't survive an update. Same machinery would probably do it if that turns out to matter.