The 2026-10-31 relayout's final verification ran against both media servers while their scanners were working. It reported rows the scanner had just written as MISSING, and one pass died outright:
sqlite3.DatabaseError: database disk image is malformed
Both libraries are in WAL mode (jellyfin.db-wal was 134 MB at the time). The verifier opened them mode=ro&immutable=1, which tells sqlite the file never changes — so it skips the write-ahead log. Every committed-but-uncheckpointed row was invisible, and reading a live DB that way is also what produces the malformed error.
The same defect sat in media-census.py's two Plex arms (plex and audiobooks) while its Jellyfin arms already read plain mode=ro. That one is worse in consequence: the census output is the authority a plan is built from, so an item a scan had just matched is simply absent from the file, and the plan reports "no provider identity" with no way to see why.
Fix
Open plain mode=ro first — sqlite then reads the WAL through its -shm — and keep immutable=1 only as the fallback for a database whose -shm cannot be read (read-only filesystem, unwritable directory). media-census.py gets one open_plex_ro() for both Plex arms, so the policy cannot drift inside one file again. Both docstrings state the law so nobody "simplifies" it back.
Tests
tests/media_verify.bats: a journaled move left committed-but-uncheckpointed (a holder connection keeps the WAL from folding in; the verifier runs as a real subprocess). Red before — landed: 0, missing: 1 — green after. 12/12.
tests/media_audiobooks.bats: a chapter row left in the WAL must still appear in the census export. Red before, green after; media_audiobooks + media_relayout + media_verify + media_music = 102/102.
Docs
The operational half of this law landed on main separately (7ee831a, docs-only): Media Relayout now states the plain-ro rule, the four verdict classes, and that Jellyfin owns no audiobook library. That page also now describes this branch's census behaviour — merge this so code and page agree.
cwd: /home/blank/.omp/wt/wt-20261030-163847-6d1fc1f · session: 01a18f2e-18b1-7573-bcd5-ac99636f8bcb
(committed in the build worktree /home/blank/.omp/wt/wal-fix)
Second commit (a5a9c04): the verdict now splits missing from the DB by
stat'ing both paths — on disk at dst, no row names it / REAL — file still at src / gone — at neither path. Same defect family as the WAL fix: the tool
reported a number the database could not justify, and missing from the DB: 1851
read as 1851 lost albums instead of 1851 sidecars. Two new tests; two existing
layout assertions loosened from line positions to content. 14/14.
## What broke
The 2026-10-31 relayout's final verification ran against both media servers while their scanners were working. It reported rows the scanner had just written as MISSING, and one pass died outright:
```
sqlite3.DatabaseError: database disk image is malformed
```
Both libraries are in WAL mode (`jellyfin.db-wal` was 134 MB at the time). The verifier opened them `mode=ro&immutable=1`, which tells sqlite the file never changes — so it skips the write-ahead log. Every committed-but-uncheckpointed row was invisible, and reading a live DB that way is also what produces the `malformed` error.
The same defect sat in **`media-census.py`'s two Plex arms** (`plex` and `audiobooks`) while its Jellyfin arms already read plain `mode=ro`. That one is worse in consequence: the census output *is* the authority a plan is built from, so an item a scan had just matched is simply absent from the file, and the plan reports "no provider identity" with no way to see why.
## Fix
Open plain `mode=ro` first — sqlite then reads the WAL through its `-shm` — and keep `immutable=1` only as the fallback for a database whose `-shm` cannot be read (read-only filesystem, unwritable directory). `media-census.py` gets one `open_plex_ro()` for both Plex arms, so the policy cannot drift inside one file again. Both docstrings state the law so nobody "simplifies" it back.
## Tests
- `tests/media_verify.bats`: a journaled move left committed-but-uncheckpointed (a holder connection keeps the WAL from folding in; the verifier runs as a real subprocess). Red before — `landed: 0`, `missing: 1` — green after. 12/12.
- `tests/media_audiobooks.bats`: a chapter row left in the WAL must still appear in the census export. Red before, green after; `media_audiobooks + media_relayout + media_verify + media_music` = 102/102.
## Docs
The operational half of this law landed on `main` separately (7ee831a, docs-only): *Media Relayout* now states the plain-ro rule, the four verdict classes, and that Jellyfin owns no audiobook library. That page also now describes this branch's census behaviour — merge this so code and page agree.
---
cwd: /home/blank/.omp/wt/wt-20261030-163847-6d1fc1f · session: 01a18f2e-18b1-7573-bcd5-ac99636f8bcb
(committed in the build worktree `/home/blank/.omp/wt/wal-fix`)
**Second commit** (`a5a9c04`): the verdict now splits `missing from the DB` by
stat'ing both paths — `on disk at dst, no row names it` / `REAL — file still at
src` / `gone — at neither path`. Same defect family as the WAL fix: the tool
reported a number the database could not justify, and `missing from the DB: 1851`
read as 1851 lost albums instead of 1851 sidecars. Two new tests; two existing
layout assertions loosened from line positions to content. 14/14.
A verify run beside a running scan reported rows the scanner had just
written as MISSING, and could fail outright as "database disk image is
malformed". Both libraries live in WAL mode, and the verifier opened them
with immutable=1, which tells sqlite the file never changes: it skips the
write-ahead log, so every committed-but-uncheckpointed row is invisible.
134 MB of jellyfin.db-wal was riding past the verifier's eyes during the
2026-10-31 rescan.
Open plain mode=ro first — sqlite then reads the WAL through its -shm —
and keep immutable=1 only as the fallback for a database whose -shm
cannot be read. The bats fixture leaves a journaled move uncheckpointed
and proves the verifier still counts it landed.
The two Plex arms of media-census.py — `plex` (the movie planner's title
authority) and `audiobooks` (the chapter rows the audiobook plan files by) —
opened the library `mode=ro&immutable=1` while the Jellyfin arms already used
plain `mode=ro`. Same defect as media-verify.py: an uncheckpointed row of a
running scan is invisible, so a just-scanned item is simply absent from the
authority file a plan is built from, and the pass cannot say why.
Both now go through open_plex_ro(), which tries plain ro first and keeps
immutable only as the fallback for a database whose -shm cannot be read. The
new audiobooks test leaves a chapter row committed-but-uncheckpointed and
asserts the export still carries it; red before the change, green after.
"No row at the destination" was one number, and it conflated three different
things: a sidecar the server never indexes, a file a later pass renamed again,
and a file that never left its source — the last one being actual damage. The
runbook already named the four classes; the tool never computed them, so an
operator reading `missing from the DB: 1851` had no way to tell a booklet.pdf
from a lost album and might have run a rollback over a false alarm.
`missing` rows are now stat'd on both paths and reported as
`on disk at dst, no row names it` / `REAL — file still at src` /
`gone — at neither path`, with offenders listed in that order. A row still
naming `src` stays a separate line: that is the server's own stale pointer, a
phantom, and it costs a re-scan rather than a restore.
Two new tests (REAL, and gone-vs-sidecar) plus two existing layout assertions
loosened from line positions to content. 14/14 in tests/media_verify.bats.
dank
merged commit 04802b3e48 into main2026-10-31 12:12:23 -07:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
What broke
The 2026-10-31 relayout's final verification ran against both media servers while their scanners were working. It reported rows the scanner had just written as MISSING, and one pass died outright:
Both libraries are in WAL mode (
jellyfin.db-walwas 134 MB at the time). The verifier opened themmode=ro&immutable=1, which tells sqlite the file never changes — so it skips the write-ahead log. Every committed-but-uncheckpointed row was invisible, and reading a live DB that way is also what produces themalformederror.The same defect sat in
media-census.py's two Plex arms (plexandaudiobooks) while its Jellyfin arms already read plainmode=ro. That one is worse in consequence: the census output is the authority a plan is built from, so an item a scan had just matched is simply absent from the file, and the plan reports "no provider identity" with no way to see why.Fix
Open plain
mode=rofirst — sqlite then reads the WAL through its-shm— and keepimmutable=1only as the fallback for a database whose-shmcannot be read (read-only filesystem, unwritable directory).media-census.pygets oneopen_plex_ro()for both Plex arms, so the policy cannot drift inside one file again. Both docstrings state the law so nobody "simplifies" it back.Tests
tests/media_verify.bats: a journaled move left committed-but-uncheckpointed (a holder connection keeps the WAL from folding in; the verifier runs as a real subprocess). Red before —landed: 0,missing: 1— green after. 12/12.tests/media_audiobooks.bats: a chapter row left in the WAL must still appear in the census export. Red before, green after;media_audiobooks + media_relayout + media_verify + media_music= 102/102.Docs
The operational half of this law landed on
mainseparately (7ee831a, docs-only): Media Relayout now states the plain-ro rule, the four verdict classes, and that Jellyfin owns no audiobook library. That page also now describes this branch's census behaviour — merge this so code and page agree.cwd: /home/blank/.omp/wt/wt-20261030-163847-6d1fc1f · session: 01a18f2e-18b1-7573-bcd5-ac99636f8bcb
(committed in the build worktree
/home/blank/.omp/wt/wal-fix)Second commit (
a5a9c04): the verdict now splitsmissing from the DBbystat'ing both paths —
on disk at dst, no row names it/REAL — file still at src/gone — at neither path. Same defect family as the WAL fix: the toolreported a number the database could not justify, and
missing from the DB: 1851read as 1851 lost albums instead of 1851 sidecars. Two new tests; two existing
layout assertions loosened from line positions to content. 14/14.