A 2026-11-04 crash left no trace in the engine's own log and reported
Result=success with NRestarts=1: the unit's ExecStart ends in 'exec podman
wait', whose exit status is 0, while the container's real code is printed to
stdout. The only copy of the reason was the client-facing error text. Both
traps now live in the endpoint page's Laws, with the observed trigger shape
(concurrent triple-digit-thousand-token prefills) and the instruments that
ruled out OOM, GPU hang, MCE and swap pressure.
The picker's decoration rows carried two payload-swap drifts: frank-dragoon
still named halogen 0.16.2 (live 0.18.20) and frank-zealot build 10777 /
3x262,144 / MTP off (live build 12118 / 3x200,704 / draft-mtp armed). Worse,
their params.context.length values had swapped, so OpenWebUI advertised more
window than the zealot slot grants and gave up 61k tokens on dragoon. Live
rows re-keyed through the admin API and ui.default_models set to
frank-archon; the swap procedure in AGENTS.md now names the re-key step.
Provisioned jason (pk 24, human-user template) and drove his first SSO
login in a clean browser: the row appeared as role='user' with chat and
wilwheaton TTS working, no manual step. The pending-then-flip law is
therefore wrong for a claim-carrying account - ENABLE_OAUTH_ROLE_MANAGEMENT
+ the roles scope decide it, and 0.12.5 refuses a claim-less login with 403
instead of falling back to ui.default_user_role. Census gains jason.
Diagnosed 'authentik login does not work in Jellyfin' 2026-11-04: nothing was
broken. The Jellyfin admin account 'dank' is a LOCAL-provider account, and the
operator's authentik identity (akadmin) was not in jellyfin_users, so the plugin
search found nothing and Jellyfin fell back to local auth - same UI message,
opposite fix. akadmin is now in jellyfin_users (directory data, DB-owned).
- Jellyfin: the admin-filter law (DN match, so a group-OU base grants nothing;
memberOf shape is the working one), the AuthenticationProviderId diagnostic,
re-verified LDAP path (bind + memberOf search from Dankserver).
- Authentik Outpost Reference: settle the token-intent argument (the plugin
sends a password, never a token), verify the bind DN live, and record the
instruments/traps: the outpost journal never shows a bind, Event is keyed by
created/action, Outpost has no apps, and piping ak shell steals its heredoc.
- Authentik: stop restating the LDAP outpost digest - the unit file owns it.
`status` compares a recorded timestamp with `--after`. Measured 2026-11-04: Plex
answered `settled` in the same second its own trigger was accepted (stamp
19:46:59Z for a request accepted 19:46:58Z) and still answered `settled` from that
one stamp 30 minutes later while the walk ran. So no epoch survives it — a journal
mtime fails and the trigger's epoch fails, because the trigger satisfies the door it
was asked to wait for. That is the correction of b82655d, which prescribed the
trigger's epoch for this door.
Jellyfin's `EndTimeUtc` only advances when a scan finishes, so there the trigger's
epoch is sound and a file mtime is not; its `wait` already refuses an epoch an
earlier task passes. The proof a scan did its work stays `media-verify.py`
replaying the journal: a scan can clear rows but cannot invent one.
The page's own example passed the journal mtime. Measured 2026-11-04: a chain
using it fell straight through its wait loop, because status only asks whether
the section has scanned since that epoch and the section had been walked earlier
the same day. The verify that followed read a database that had not yet processed
that day's renames — a false success, which is the direction that gets believed.
`wilwheaton` kept failing in OpenWebUI with the 29-name Kokoro enum even
after the dropdown was set to Default. The page's 2026-10-18 conclusion —
"no user.settings row carries an audio override, so the choice lives in that
browser's own storage" — was a probe-depth bug: the personal audio settings
live in table `user`, and the 0.12.x front end nests them under `ui`
(`settings.ui.audio.tts.engine`). The row did carry `browser-kokoro`, and the
SPA re-applies the server copy on every load, so the UI fix never stuck.
Corrected facts on the owner page:
- where the override really lives, and the version-dependent nesting;
- repair BOTH engine and voice: engine "" still sends the stored voice, so a
Kokoro-era `voice: "alloy"` returns as frank-tts's own 400;
- the one-line tells: an `/wasm/ort-wasm-simd-threaded.jsep.wasm` fetch means
Kokoro.js is live in that browser, and uvicorn logs every request so a
missing `/api/v1/audio/speech` line is evidence;
- deployed build is 0.12.5 (pin `0b399a8b`, bumped 2026-10-30) — the page
still claimed 0.11.4 — and the 401 paragraph's duplicated sentence is gone.
Verified 2026-11-04: with the row repaired, `POST /api/v1/audio/speech` as a
real user over the vhost returns 200 audio/mpeg and frank-tts logs
`speech voice=wilwheaton`.
The duplicate-prune, TV-duplicate, Ghibli and promo-clip holds went out of the
array trash on Frank's word: ~1.2 TB back. This replaces the three "purge pending
Frank's word" rows with what happened.
Two procedures the page did not carry:
- The purge gate. `REAL: 0` in both databases only says no row points at a moved
path; it says nothing about whether the film survived. Every held file needs a
same-work carrier in the live library at >= its size, re-derived at purge time,
because the relayout renames folders AND keeper files, accents defeat ASCII
matching (Nausicaä), and a carrier is usually another container at another
size. That supersedes the 2026-11-01 "re-verify by size" law, which is exactly
the test that reports survivors as lost.
- The purge mechanics. The repo purger acts on a root's own .AGENTTRASH and
nothing else, so a partial purge is staged into a scratch root while the
unapproved buckets sit parked at the array root as .HOLD-<date>-*.
./tests/run.sh: 824 assertions, all pass.
Plex.md and Jellyfin.md keep every law and point at the driver that now carries
them, so the procedure has one owner and the pages keep the failure signatures.
Jellyfin.md also records where `jellyfin.db` actually lives on this host (the
quadlet's config bind, not the stock path) and demotes `immutable=1` on the
`ApiKeys` read to the `-shm` fallback, because the database is WAL and
`immutable` skips the log. Plex.md gains the live section keys and the
`scanned_at == scannedAt` fact.
Media Relayout.md's hand-off after an apply names both drivers and states the
ordering law: Jellyfin first and alone, since its one door walks every library
while Plex forks a scanner per section, and the array is what both share.
Media Duplicate Prune.md gains the guard the 2026-11-04 hold taught: 32 files
carrying real film titles were 8-20-second h264 clips (49 MiB, 25 of them in a
`_release-group watermarks/` folder), invisible to every duplicate screen
because nothing shares their size and the feature-length guard hides them. The
census that finds the next batch is a duration floor, not a hash.
frank-archon.md's intro promised 2 conversations while its own shape table said
MAX_SEQS=4; AGENTS.md carried --max-num-seqs 2 and a 44,928 MiB host pool against
a live 4 and 49,152. Both now quote the env.
dankclaw-iommu-translation-off.md re-transcribed frank-dragoon's boot ledger,
including a pool figure that was superseded the same day, and asserted in the
present tense that no NPU endpoint is deployed - falsified by the XDNA2 embedding
lane on 2026-10-19. Both facts now belong to [[frank-dragoon]] alone.
Deployment Flow.md had a sentence broken by an earlier link removal.
Census: every tunable in the three scripts/<svc>.env against its endpoint page
(only truncated-sha and elided-path forms, which the pages omit on purpose), live
/health + /v1/models on all three endpoints, check-okf.py clean at 125 pages, no
duplicate titles, no page unlinked from an index.
Jellyfin's card still read "Work in progress: not open to family users yet"
with no link; the server answers 12.4.0 on :8096 and carries a second
(non-admin) account, so the card flips to live status with its Open link on
http://jelly.franksdank.cloud. It has no Authentik link, and the copy says so.
Audiobookshelf moves category admin -> user. A card renders on exactly one
page, so it leaves /admin: Authentik SSO made it a household app, and the
household page is the door that matters.
Marreta's description now names what it is - the content-cleaning proxy in the
Gluetun netns with a headless companion - and its honest limit.
Also in this change:
- docs.html: a "Media & files" line for the audiobook door and its SSO, plus
the pre-existing unclosed <ul> under frank-dragoon, which made the whole
frank-archon block parse inside the dragoon bullet.
- Caddyfile: the jelly. vhost gains the exemption reason the External Exposure
Policy requires (comment only - Caddy strips comments, so no proxy bounce).
- Homepage.md: classification list gains Audiobookshelf, and the section now
states that services.json is the authority for placement/WIP/link.
The SSO paragraph left "whether an access policy should be bound to the
audiobookshelf application" open, which invites the next agent to relitigate
it. Frank's answer of 2026-11-03: accepted on purpose — every signer becomes a
listener with all libraries and downloads, no access policy on the application,
narrowing done per account in Settings -> Users. What that makes load-bearing is
authentik's own signup flow, now named in the same paragraph as the thing that
must stay operator-only.
A wrong path was mistaken for a dead break-glass credential. Local login is
POST <base>/login; /api/* is behind the bearer guard, which answers a bare 401
and logs nothing, so a bad path looks exactly like a bad password. The recorded
value bcrypt-matches users.pash, so local really is the door when authentik is
down, and the rate-limit line was protecting a path that cannot carry a login.
Also records what auto-registration actually grants, measured from the state DB
because GET /api/users returns the limited shape: every SSO signer arrives with
accessAllLibraries and download already on, and carries no local password.
Rewrite the listener onboarding, the role row, and the fail-closed law: the
claim grants every signer, revocation happens on the ABS side (disable or
delete the user), and what actually guards the library is who can reach the
audiobookshelf application in authentik. Name both e2e gates in Verification.
AGENTS.md states the three exposure postures and why the guard chain cannot front a
non-browser client. Infrastructure/Caddy.md gains *Unguard a vhost* — route first,
IdP rows last, audit lists after — since deleting the proxy provider while traffic
still enters :8016 hands browsers authentik's error page. The Audiobookshelf page
retires its two-doors law (the guarded vhost as a browser-only door, remote
listening via VPN) for one address every client carries, adds how a listener gets an
account, and says plainly what the exemption costs.
`abs.` was a three-letter abbreviation of the container; the library it serves is
/mnt/entropy/Audiobooks, and `audiobooks.` says what the door is. Renamed in one
commit on purpose: the pihole seam fails the suite if a Caddyfile vhost has no
addn-hosts record, so any split would leave a red commit, and the `abs` name was
never deployed at the IdP with this fix, so nothing has to keep answering it —
no alias, no redirect, no stale URI.
Moves together: the Caddyfile site block, its matcher (@abs -> @audiobooks), the
LAN record, ABS_EXTRA_BASES in authentik-audiobookshelf-sso.sh (that script
REPLACES the provider's redirect-URI list, so the old origin's callback leaves the
RP when it is next run), the homepage admin tile, the guard spec's GUARDED list and
leak marker, and the three vault pages that name the door. authentik's provider row
follows in the deploy window (external_host + name), which re-derives its
redirect_uris through scripts/authentik-proxy-provider-defaults.sh; the Application
row keeps its own name, "Audiobookshelf Guard" — it names the app, not the vhost.
Step 1 gains the closing sentence the abs-proxy incident demands: the ak-shell
snippet that creates a ProxyProvider with objects.create() must be followed by
scripts/authentik-proxy-provider-defaults.sh, or the guard fails before any
credential. Step 2 gains the forwarded-scheme clause for any app that derives its
own origin from the request. Each fact keeps one owner — the laws stay on
[[Authentik Outpost Reference]] (provisioning rule 3) and [[Audiobookshelf]]; this
page only says where in the procedure they bite. Outpost page timestamp bumped to
match its substantive edit.
ABS checks every OIDC callback against the origin it believes it is served from:
getRequestOrigin() is x-forwarded-proto://Host (dist-server/server/utils/requestUtils.js,
read in the pinned image digest 2026-11-03). The post-auth http://:8017 listener is fed
over plain http, so Caddy's own X-Forwarded-Proto is http, ABS derives
http://abs.franksdank.cloud, and answers its own https callback with
400 {"message":"Invalid callback URL - must be same-origin"}
Measured at the app with the callback held fixed: Host abs.franksdank.cloud alone
reproduces the 400 (as does every other Host, including the bare LAN IP), and adding
`X-Forwarded-Proto: https` turns it into a 302 to /application/o/authorize/ carrying
the registered redirect_uri. The edge for a guarded name is TLS-only, so https is the
truthful value, not a spoof; the LAN door needs nothing because there browser and
server already agree on http, which is why only the guarded path was broken.
A proxy provider built by hand in `ak shell` never runs
ProxyProvider.set_oauth_defaults() — the REST serializer does, the ORM does not —
so the row looks fully configured while redirect_uris is empty (they are derived
from external_host by _get_callback_url()), grant_types is [], and
authorization_flow is NULL. Each one kills a guarded login in a different,
unrelated-looking way, all before any credential is requested: authentik's own
"Redirect URI Error" page, then a bodiless HTTP 400 from /application/o/authorize/
once the URIs are filled by hand, and a 500 NoneType.slug in the flow planner.
abs-proxy (abs.franksdank.cloud) hit the first two on 2026-11-03.
The script calls the vendor's own set_oauth_defaults() and points the
authorization flow at default-provider-authorization-implicit-consent for every
proxy provider, idempotently; a provider that already matches is untouched, so a
deliberate deviation is never flattened. `--check` is the census (exit 1 on
drift, writes nothing) for gate use. Applied live 2026-11-03: abs-proxy fixed,
the other ten (REST-created) already compliant, census now reports drift: 0.
This is authentik DB state and no deployable file carries it — a blueprint
re-import or an ak-shell re-creation reverts it and the symptom returns, so the
law and the failure signatures are recorded on the outpost page as provisioning
rule 3.
Infrastructure/Caddy.md now owns the order of operations for exposing a vhost
(proxy provider + application + embedded-outpost claim, then the route, then the
name and the chain audit), because the IdP row is invisible state that nobody
recreates. Audiobookshelf's Internal-only row and law become Two doors, one app;
the homepage link and index.md follow.
authOpenIDMatchExistingBy is unset on this deployment, so ABS matches an
OpenID userinfo on sub only and auto-registers everyone else. A new authentik
username that equals an existing ABS username therefore dies on the unique
constraint inside the callback, reported only as 'Unauthorized' - which is
exactly what a future agent would hit by 'just creating a frank user' in the
IdP to reach the root account. Records the two real doors (match by username,
which links and keeps root, since setUserGroup refuses to downgrade a root;
or a non-colliding name), and corrects the admin-membership row: akadmin is
the operator's own authentik account, already granted site admin.
Records that an ORM-created provider has no grant types and that the claim
mapping is not the whole property_mappings list, with the failure signatures
to recognise; names qa-e2e-admin as the admin-group fixture and the new
Playwright spec as the live verification.
Measured on both shapes: ping returns success:true and healthcheck returns OK at the root whether or not the app is mounted under a prefix, which is why the phone app's connectivity check works before it ever resolves the base path. The paragraph now says which routes were probed instead of asserting 200s in general, and the frontmatter description fits the OKF 120-char budget the linter enforces.
The page owned the URL as a plain host:port; every client now carries the /audiobookshelf prefix. Added the two laws a next operator needs (why the base path is not a choice, why the redirect subfolder must be sent) and the Single sign-on section naming which script owns which half.
The page told an agent how to sign in from a browser and nothing about the phone.
Measured 2026-11-03 on the deployed service:
- Server Address `http://192.168.0.235:8000`, user `frank`, plain HTTP, no base-path
prefix; `/healthcheck` and `/ping` answer 200 from a second host on the LAN and
`iptables -S INPUT` has no rule for 8000, so no phone-side setup is needed.
`download: true` on the root account is what enables offline listening.
- `POST /login` puts the bearer at `user.token`, not top level. Reading
`response.token` yields "", the client sends `Authorization: Bearer ` with nothing
after it, and every `/api/*` answers 401 as though the password were wrong — which
it was not. That is the failure I hit inside five minutes of scripting this.
- The LAN-only law gains its consequence for clients: the Anubis + Authentik chain
serves browsers, so guarding a vhost does not make the phone app work remotely.
Remote listening wants a VPN into the LAN; the LAN address is then still correct.
bats: 822 seams green.
The campaign-artifact home moved (42bc482) but the copy-paste surfaces still
named the old ones, and a tool's usage block is exactly what an agent runs:
- scripts/media-shows.py, media-tv.py, media-tvdupes.py: usage examples wrote
plans and journals at the array root. media-apply.py defaults its journal to
<root>/media-relayout.jsonl, so any run copied from those lines re-creates
the root-level clutter the vault now forbids. Each block now names
/mnt/entropy/.media-campaign/ and says why --journal is explicit.
- media-shows.py: --park named the retired media-park; the pile is
/mnt/entropy/NeedsProcessing/TVShows.
- Jellyfin.md: library-coverage law named /data/media-park; names the pile by
its current path and records when it moved.
- Media TV Relayout.md: the runnable --park path named media-park.
bats: 822 seams green.
The array root carried 48 planning artifacts (journals, plans, run logs)
accumulated by the relayout campaigns since 2026-11-02. They sit beside the
libraries now, which is how a media server ends up indexing a journal.
Moved all 48 (219.8 MiB) into the dot-directory `/mnt/entropy/.media-campaign`
with a journaled `media-apply` run (plan, apply, verify — 48 verified at their
targets, 0 root files left). Jellyfin and Plex ignore dot-directories, and the
overlay view exposes the array root to both servers, so the same pollution
existed on the view side.
Every vault page that named a root-level journal path is rewritten to the new
home (26 occurrences across 6 pages), and the law is stated once, in
[[Media Relayout]], which owns the tool chain: `media-apply.py` defaults its
journal to `media-relayout.jsonl` beside `--root`, so a run that omits
`--journal` recreates exactly the root-level log this now forbids.
- the per-library switches are under LibraryOptions, not top-level DTO keys
- Shows was flipped on 2026-11-02; Music is the only silent file index left
- never pin Providers= on a TV art loop: the TVDB-only offers disappear
- /Items?Ids= needs no UserId where /Items/{id} does, so verify writes there
A parked folder whose name parses to a registered show merges into the row
that owns the show. The proof is the episode rows standing under the owner
folder — never the id-less folder list, because one tvdb id can carry two
`Series` rows and an id-less folder that parses gets identified on its own
and keeps its own folder, offering the show twice.
No library file moves while `Scan Media Library` is Running: wait for a
Running sighting and then two consecutive Idle polls, because a gap between
sub-tasks reads as finished. Both sides speak different path dialects (the
server records the container coordinate, the journal speaks the array), so a
join that forgets the translation reads as an empty library.
Lands on [[Media TV Relayout]] — the page that owns the TV arm — so
`Media Relayout.md` stays under the OKF ceiling.
read_plan's if/elif chain had no else, so any row that was not mkdir, keep
or move was dropped in silence. A plan of nothing but typos therefore
applied as a successful no-op: on 2026-11-02 a pass meant to rename two
TVShows series folders reported "applied: 0 moved" and exit 0, and the
journal was never written — nothing told the operator the plan had never
been read. An unread row is a plan bug, and every other plan bug in this
tool refuses before the first rename, so now this one does too, naming the
line and the verb.
cwd: /home/blank/.omp/wt/wt-20261030-163847-6d1fc1f · session: 01a18f2e-18b1-7573-bcd5-ac99636f8bcb
Media Relayout.md crossed the OKF 500-line ceiling with the 2026-11-02 TV
laws, and the ceiling was right: the TV arm reads a different authority and
refuses for different reasons, like the music and audiobook arms that already
have pages. The arm moves verbatim to Media TV Relayout.md and gains two laws
from the TVShows pass: a folder naming two series is not evidence of two
series (ffprobe's embedded title tag answers per file), and right numbering
with a wrong name means rename the folder only. The executor page keeps the
executor laws and gains the hold-outside---root/--force law; Jellyfin.md gains
the settle read (ScheduledTasks State, since /Library/ScanStatus is a 404 on
12.4). Index regenerated with gen-vault-index.py.
cwd: /home/blank/.omp/wt/wt-20261030-163847-6d1fc1f · session: 01a18f2e-18b1-7573-bcd5-ac99636f8bcb
The `Shows` flip landed today (one field, proven in the server's own
`LibraryOptions` answer), so the posture table no longer calls it a silent file
index and `Music`/`Collections` are named as the remaining ones.
Two corrections the same day's measurements force:
- The per-library `POST /Items/{id}/Refresh` answered `204` and queued nothing
both before the flip and after it — zero journal lines for ten minutes, and
`Scan Media Library` kept `LastTaskResult=None, EndTimeUtc=None`, the never-run
signature. The provider switch was never the reason that door is dead; only
`POST /Library/Refresh` runs (204, then `Running` inside 15 s at 33 % CPU), and
it walks every library, so a library-scoped pass has no door on this build.
That paragraph also records the null-pair read that separates "never ran" from
"just finished", and that query+body on the same call answers `400` with an
empty body whatever the values.
- The key law: a plugin owns its own config file under its own
`<PluginConfiguration>` root and omits null settings, so TVDB's `<ApiKey>`
element never existed and `install` refused — which is what `MISSING key` had
been reporting since October. PR #121 fixes the writer.
The `Shows` art census replaces speculation with the split that matters: 2,908 of
3,558 series+season carry a primary, 624 have no provider id, 26 have an id and no
poster. A `Download` campaign over them took 20 s and downloaded nothing, which is
the measurement that says identification is the only lever left.
Four laws from finishing the un-shadow, correcting one:
- A book whose files ALL fail to probe never becomes an item, so a disk-vs-DB
folder gap beside `Invalid data` lines is corrupt media, not a lost book. A
format-only ffprobe clears files the scanner rejects — do not trust it.
- `Disc 01`/`CD 1` subfolders are parts of one item (110 of them here, under a
192-track book); the extension census counts them, ABS does not.
- `.cbr`/`.cbz` group like media but `parseComicMetadata` throws `ERAR_BAD_ARCHIVE`,
so those items land media-less: the file, not the scanner.
- Scan completion is a NEW scan uuid; `podman logs --since` is a sliding window
and ages its own evidence out.
- `?force=1` is now recorded as neither discovery nor repair: a forced full
re-scan left item count, path set and media-less count identical.
Verification names the five book folders that need re-acquiring.
Corrects the law pushed earlier today. The mechanism is
`groupFileItemsIntoLibraryItemDirs` (utils/scandir.js), called on a live
`recurseFiles` walk of the tree EVERY scan: each file attaches to the FIRST
directory along its path that already has a group, so one media file at a
collection root owns everything beneath it. Measured: one 949 MB `.cbr` hid
652 book folders behind a single 5,693-track item.
- "Media file" is audio OR ebook (`audiobooksOnly=false` -> SupportedAudioTypes
∪ SupportedEbookTypes, 17 audio + 6 ebook extensions). An audio-only census
cannot see the file that hides an Audiobooks library.
- Deleting the covering item does not fix a shadow: grouping is recomputed from
disk and the next scan rebuilds the item (measured: `1 Added`, same path).
- Un-shadow by MOVE: one file per folder is ABS's own single-book layout.
- `?force=1` is not a discovery arm (both arms walk the disk; force re-scans
every existing item, 1:33 against 0:04).
- Paging corrected: `offset` is ignored outright, `page` is the parameter.
- `DELETE /api/items/:id` is a DB row; `?hard=1` is `fs.remove()` of the whole
swallowed subtree. Never pass `hard`.
Shape table gains the layout row and the move journal; the stale "1,103
shelves" scan count is destroyed.