Compare commits

..
3 Commits
Author SHA1 Message Date
Frank b651d61056 docs(niri): portable desk runs landscape, HDMI left of the laptop
record the normalized layout that landed on main as 9b88208 and the law
that surprised it: positions are absolute, so rotating a panel widens
its logical box and every x to the right of it moves — recompute the
row, never just the rotated block.
2026-10-30 15:56:39 -07:00
Frank e197c80dcb Merge remote-tracking branch 'origin/main' into docs/niri-portable-desk 2026-10-30 15:56:39 -07:00
Frank b8584cd6a6 docs(niri): own the portable desk and the no-match defect
displays.md still described desk-2-portable as an EDID-pending skeleton
and left the PM1601 identity unnamed. record the second desk's rig,
its position math, and the law that a rig with no eligible profile runs
the previous desk's values rather than "unchanged" — the failure that
left the portable panels on 3440x1440@180 fallbacks for weeks.
2026-10-30 15:54:43 -07:00
46 changed files with 137 additions and 1157 deletions

No files matched your search

@@ -1,59 +0,0 @@
---
name: container-port-image-identity-binding
description: "Diagnose or write fleet gates that silently verify the WRONG process/image: /health answered by a dying container, podman inspect naming a merely-created container, cgroup lacking the service name, RepoDigests[0] vs the pin, detached launchers losing cwd. Bind every fact to its object by host pid."
---
# Container / port / image identity binding
For fleet gates, restart checks, "is it really up?" triage, and any bench/window harness that stops
a production service and starts a rig on the same port. Use when a check *passes* but the thing
still misbehaves, or when the same class of silent failure keeps recurring.
## The failure signature (all of these mean "you checked a proxy")
| Check that lied | What it actually measured |
|---|---|
| `curl /health` = 200 | whichever process owned the socket, often the one being stopped |
| `podman inspect -f '{{.State.Running}}'` | a container that was created, not one that bound a port |
| `podman ps` / container name | existence, not service |
| `pgrep -f "<pattern>"` | your own polling shell, or a sibling arm |
| log mtime advancing | some request completed, not necessarily yours |
| `df <dir>` | a filesystem that merely looks related to the one the bytes go to |
| `podman image inspect -f '{{index .RepoDigests 0}}'` | ONE of several digests — a by-digest pull makes an image carry the tag digest first |
| rootless cgroup path | the container **ID**, never the service/container name |
## The bindings that work
- **Listener → process:** `ss -H -ltnp 'sport = :<port>'` → `pid=`; needs privilege for foreign
processes (`sudo ss` when the listener is root's).
- **Host pid → container:** `podman top <ctr> hpid` (header line first).
**`podman top <ctr> pid` prints container-internal pids (`1`, `7`) and compares against nothing** —
this is the single most expensive mistake in this class.
- **Image pin:** `podman image inspect -f '{{range .RepoDigests}}{{println .}}{{end}}' <ref>` and test
*membership* of the pin, never position. Message the difference: "present but does not carry X".
- **Device holder:** `fuser /dev/kfd` → pids → resolve each through `podman top <ctr> hpid`; anything
outside the allowed set is foreign (an orphaned engine wedges the next boot as `device error`).
- **Detached launcher:** assert cwd (`cd "$(dirname "$0")/.."`; verify a known executable exists) and
write a `PASS/FAIL` line per fact. `setsid nohup … &` turns a lost cwd into silence, not an error.
## Procedure
1. Write ONE preflight that prints `PASS <fact> …` / `FAIL <fact> — reason`, exit 2 on any FAIL.
Facts: cwd, image digest membership, free space on the real target filesystem, stale artifacts,
GPU holders, listener, inputs, reference data.
2. Call it as step 0 of the orchestrator, before any GPU/port is touched. Delete the duplicated
inline checks so the fact has one owner.
3. Pin each lie with a bats test using PATH stubs (`podman`, `ss`, `fuser`): wrong digest, foreign
listener pid, foreign GPU pid, dirty dir, wrong cwd. Fixture files must be NON-EMPTY — a `-s`
fact on a `: > file` fixture fails for the wrong reason.
4. Keep one driver. Ad-hoc copies beside the orchestrator drop guards, and two drivers racing on one
port produce phantom "strays" that look like a hijack.
## Traps
- `podman top … pid` vs `hpid` — see above.
- Stub collision: a test stub that answers `top` for both the rig and production must branch on the
container name, or the test proves nothing.
- A gate that reports a stale-but-present image must distinguish "absent" from "retagged/older" —
the operator's fix differs.
- Verify a kill by re-reading the port; `podman rm -f` frees the cgroup, a pattern-kill may not.
@@ -1,82 +0,0 @@
---
name: dankserver-disk-census
description: "Analyze what fills dankserver's root filesystem (excluding /mnt/entropy) and price reclaim safely: du/findmnt method, podman+docker accounting, Plex DB backups, orphan KV spill, ext4 reserve and the calibration space gate."
---
# Dankserver root-filesystem census
Use when asked "what is using disk on dankserver", why `/` is full, or what must be freed before a
calibration/convert window. Read-only first; recommend, never delete without Frank's explicit OK.
## Shape of the box (verify, do not assume)
`/` is `/dev/nvme0n1p2` ext4 (~1.8T). `/mnt/entropy` is a separate ~91T LUKS/ext4 volume; when the
task says exclude it, `du -x` already stays on the root filesystem. `/var/lib/docker` and
`/var/lib/plexmediaserver` are on `/`, not entropy.
```bash
ssh dankserver 'df -hT -x tmpfs -x devtmpfs -x squashfs; findmnt -t ext4,xfs,btrfs,zfs'
ssh dankserver 'sudo du -x -d1 -h / 2>/dev/null | sort -h | tail -20'
ssh dankserver 'sudo du -x -d2 -h /home 2>/dev/null | sort -h | tail -30'
ssh dankserver 'sudo du -x -d2 -h /var 2>/dev/null | sort -h | tail -20'
```
`du` is device usage (st_blocks), so sparse files read small; `df` is the truth for free space.
A full top-level scan of `/` takes ~2 min — background it and keep working.
## Where the bytes actually live
- `/home/dank/models`: the vendor HF checkpoint **and** its `.rad` conversion are two representations
of the same weights (each ~336G for Qwen3.8-Flash-Next) plus the live serving `.rad`. All can be
legitimately present; check `scripts/frank-archon.env` for the live payload before calling any of
them dead.
- `/home/dank/.local/share/containers`: rootless podman storage is usually the largest non-model
consumer. Do not estimate it with `du` alone.
- `/home/dank/radiance-state/kvcache`: disk KV spill (`blocks.bin`, `ckpts.bin`). It is orphaned
whenever the running container does not mount it — check
`podman inspect frank-archon --format '{{range .Mounts}}{{.Source}} -> {{.Destination}}{{"\n"}}{{end}}'`
(rootless, `XDG_RUNTIME_DIR=/run/user/$(id -u)`). The unit is a root unit but the container is
rootless; `sudo podman ps` looks empty even while it serves.
- `/home/dank/corpora` vs `/home/dank/campaign/<name>`: there may be an OLD campaign's grams/KLD
reference beside the current one. Identify a KLD reference's base by its header, not its date:
`head -c 600 corpora/kld-ref/kld.json` → `"model": "/models/rad/....rad"`.
- `/var/lib/plexmediaserver/.../Plug-in Support/Databases`: Plex keeps dated DB backups; four stale
sets can be tens of GiB. `Metadata/` and `Media/` are thumbnails and chapter art.
- `/opt/vllm`, `/opt/llama.cpp`, `~/.local/bin/llama*`: pre-radiance build leftovers; confirm no unit
references them (`grep -RIl /opt/vllm /etc/systemd/system /home/dank/.config/systemd`).
## Container accounting
```bash
XDG_RUNTIME_DIR=/run/user/$(id -u) podman system df
XDG_RUNTIME_DIR=/run/user/$(id -u) podman images --sort size --format '{{.Size}}\t{{.Repository}}:{{.Tag}}\t{{.ID}}'
XDG_RUNTIME_DIR=/run/user/$(id -u) podman ps --format '{{.Image}}' | sort -u # in use
sudo docker system df; sudo docker ps -aq | wc -l; systemctl is-enabled docker
```
- `podman system df`'s **reclaimable** is the authoritative figure. Summing per-image `.Size` over
dangling images double-counts shared layers and can read several times larger than the disk.
- Docker may be `disabled` with zero containers yet hold ~120G of images from the vLLM era: full
reclaim candidate.
- Keep the radiance pins and the media-stack images; the big unused ones are vllm/rocm/fp030/harbor.
## Free-space levers and their traps
- **Trash is space-neutral on the same filesystem.** Moving a 336G payload to `~/.AGENTTRASH` frees
nothing until the trash is purged; say so before recommending it.
- **ext4 reserves 5 %** (`tune2fs -l /dev/nvme0n1p2 | grep -i 'Reserved block count'`, × block size).
`df` Avail excludes it, and the campaign runs as `dank`, so the reserve silently blocks non-root
writers while root still has room. Lowering to 1 % releases the reserve with no deletion, but it is
the fragmentation cushion on a full disk — last resort, never the first move.
- **The calibration window has a 60 GiB gate** (`campaign/*/window-when-space.sh`,
`Q38_CALIB_MIN_FREE_MIB=61440`). A failed tap keeps the BF16 container on disk; banked-but-not-purged
is not free.
- `uv`'s archive cache (`~/.cache/uv/archive-v0`) is routinely tens of GiB and safe to `uv cache prune`.
## Reporting
Rank by reclaim size, then risk: orphan KV spill and Docker images first; stale Plex DB backups and
old-campaign corpora next; `/opt`/binaries/journal/snaps last. Give exact path, size, evidence that
nothing uses it, and what breaks if it is wrong. Never execute a prune or delete in the census turn —
Frank approves each removal, and the repo deletion law (trash, never `rm`) still applies to anything
that is a record rather than a cache.
@@ -1,94 +0,0 @@
---
name: dankserver-space-reclaim
description: "Execute approved low-risk disk reclaim on dankserver's root fs: prove targets unreferenced, orphan KV first, dangling images, Docker prune, Plex DB backups; the campaign space watcher fires the moment the gate opens."
---
# Dankserver root-fs reclaim (approved low-risk sets)
Run only with the user's explicit approval for the named set. Deletion is the point: moving a cache
to `.AGENTTRASH` on the same filesystem frees nothing, so say that before proposing it. Never touch
`/mnt/entropy` (separate 90T volume) and never delete `/home/dank/models/rad/qwen38-bf16.rad` or
`campaign/**` while the qwen38 campaign is unfinished.
## Census (live, never from docs)
```sh
ssh dankserver 'df -hT -x tmpfs -x devtmpfs; findmnt -o TARGET,SOURCE,FSTYPE,SIZE,USED,AVAIL -t ext4,xfs,btrfs'
ssh dankserver 'sudo du -x -d1 -h / 2>/dev/null | sort -h | tail -15'
```
`-x` keeps `/mnt/entropy` out automatically (separate mount). Then drill `/home` and `/var` with
`sudo du -x -d2`. Typical 2026-10 shape: `/home` ≈ 1.4T (models 785G, `.local/share/containers` 334G,
`radiance-state` 69G, corpora 64G, campaign 45G), `/var` ≈ 215G (docker 119G, plex 80G), `/opt` 17G.
Verify live; sizes are disposable.
Do NOT `grep -R` across `/home/dank/Containers` or `~/Projects` — jellyfin cache JSON makes it run
for minutes. Scope `find -maxdepth 3 -iname '*harbor*'` and check unit dirs only.
## Prove each target unreferenced before deleting
- `sudo lsof <paths>` (fuse warnings are noise) and, for podman image IDs, cross-check
`podman ps -a --format '{{.ImageID}}'`; `podman image prune` keeps an in-use untagged image, but
confirm the container survives.
- Rootless vs rootful matters: `frank-archon` runs as rootless podman under `dank` even though its
unit is a root unit. Check both `sudo podman ps -a` and
`XDG_RUNTIME_DIR=/run/user/$(id -u) podman ps -a`, including stopped containers, and inspect their
mounts (`podman inspect -f '{{range .Mounts}}{{.Source}} {{end}}'`).
- `grep -RIl '<path>' /etc/systemd/system /home/dank/.config/systemd` must be empty.
- A running archon mounts only `/models`, `radiance-state/tune` and the template — **not**
`radiance-state/kvcache`, so stale `blocks.bin`/`ckpts.bin` are genuinely orphaned.
## Reclaim ladder (safest first)
1. **Orphan radiance KV spill** — `radiance-state/kvcache/frank-archon/{blocks.bin,ckpts.bin}`
(measured 69G, last write weeks old, no handle). `sudo rm -f` both. This alone can clear the
campaign gate.
2. **Docker** — only if `systemctl is-enabled docker` is `disabled` and `docker ps -aq` is empty:
`sudo docker system prune -a -f` (measured ~104GB; volumes are NOT removed, keep it that way).
Verify `docker volume ls` still lists the volumes.
3. **Podman dangling only** — `podman image prune -f` (default dangling, no `-a` without approval).
Measured 33 removed / ~62G freed; one dangling ID is the running calibration image and is skipped.
The remote shell is **zsh**: unquoted `$ids` does not word-split, so `podman rmi $ids` receives a
newline-joined string and fails with `invalid reference format`. Pipe IDs through xargs instead:
`podman images --format '{{.ID}} {{.Repository}}' | grep goharbor | cut -d' ' -f1 | sort -u | xargs -r podman rmi`.
4. **Harbor registry images** — all `goharbor/*` v2.13 images are ~2.1G total and safe once no
harbor container/unit/compose exists (`~/.harbor` is a cmdh manifest, not the goharbor stack). The
big `harbor-hf`/`harbor-vllm` build images live under Docker and disappear with step 2.
5. **Plex DB backups** — in `Plug-in Support/Databases`, keep the newest `*.db-YYYY-MM-DD` set and
delete older sets (measured 3 × 4.9G blobs + 3 × 1.4G library ≈ 19G). Plex stays up; never touch
the live `library.db`/`blobs.db`.
6. **Later, with separate approval** — Swift-era `corpora/calib` + `corpora/kld-ref` (KLD header
names the base: `model` field), `uv cache prune` (~25G), `/opt/vllm`, `/opt/llama.cpp`,
`~/.local/bin` stale llama binaries, journal vacuum, old snaps. Tagged-but-unused podman builds
(vllm/rocm/fp030) remain ~225G; removing them loses build caches, so scope them explicitly.
## The campaign watcher is the side effect
`campaign/qwen38-w5nl64/window-when-space.sh` polls
`df --output=avail -B 1M <campaign>/calib` every 60 s for `Q38_CALIB_MIN_FREE_MIB` (default 61440).
The moment reclaim opens the gate it starts the documented calibration window, which **stops
frank-archon** and boots `qwen38-bf16` on port 7770. Expect production downtime and a `health=000`
during load; the engine is healthy once `/health` returns 200. Do not restart frank-archon by hand
during the window — it would fight the rig for both cards.
If the window fails, read `logs/window-start-*.log` and `/home/dank/logs/qwen38_bf16_calib.log`:
`I stopping: draining in-flight requests` + 429 `the server is shutting down` means the orchestrator
SIGTERMed a healthy engine (tap marker state), not a crash; a retry may then fail with
`E startup stopped: device error` while the prior process's `VM memory stats ... non-zero when fini`
clears. Restore is automatic: the window script restores frank-archon and reports the banked/kept
`.rad`.
## Verify
```sh
df -h /; df --output=avail -B 1M /
XDG_RUNTIME_DIR=/run/user/$(id -u) podman system df
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:7770/health
XDG_RUNTIME_DIR=/run/user/$(id -u) podman ps --format '{{.Names}} {{.Status}}'
```
All running containers must be untouched, archon back to 200, and the campaign gate recomputed with
its own command. ext4 reserves 5% (~90G on the 1.8T root): `df` Avail is what the unprivileged
campaign sees, so root's ability to write does not mean the gate passes; lowering the reserve
(`tune2fs -m 1`) releases ~72G but removes the fragmentation cushion on a nearly full disk — last
resort only.
@@ -1,76 +0,0 @@
---
name: dankvoice-behavior-scenario-authoring
description: "Author or extend a dankvoice behavior.bats scenario: driver env knobs (DV_REWRITE/DV_FAIL/DV_CUT_MS/DV_TAG_LOG), the SPOKEN heard-words census, private-TMPDIR leak assertions, and the arm-the-precondition-before-absence trap."
---
# Writing a dankvoice behavior test
`tests/behavior.bats` drives the **built bundle** through a generated harness. Each `@test`
is: set env → run a named *scenario* → assert on printed census lines. Never assert on
internals; the observable vocabulary is `CALLS`, `PLAYS`, `PLAYORDER`, `SPOKEN`, `NOTIFY`,
`CALL <url> input=… auth=… at=…`, `PLAYER`, `KILLED`.
The driver lives in a heredoc inside `setup()`; find and read it before adding anything:
```bash
sed -n '/cat > "$DV_DRIVER" </,/^TS$/p' tests/behavior.bats
```
A scenario is a `case "<name>":` in that switch that calls the extension's own handlers
(`say.handler("on", ctx)`, `messageEnd?.(said("…"), ctx)`, `agentEnd?.(answer("…"), ctx)`).
Prefer delivering narration through the harness (`agent_end`/`message_end`) over a typed
`/say`: the humanizer, mood, heckle, framing and the frequency hold govern **unsolicited**
narration only, so a typed line cannot exercise them.
## Fixture knobs (set per test, all read by the driver)
| Knob | Effect |
|---|---|
| `DV_REWRITE=off\|echo\|prose\|reasoning\|stall` | what the chat (humanizer) route answers; unset ⇒ it 404s and the rewrite falls back to mechanical text |
| `DV_STALL_MS` | delay the chat answer (timeout behaviour) |
| `DV_FAIL=<N>` | the first N `/audio/speech` requests answer HTTP 500 JSON — an endpoint outage a test can watch the queue weather |
| `DV_HOLD=<sec>` | how long the player stub holds each clip; sets the playback windows |
| `DV_CUT_MS=<ms>` | in `stagecut`, when another session cuts the line |
| `DV_TAG_LOG`, `DV_TIMING_LOG` | per-clip tag census (`PLAYORDER`, `SPOKEN`) and player START/END ms |
| `DV_CT` | content type of the answer (non-WAV routing) |
| `DV_KEYS` | key replayer for the settings modal |
| `TMPDIR="$WORK/scratch-tmp"` | scope synthesis temp dirs so a leak assertion is yours alone |
Always pass `DANKVOICE_STAGE_DIR="$WORK/stage"` when the test touches the stage, and use the
`say_run <scenario>` helper (it wraps `run env …` so `$output` is captured); a bare
`scenario_name` invocation prints without capturing.
## Two traps that have each hidden a real bug
**Assert absence only with the producer armed.** `DV_REWRITE` unset ⇒ the chat route 404s,
so `[[ "$output" != *"/chat/completions"* ]]` and "payload equals the source text" pass even
when rewriting is happening. Nine tests pinned the opposite of the repo's law this way. Pin
with `DV_REWRITE=prose` (the fixture answers with its *own* words) and assert the payload you
expect — then survival is evidence, not absence.
**Count what was HEARD, not what was requested.** A segment whose clip was killed mid-play,
or aborted mid-synthesis, is legitimately synthesized again on resume. Laws about framing
("one intro per narration") are about hearing, so count `SPOKEN` lines: the driver maps each
played tag back to the `/audio/speech` request that produced that clip. `CALLS` counts
requests and will over-report across a line-cut.
Temp-dir leak assertions mirror this: `TMPDIR` private, then
`find "$TMPDIR" -maxdepth 1 -type d -name 'dankvoice-*' | wc -l`.
## Pinning timing
Derive windows from `DV_HOLD`: with 0.5 s clips, clips occupy ~[10,310], [310,610], [610,910];
choose `DV_CUT_MS` so the cut lands strictly inside the clip you mean to interrupt, leaving
margin for the 250 ms stage poll and synthesis time. Default scenario timings are env-overridable
(`DV_CUT_MS ?? 600`) so a new test never edits an existing one's schedule.
## Red-proof and gate (project invariants)
- Prove a new assertion can fail: in a **private worktree**, `git show HEAD:src/<file> > src/<file>`,
`bun run build`, run the filter, restore the file and rebuild. Never while another suite runs
(`setup()` rebuilds the bundle from `src/`). See `dankvoice-bats-serialization`,
`dankvoice-bundle-swap-red-proof`.
- `bun run build` before staging: `tests/packaging.bats` byte-compares committed `dist/` with a
fresh build, so `dist/index.js` must be in the same commit as the `src/` change.
- Gate with the whole suite (`bats tests/`), not the filter; ~5 min, so run it unbounded/async and
never write to `src/` while it runs.
@@ -1,48 +1,58 @@
---
name: dankvoice-bundle-swap-red-proof
description: Prove a dankvoice behavior test is red against the pre-change bundle by pointing DV_EXT at a built old bundle; includes the source-swap fallback when the extracted bundle cannot resolve node_modules outside the repo.
description: "Prove a dankvoice behavior test is red against the pre-change bundle by pointing DV_EXT at git show HEAD:dist/index.js, with the driver extracted from the bats heredoc into a pinned scratch directory. Use before claiming a new bats assertion is red-capable."
---
# Red-proofing a dankvoice behavior test against the pre-change bundle
Prove a new `tests/behavior.bats` assertion is **red-capable** — that it fails against the *previous* build, not just passes against the new one — without stashing, branching, or editing `src/`.
The suite drives the **built** bundle (`dist/index.js`, rebuilt by `setup()` from `src/`), so "is this new assertion actually red-capable?" is a question about a bundle that no longer exists after the fix. Two routes; try A first, fall back to B.
## Why
## Route A — `DV_EXT` at the old committed bundle
`tests/behavior.bats` is a black-box suite: the plugin bundle is loaded through `DV_EXT`, so a *different bundle* is the red side. Stashing the fix would break the `dist` byte-identity pin test and the shared worktree.
## Working recipe
```bash
git show HEAD:dist/index.js > "$W/old-dist.js"
DV_EXT="$W/old-dist.js" DV_STUB=... <same env as the test> bun "$DV_DRIVER" <scenario> # expect RED
DV_EXT="$PWD/dist/index.js" <same env> bun "$DV_DRIVER" <scenario> # expect GREEN
cd ~/Projects/dankvoice # or the feature worktree: ~/Projects/wt-dv-*
bun run build # current src = the green side
git show HEAD:dist/index.js > /tmp/old-dist.js # the red side (pre-change bundle)
W=/tmp/dv-redproof # PINNED name; see traps below
rm -rf "$W" && mkdir -p "$W/bin"
cat > "$W/player.sh" <<'SH'
#!/usr/bin/env bash
printf '%s\n' "$*" >> "$DV_STUB_LOG"
for arg in "$@"; do case "$arg" in *.wav) cp "$arg" "$DV_COPY" ;; esac; done
sleep "${DV_HOLD:-0.1}"
SH
chmod +x "$W/player.sh"
ln -s "$W/player.sh" "$W/bin/pw-play"
printf '#!/usr/bin/env bash\nprintf "116\\talsa_output.usb-KT.0\\taudio/sink\\t*\\n"\n' > "$W/bin/wpctl"
chmod +x "$W/bin/wpctl"
# The driver lives inside the bats file as a heredoc: extract it verbatim.
end=$(grep -n '^TS$' tests/behavior.bats | cut -d: -f1)
sed -n "86,$((end - 1))p" tests/behavior.bats > "$W/driver.ts"
export DV_EXT="$PWD/dist/index.js" DV_STUB="$W/player.sh" DV_STUB_LOG="$W/player.log" \
DV_COPY="$W/played.wav" DV_OMPCONFIG=/dev/null DANKVOICE_CONFIG="$W/settings.json" \
DV_MODELS="$W/models.yml" DV_WPCTL=x DV_SELECT=""
for ext in /tmp/old-dist.js "$PWD/dist/index.js"; do
rm -f "$W/player.log" "$DANKVOICE_CONFIG"
DV_EXT="$ext" bun "$W/driver.ts" <scenario> 2>&1 | grep -E '^PLAYS|^CALLS|^NOTIFY|^SETTINGS'
done
```
Build the driver harness by extracting the heredoc `setup()` writes into `tests/behavior.bats`:
The scenario name is the driver's third argv (`speak`, `auto`, `repeat`, `humanizecycle`, `badurl`, …); the bats `-f` test body's env (`DV_PLAYER_BIN`, `DV_SETTINGS_JSON`, `DANKVOICE_VERBOSITY`, …) is passed the same way. `DV_SELECT` holds the menu answers, `|`-separated, consumed in order.
```bash
sed -n '<first-body-line>,<last-body-line>p' tests/behavior.bats > "$W/driver.ts"
```
## Traps, each one cost a turn
Re-check that range after every edit that adds lines to the driver (`console.log` census blocks included) — a stale end line silently appends the `TS` terminator and the driver dies after printing, which looks like a passing probe.
- **The scratch directory must be pinned by name.** `ls -dt /tmp/tmp.*` picks up *other sessions'* temp dirs, and then `player.sh` and `driver.ts` live in different directories — the run silently does the wrong thing.
- **The stub must append to `$DV_STUB_LOG`.** The driver counts player invocations from that file; a stub that only `sleep`s reports `PLAYS 0` for *both* bundles and the whole proof reads as "fix does nothing".
- **`DV_EXT` is required.** Missing it yields `Cannot find package 'undefined'` from the driver, which looks like a broken driver, not a missing env var.
- **`DV_SETTINGS_JSON` supports `__PORT__`** (the driver substitutes the fixture port), so a preseed can name a prefixed route — needed whenever the test is about *which* endpoint got resolved.
- **Never run this while a `bats` run is in flight** in the same repo: `setup()` runs `bun run build`, so two runs clobber `dist/` and the pin test.
Copy the project's real player stub, `wpctl` fixture, `models.yml` and `ompconfig.yml` verbatim; a simplified stub prints a false `PLAYS 0`.
## Reading the result
**Trap (why A fails in a worktree):** module externals resolve from the *bundle's* directory. A bundle extracted to `/tmp` cannot resolve `@oh-my-pi/pi-natives` and dies with a version-pinned `Cannot find module .../pi-natives@<ver>/.../loader-state.js` — nothing to do with the fix under test. Putting the old bundle inside the repo (next to `node_modules`) fixes resolution, but in a linked-worktree where `node_modules` is a symlink to another checkout whose installed version differs from the version baked into `HEAD:dist`, it still fails. Then use route B.
Red side must show the *defect* (`PLAYS 0`, `644`, a chat `CALL` with the wrong prefix, `Invalid URL`), green side the fix — assert on printed markers only (`PLAYS`, `CALLS`, `CALL <url>`, `NOTIFY`, `SETTINGS`). `CALLS` counts `/audio/speech` calls only; the rewrite call appears as its own `CALL /…/chat/completions … sys=<full system prompt>` line, which is how prompt-level knobs (verbosity, heckle) are observed.
## Route B — swap the source, rebuild, restore (private worktree only)
`setup()` rebuilds `dist` from `src/`, so reverting one source file reproduces the pre-fix bundle exactly:
```bash
cp src/<file>.ts /tmp/new-<file>.ts
git show HEAD:src/<file>.ts > src/<file>.ts
bun run build >/dev/null
bats tests/behavior.bats --filter '<new test names>' # expect every new test NOT ok
cp /tmp/new-<file>.ts src/<file>.ts && bun run build >/dev/null && echo RESTORED
```
Always restore in the same command line that ran the red check, so an interrupted session cannot leave pre-fix source behind. `bats --filter` takes a regex over the `@test` titles; use it instead of the whole suite (the full run is ~5 min).
Read the failure lines, not just the count: a red test that fails because the driver crashed (`PLAYS 0`, no `CALL` lines) proves nothing about the fix.
## Gate afterwards
`bun run build` and commit `dist/` in the same commit as `src/` — `tests/packaging.bats` byte-compares the **committed** dist (`git diff --name-only -- dist` must be empty), so an uncommitted rebuild shows up as a red packaging test, not as a missing fix.
Prefer this over a synthetic leak: if the check already caught a real leftover/failure once, that event *is* the sensitivity proof — say so in the commit message instead of faking one.
@@ -1,85 +0,0 @@
---
name: fleet-maintenance-window-refresh
description: "Execute a fleet-wide OS + container image refresh on Dankclaw/Dankserver: apt without reboots, registry pin census, digest bump to main, per-service deploys, and proving the container actually moved."
---
"Update the servers / update all containers" on Dankclaw (192.168.0.143) + Dankserver (192.168.0.235). Both are Ubuntu 26.04, rootless podman quadlets, deploy via `scripts/deploy.sh`.
## 0. Recon before touching anything
```bash
for h in .143 .235; do ssh dank@192.168.0.$h 'uptime; systemctl --failed --plain --no-legend; sudo -n apt-get -s upgrade | grep -c ^Inst'; done
ssh dank@192.168.0.235 'ps -eo pid,etimes,args | grep -E "radiance|llama-server" | grep -v grep'
```
- A radiance/llama process holding the GPUs is a campaign or a bench: **do not touch it**, and never reboot without explicit approval.
- Note pre-existing failed units so you do not blame them on your deploy later (`mnt-md0.mount`, `nvidia-cdi-refresh*`, `logrotate` fail on Dankserver by default).
## 1. OS packages, no reboot
```bash
sudo -n env DEBIAN_FRONTEND=noninteractive NOTIFY_SOCKET= apt-get update -qq
sudo -n env DEBIAN_FRONTEND=noninteractive NOTIFY_SOCKET= apt-get -y \
-o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold \
-o APT::Get::AutomaticRemove=false full-upgrade
```
- `needrestart` is NOT installed on these hosts — check before assuming daemons will not bounce.
- Dankserver's initramfs trigger takes ~9 minutes; "dpkg BUSY" for 20 minutes is normal, do not kill it.
- After: `cat /var/run/reboot-required`, `uname -r` vs newest `/boot/vmlinuz-*`. A staged kernel is not a running kernel — say so.
- **Dankclaw only:** `grep -o 'iommu=pt' /boot/grub/grub.cfg` must still match after the kernel's `update-grub`, else the XDNA2 NPU lane dies at the next boot.
## 2. Registry pin census (full digests, not prefixes)
Read every `Image=` in `**/quadlet/*.container` plus `FRANK_*_IMAGE=` in `scripts/*.env`, resolve `tag -> Docker-Content-Digest` and compare.
- Docker Hub: token from `auth.docker.io/token?service=registry.docker.io&scope=repository:<repo>:pull`, then `HEAD registry-1.docker.io/v2/<repo>/manifests/<tag>` with an index+list+manifest `Accept`.
- ghcr.io: `https://ghcr.io/token?scope=repository:<repo>:pull&service=ghcr.io`.
- **lscr.io answers 404 through its own `/v2` path.** Its images are byte-identical on `docker.io/library/linuxserver/<repo>` — enumerate there; pinned lscr digests match Hub digests exactly.
- Print the FULL digest. A 12-char prefix cannot be pasted into a pin.
- `latest`-tagged services differ constantly; a differing digest IS the update. Do not call a versioned pin stale without a newer version tag.
## 3. Bump the pins in the repo
- Digest-only change: `sed -i 's|sha256:OLD|sha256:NEW|g'` (verify with `git diff -U0`).
- Tag+digest change: edit the line.
- Some pins MUST stay tagless `repo@sha256:…` — a bats seam greps the exact form (e.g. `frank-dragoon.env`). Check `grep -n 'FRANK_DRAGOON_IMAGE=' tests/*.bats` before adding a tag.
- Repo-wide truth is the canonical checkout **`/home/blank/Projects/serverAppConfig`** (on `main`, has `tests/e2e/.env.local`). Session worktrees cannot run `deploy.sh`: it refuses a dirty tree and any branch but `main`, and `git checkout` is blocked by workspace policy. Edit in the worktree, then there: `git pull --ff-only`, `git merge --no-ff <branch>`, `git push`.
- Restore canonical-checkout files you edited by accident with `git show HEAD:<path> > <path>` (checkout/restore are blocked).
## 4. Pre-pull, then deploy
Quadlets ship `Pull=never`/`missing` and the deployers refuse an absent image, so pull first as the **owner** (rootless user `dank`):
```bash
ssh dank@host podman pull -q 'repo/name:tag@sha256:...' # exact pinned ref
```
Then, from the canonical checkout, batch services per host in one invocation (each invocation also runs the ~1 min Playwright gate):
```bash
./scripts/deploy.sh dankclaw --service grafana --service alertmanager --service seerr
```
Order within a batch: DB before app (postgres/valkey/mariadb first), `gluetun` before `searxng` (shared netns). A failure aborts the rest of the batch, so group tightly.
## 5. PROVE the container moved
A green deploy is not evidence. Compare running digests to the pins:
```bash
ssh dank@host podman ps --format '{{.Names}}|{{.Image}}'
```
- Handlers that call `systemctl --user start` no-op on an active unit: the image installs, the old container keeps serving, and the health gate + e2e still pass. (This bit authentik server/worker on 2026-10-30; the handler now restarts.) If a digest did not move, `systemctl --user restart <unit>` and re-check.
- `systemctl is-active` without `--user` reports system units — Dankclaw app quadlets are user units. Always `--user` for them.
## 6. Locally built images (loki, alloy, caddy-porkbun)
`build_compose_image` skips the build when the image's `cfhash` label matches the Containerfile hash, so a rolling base (`alpine:latest`, `grafana/loki:latest`, `caddy:2`) never refreshes on its own. Force it by rebuilding with the same label, then deploy to restart:
```bash
h=$(sha256sum dankclaw/loki/Containerfile | cut -d' ' -f1)
podman build --format docker --no-cache --label "cfhash=$h" -t franksdank/${svc}:latest -f <Containerfile> .
```
- **zsh trap:** over `ssh` the remote shell is zsh, where `$svc:latest` is a history modifier (`:l` lowercases) — `-t franksdank/$svc:latest` tags `lokiatest`. Always brace: `${svc}:latest`.
- alloy is mirrored to the ROOT store by its handler (`mirror_image_to_root`), so deploy it rather than restarting the user unit.
## 7. Close
`./tests/run.sh` before each push (577 seams, ~85 s). Docs: update the endpoint/service page as the owner of the fact; `AGENTS.md` only for a cross-cutting law. Report the reboot as OWED, not done, when it is staged.
@@ -1,57 +0,0 @@
---
name: llamacpp-prism-fork-rebuild-swap
description: "Rebuild the PrismML llama.cpp fork on Dankclaw from origin/prism and swap frank-zealot's LLAMA_BIN onto it, including the gcc-13 host-compiler and install-RPATH requirements."
---
Rebuild the PrismML llama.cpp fork on Dankclaw and swap `frank-zealot` onto it, without guessing the build flags.
## Recover the build configuration first
The binary dir carries no CMakeCache. The configure that produced the running build lives in the source tree:
```bash
grep -E 'GGML_CUDA|CUDA_ARCH|BUILD_TYPE|SHARED|HOST_COMPILER|FA_QUANTS|GENERATOR' \
/home/dank/GitPackages/llama-prism-src/build-*/CMakeCache.txt
```
The pinned source clone is `/home/dank/GitPackages/llama-prism-src`, branch `prism`, remote `https://github.com/PrismML-Eng/llama.cpp.git`. `origin/HEAD -> origin/prism`; there is no `master`/`main`.
## Build into a fresh worktree at a pinned commit
```bash
SHA=$(git -C /home/dank/GitPackages/llama-prism-src rev-parse origin/prism)
git -C /home/dank/GitPackages/llama-prism-src worktree add --detach /home/dank/GitPackages/llama-prism-next "$SHA"
cmake -B .../llama-prism-next/build-cuda -S .../llama-prism-next -G Ninja \
-DCMAKE_BUILD_TYPE=Release -DGGML_CUDA=ON -DCMAKE_CUDA_ARCHITECTURES=86-real \
-DCMAKE_CUDA_HOST_COMPILER=/usr/bin/gcc-13 -DLLAMA_CURL=OFF -DGGML_NATIVE=ON \
-DCMAKE_INSTALL_PREFIX=/home/dank/GitPackages/llama-prism-<sha7> \
-DCMAKE_INSTALL_RPATH='$ORIGIN;$ORIGIN/../lib'
cmake --build .../build-cuda -j 8
cmake --install .../build-cuda
```
- **`CMAKE_CUDA_HOST_COMPILER=/usr/bin/gcc-13` is mandatory**: the box default is gcc 15 and nvcc 12.4 rejects it.
- **`CMAKE_INSTALL_RPATH` is mandatory**: `cmake --install` strips the build-tree rpath, and the installed binary then dies on `libllama-server-impl.so`. `patchelf` is NOT installed, so there is no post-hoc fix.
- 672 targets on the first full build; run it detached (`setsid nohup … > log`) and keep `-j 8` so the co-tenant services stay usable.
- Disk: the box runs ~84% full; check `df -h /home/dank` before starting.
## Verify the payload gate BEFORE swapping
```bash
D=/home/dank/GitPackages/llama-prism-<sha7>
$D/bin/llama-server --version # expect 0.x.y-dev (build N, commit <sha>)
ldd $D/bin/llama-server | grep -c "not found" # must be 0
strings $D/lib/libggml-base.so.* | grep -c PQ2_0 # custom tensor type 142
strings $D/lib/libllama.so.* | grep -c prism.hadamard # dequant metadata
```
A fork that dropped either cannot load `Ternary-Bonsai-2-9B PQ2_0` — stock llama.cpp refuses the file and a loader ignoring the metadata dequantizes wrong, silently.
## Swap
1. `scripts/frank-zealot.env`: `LLAMA_BIN=<D>/bin/llama-server` (install layout is `bin/`+`lib/`, not the flat dir older builds shipped — the old path `<dir>/llama-server` no longer exists).
2. Update the build/version in the env header and `serve-frank-zealot.sh` (default + comments).
3. Land on `main` from the canonical checkout, then `./scripts/deploy.sh dankclaw --service frank-zealot`.
4. Prove it: process arg0 is the new path, and the journal shows `- type pq2_0: N tensors`, `load_tensors_hadamard: loaded N Hadamard-folded weight(s)`, the adapter's `prism.abliterate.base_gguf` line, and `draft acceptance` once MTP drafts. Then probes: chat, a `tools` turn returning valid JSON `tool_calls`, and vision (solid-colour PNG via the OpenAI `image_url` content part, `max_tokens` ≥512 — thinking tokens count against it).
5. Rollback: point `LLAMA_BIN` back at the previous `llama-prism-b<old>` dir and redeploy. Keep that dir on disk.
## Test seam
`tests/frank_zealot_unit.bats` asserts `LLAMA_BIN` names a `llama-prism-*` **fork family** path, not a literal build directory: the suite runs on a box with no `/home/dank`, so existence cannot be checked there either. Do not re-pin a literal path.
@@ -1,67 +0,0 @@
---
name: media-library-dup-similarity-census
description: "Census a large media library for byte duplicates and same-film/different-encode copies with codecs, over SSH, without disturbing a live inference service."
---
# Media library duplicate + similarity census
Read-only pipeline for `find duplicates and similars in /mnt/... with sizes and codecs`
on a multi-TiB spindle library. Everything `nice -n 19`, one owner per stage, results as TSV.
## Stages (never skip the cheap ones)
1. **Enumerate** → TSV `size \t ext \t path`. Skip `.AGENTTRASH`, `@eaDir`, `.thumbnails`.
Report totals per extension + check `find -type f -links +2` (hardlinks are a separate
duplicate class).
2. **Size groups** → candidates for byte-identity. Seconds.
3. **Partial hash**: blake2b of first 4 MiB + last 4 MiB. Kills size collisions cheaply.
4. **Full blake2b** on the survivors only. Confirm, don't assume — report how many
partial-groups split on the full hash (0 = the head+tail screen was exact).
5. **ffprobe census** (codec, WxH, pix_fmt, duration, audio codec list) for every video
container. Parallel 6 workers; expect ~11-21 files/s on a spindle array with contention.
6. **Title/year/duration clustering** for "same film, different encode".
## Similarity engine: the rules that survive contact with real data
- **Runtime vetoes names.** Films titled `The Menu`, `Trailer`, `Trailer Park Boys`,
`ET — The Extra-Terrestrial` run 100+ min → not bonus material. Any file over ~45 min
is a film whatever it is called or wherever it sits.
- **Long-shaped bonus words** (commentary, documentary, interview) believed only under
~25 min; a feature-length commentary cut is a film.
- **Bonus folder evidence is path-wide below the film root** — `Film/Extras/x.mp4`,
`Film/Featurettes/Deleted Scenes/x.mkv`, `Film/04b. Film Extras/Logo.mp4` are all bonus.
The ROOT folder is exempt: it names the film. Match whole words only, so
`Extraordinary` / `The Extras` are not evidence.
- **A file named for the folder's film name is that film**, not its bonus — otherwise a
whole movie filed in another film's `Extras/` disappears from the film census.
- **Cluster by duration inside a title bucket**, not by title alone: 108 min vs 121 min
is a different cut, and franchise prefixes ("jump street") collide without a duration check.
- **Exclude release-group watermark clips** (`ETRG.mp4`, `RARBG.com.mp4`, `TGx.mp4`,
<20 MiB): they pair with themselves across unrelated films.
- **Exclude samples/trailers/menus/galleries** — generic names create cross-film pairs.
- BDMV trees: one pseudo-entry per tree, described by its longest stream (`BDMV/index.bdmv`
ends in `.bdmv`, so a `VIDEO_SUFFIX` check catches it); size = whole tree.
- AppleDouble `._*` files are not media.
## Operational traps
- `python3 enum.py` shadows the stdlib `enum` module — never name a script a stdlib name.
- Detached remote jobs: `setsid nice -n 19 ... > log 2>&1 < /dev/null &`. Killing the SSH
client does NOT kill the remote child — two hashers raced on one output file and truncated
it to 0 bytes. `pgrep -af` on the remote before relaunching, and kill the orphan by pid.
- A background bash job with a default deadline can die mid-read; for long reads use
`timeout: 0` or run detached and poll the log.
- Run probes and hashers **sequentially**, not in parallel — they contend on one array and
each starves the other.
- Ship scripts with `cat local | ssh host 'cat > /tmp/x.py'`; a filename collision with the
remote's cwd is invisible until it crashes.
- `/tmp/mc/` on Dankserver can vanish mid-session (systemd tmpfiles); pull TSVs local early.
- Quote remote awk/heredocs carefully — nested quoting fails before it ever runs.
## Reporting
Give: per-extension library totals; exact-dup table (size, codec, path); top same-film groups
by reclaim bytes with per-file size + codec + runtime; a reclaim table that states whether the
classes overlap; and the noise classes you excluded (extras, watermarks, `.!qB` partials,
TV episodes filed as movies). Audit your own duplicate tier: flag groups whose year/base-name/
runtime disagree and read every flagged group by hand before reporting the total.
@@ -1,28 +1,26 @@
---
name: monolith-split-under-dist-pin
description: "\"Use when cutting a monolithic source file into modules behind a committed-bundle pin (dist must stay byte-identical to src); one verified commit per cut, red-proof against the previous bundle. Not for feature work or doc-only edits.\""
description: "Use when cutting a monolithic source file into modules behind a black-box test suite whose committed build artifact (dist) must stay byte-identical to src; one verified commit per cut, red-proof against the previous bundle. Not for feature work or doc-only edits."
---
Procedure that landed a 1354-line `index.ts` as 9 modules + 301 lines of wiring across 9 commits with zero red commits; the same loop then split a 487-line `utterance.ts` in two cuts. Assumes: black-box test suite (tests drive the built bundle, not internals) and a pin test asserting the committed `dist/` equals a fresh build of `src/`.
# Monolith split under a committed-bundle pin
Procedure that landed a 1354-line `index.ts` as 9 modules + 301 lines of wiring across 9 commits with zero red commits. Assumes: black-box test suite (tests drive the built bundle, not internals) and a pin test asserting the committed `dist/` equals a fresh build of `src/`.
## Per-cut loop (one commit per cut, always in this order)
1. `bun run build` (or project equivalent) **before** staging — commit `dist/` in the same commit as the moved `src/`, otherwise the pin test fails ("committed dist is byte-identical to a fresh build") and you must amend. Amending is fine only while the branch is unpushed. Expect this to be the ONLY red test before the commit lands: it is `git diff --name-only -- dist` being non-empty, not a broken build.
1. `bun run build` (or project equivalent) **before** staging — commit `dist/` in the same commit as the moved `src/`, otherwise the pin test fails ("committed dist is byte-identical to a fresh build") and you must amend. Amending is fine only while the branch is unpushed.
2. `tsc --noEmit`, then run the full suite async (`bats tests/ > /tmp/x.log 2>&1; grep -E '^1\.\.'`).
3. **Launch the async suite with no deadline.** The default background deadline (~300 s) kills a minutes-long suite mid-run; the log then ends on a plausible `ok` line and reads like a partial pass. Likewise never edit `src/` while it runs.
4. **Never write to `src/` while the suite runs.** `setup()` rebuilds the bundle from `src/`, so a concurrent edit attributes someone else's half-written state to your run. Read-only prep (planning the next cut's exact line ranges) — or writing a NEW module file that nothing imports yet — is the correct thing to do while waiting: an unimported file cannot change the bundle.
5. Gate green → next cut. Gate red → fix, rebuild, amend, re-gate.
3. **Never write to `src/` while the suite runs.** `setup()` rebuilds the bundle from `src/`, so a concurrent edit attributes someone else's half-written state to your run. Read-only prep (planning the next cut's exact line ranges) is the correct thing to do while waiting.
4. Gate green → next cut. Gate red → fix, rebuild, amend, re-gate.
## Cutting mechanics
- Cut leaf/pure modules first (grammar, transcript rules, URL resolution, pure segmenter), then stateful ones (config, IO, pipeline, UI). Never let a module import `index` — if the new module needs something from `index`, that thing is a leaf and must move too (this is what forced `segment.ts` out early).
- Extract in **verbatim** bodies: read the exact ranges, write them into the new file, then delete the same ranges. Only module docs, export keywords, and injected dependencies change. Keeps each commit reviewable as a move.
- Edit tools that require "displayed lines" from the latest read snapshot: re-read the exact ranges under the current tag, then issue one edit call with all ops listed **bottom-up** (original line numbers throughout). Adjacent ranges merge into one `CUT a=b`; leaving a doubled blank line is the usual scar — one extra `CUT` next turn.
- A class member's JSDoc keeps the class's own indent (` /**` then ` * ` at member-level+1): a `PUT` body that starts at column 0 silently de-indents the whole block and the next `PUT` on those lines gets rejected for "restating the line below".
- Mutable config must reach a long-lived object as **getters**, not a construction-time snapshot: menu handlers mutate `cfg` after construction, and a snapshot freezes the settings the UI exists to change. Reading the getters once per unit of work (instead of per inner loop step) is a behavior improvement worth noting in the commit message.
- Mutable config must reach a long-lived object as **getters**, not a construction-time snapshot: menu handlers mutate `cfg` after construction, and a snapshot freezes the settings the UI exists to change.
- After moving code, grep the old file for the moved names: stale call sites compile under bundlers that skip typechecking (`stop()` vs `speaker.stop()` was caught only by `tsc`, not by `bun build`).
- A worktree may have no `node_modules` at all: the build fails on the natives loader with a version that doesn't match the primary checkout. `ln -sfn <primary>/node_modules <worktree>/node_modules` when `.gitignore` already anticipates the symlink (`git status` stays clean).
- Don't invent a third module to hold state the class already owns. If `queue.ts` would own no fields and need the pipeline injected as a callback, the queue stays in the class; say so in the report rather than shipping callback soup.
## Proving a new assertion is red-capable (before trusting it green)
@@ -36,20 +34,15 @@ DV_EXT=/tmp/old-dist.js <same env as the test> bun "$W/driver.ts" <scenario> #
DV_EXT="$PWD/dist/index.js" <same env> bun "$W/driver.ts" <scenario> # expect GREEN
```
A pre-fix bundle copied to `/tmp` can fail to load its natives (`Cannot find module …/pi-natives@X/loader-state.js`) because module resolution follows the *bundle's* directory, and it reports every probe as zero — not red, just dead. The robust alternative in a private worktree: `git show HEAD:src/<file>.ts > src/<file>.ts`, rebuild, run the filtered bats tests (they rebuild from `src` themselves), then copy your version back and rebuild. Keep the copy in `/tmp` before overwriting.
Copy the project's real player/server stub verbatim — a simplified stub (e.g. one that skips writing the log file the driver counts) prints a false `PLAYS 0` and looks like a failing fix. Pin the scratch dir to a fixed path; `ls -dt /tmp/tmp.*` picks up another session's directory.
## Black-box test hygiene learned the hard way
- A test that counts artifacts in the **shared** temp directory observes every other process on the box (live agent sessions synthesize into the same `dankvoice-*` dirs). Give the run a private `TMPDIR="$WORK/scratch-tmp"` and assert there; the assertion's sensitivity is proven by the fact it caught a real leftover while scoped globally.
- Let preseeds reference the ephemeral fixture port with a placeholder (`__PORT__`) substituted by the driver, so a test can preseed a URL pointing at its own fixture server.
- Count what the user **heard**, not what was requested. Synthesis counts double-count legitimately whenever a turn is cut off and the remainder is re-synthesized, so an assertion like "the intro appears once" fails on correct code. Stamp each clip with the arrival index of the request that made it (the existing tag byte), print the played tags from the player stub, and emit one `SPOKEN <text>` line per played tag by joining tags back to request payloads — then count hearings.
- Pinning an assertion to a timing window: drive both knobs from env (when the cut lands, how long a clip plays) and choose a window wider than the poll interval, so the test can't go red from a race it isn't about.
- Refactor-only commits get **no new test**: re-asserting unchanged labels/order after a commit whose point is that they are unchanged is a tautology; the existing suite is the gate. New tests belong to behavior changes, red-first.
- Tests must observe **output**, never internals: assert `PLAYS`/`CALLS`/`CALL <url>`/`NOTIFY <text>`, not that a variable was set.
- When a behavior fix turns a cluster of pre-existing tests red, check whether the cluster was pinning the *delivery path* rather than the feature: scenarios that drove a "only-unsolicited-narration" knob through a typed command were accidentally pinning the retired behavior. Move the scenario to the real path (deliver through the harness event) and leave the assertions word-for-word; strengthen any test whose fixture never answered (its assertion was absence, not evidence). Report the move explicitly — it looks like nine changed tests.
## Docs after a split (same branch, own commit)
Move each explanation to the file that now owns it (knob list → the module that reads env; state-machine prose → the module that implements it) and leave the entry point a one-line-per-module map. Then grep for pointers into the old file — a README saying "see header of `src/index.ts`" becomes a lie the moment the knobs move. Also re-check the laws table's test pointers: a law that claimed "pinned by scenarios asserting X" while those scenarios never armed the feature is a false citation, found by running the suite once.
Move each explanation to the file that now owns it (knob list → the module that reads env; state-machine prose → the module that implements it) and leave the entry point a one-line-per-module map. Then grep for pointers into the old file — a README saying "see header of `src/index.ts`" becomes a lie the moment the knobs move.
@@ -15,9 +15,7 @@ description: "Locate, edit, validate, and commit niri compositor config on Frank
- Profiles: `cfg/desk-<n>-<name>.kdl` (e.g. `desk-1-home.kdl`, `desk-2-portable.kdl`); ACTIVE profile is symlinked as `cfg/display.kdl` — `readlink` it to know which. `scripts/desk.sh` manages switching; `desk.sh capture` prints connected outputs' EDID long names.
- Panels are keyed by **EDID long name** (`output "Lenovo Group Limited 0x40A9 Unknown"` = the laptop eDP-1 panel), not connector — connector names reshuffle between desks. Long-name blocks BEAT connector-keyed blocks when both match one output (legacy connector blocks like `output "eDP-1"` linger as duplicates; niri applies the EDID block's `position`).
- Position math: x,y = top-left of each output in the logical layout; right edge = x + mode width. To place panel A left of panel B: `A.x = B.x - A.width`; bottom-align: `A.y = B.y + (B.height - A.height)`.
- **Changing `transform` rewrites every x, not one block.** A 1920x1200 panel rotated 90 is logically 1200 wide; normalizing it to `"normal"` makes it 1920 wide, which shifts the right edge +720 and therefore the x of every output to its right. Recompute the whole row from left to right and state the new x in the commit. Modes need no change (`mode` is the physical mode string, independent of transform).
- **"No matching profile" is a defect, not an idle state.** `desk.sh match` prints `no desk profile matches connected outputs` and `desk-autoswitch.sh` then exits at its initial match (the unit shows `inactive (dead)` with `status=0/SUCCESS`, minutes after start). The rig is not "unchanged" — it runs the *previous* desk's numbers, and a stale `mode` there falls back silently to preferred. Signature seen 2026-10-30: portable rig on `desk-1-home`, both 16" panels asked for `3440x1440@180.000` that no panel reported. Fix the profile, `desk.sh switch`, then `systemctl --user restart blankpad-desk-autoswitch` and confirm `is-active` — the watcher never comes back on its own.
- Verify: `niri validate` → hot-reload → `niri msg outputs | grep -iE 'Output |Position'` (prints `Logical position: x, y`). Compare `Logical size` and `Transform` too — that is what proves a rotation actually applied.
- Verify: `niri validate` → hot-reload → `niri msg outputs | grep -iE 'Output |Position'` (prints `Logical position: x, y`).
## Schema gotchas (niri 26.04)
- `preset-column-widths` AND `preset-window-heights` are children of `layout { }` as SIBLING blocks — wiki wording misleads; nesting or `window-rule` placement fails `niri validate` with `expected one of proportion, fixed` / `unexpected node`.
@@ -1,73 +0,0 @@
---
name: plex-metadata-title-cache-read
description: "Read the live Plex library DB read-only for clean titles/file paths (no sqlite3 binary, server running), and use it as the title authority over filename parsing."
---
Plex's metadata cache already holds a *clean* title for every file it matched
(`Alien`, not `Alien.Directors.Cut.1979.1080p.BRrip.x265-RARBG`). Read it instead
of parsing filenames, and name folders / group duplicates from it.
## Where it lives
Host package: data dir `/var/lib/plexmediaserver`, DB at
```
/var/lib/plexmediaserver/Library/Application Support/Plex Media Server/Plug-in Support/Databases/com.plexapp.plugins.library.db
```
(`Resources/com.plexapp.plugins.library.db` under `/usr/lib/plexmediaserver` is a
blank template — not the library.) The dir is `dank:dank`, so no sudo needed.
Containers: check `podman ps` first; on Dankserver Plex runs as a host process.
## Read it while Plex is RUNNING
No `sqlite3` binary on the fleet hosts — use `python3 -c` with `sqlite3`. Open
immutable read-only so no WAL/SHM is touched and no lock is taken:
```python
con = sqlite3.connect(f"file:{db}?mode=ro&immutable=1", uri=True)
```
A long local `find /` can hit the tool's own deadline; scope `find` with
`-maxdepth` or use the path above directly.
## Schema traps
* `library_sections` → column is **`section_type`**, not `type`. Movies = 1,
TV = 2, Music = 8.
* **`metadata_titles` does not exist** (empty name in the table list). Titles are
on `metadata_items`: `title`, `original_title`, `edition_title`, `year`,
`metadata_type`, `parent_id`, `deleted_at`.
* File path is `media_parts.file`; join
`media_parts → media_items (id = media_item_id) → metadata_items
(id = metadata_item_id)`, and `LEFT JOIN metadata_items par ON par.id =
mi.parent_id` for the show/movie parent.
* **Movies use the LEAF title** (`mi.title`); the parent row is empty. TV uses
the parent. Filter `p.deleted_at IS NULL AND mi.deleted_at IS NULL` or deleted
scans pollute the census.
```sql
select p.file, mi.title, mi.original_title, mi.edition_title, mi.year, m.container,
m.video_codec, m.audio_codec, m.width, m.height
from media_parts p
join media_items m on m.id = p.media_item_id
join metadata_items mi on mi.id = m.metadata_item_id
where p.file like '/mnt/entropy/Movies/%'
and p.deleted_at is null and mi.deleted_at is null
```
`media_items` also carries codec/resolution already — often no ffprobe pass is
needed for matched files.
## Use it as the title authority
Reject a Plex title only if it looks like a filename
(`2160p|1080p|x26[45]|hevc|bluray|web-?dl`) — a healthy cache scores **0** junk
titles. Coverage expectation measured on `/mnt/entropy/Movies`: 3,754 of 4,698
media files matched; the unmatched remainder is extras, BDMV disc trees, TV
episodes misfiled into Movies, and collection box sets — exactly the classes a
filename parser must still handle.
Propagate one match to every copy of a film (a group of versions) so sidecars,
`.nfo` and media files share one stem; fall back to the filename parser only
when no row exists.
@@ -1,54 +0,0 @@
---
name: plex-title-authority-for-relayout
description: "Read Plex's metadata cache as the clean-title authority (ziggurat schema, immutable read, no sqlite3 CLI) when naming or regrouping a media library from scripts instead of filename parsing."
---
Plex has already matched most of a library to real titles. When a script must name folders/files (Jellyfin relayout, dedup reports, library census), **read Plex's DB and let it override filename parsing** — `Alien.Directors.Cut.1979.1080p.BRrip.x265-RARBG` → `Alien`, `1917.2019.2160p…` → `1917`. Measured on dankserver's Movies section: 3,773 parts, **0** filename-shaped titles, 80% of media files matched (unmatched = extras, BDMV disc trees, TV).
## Locate and open the DB (no sqlite3 CLI needed)
```
DB="<data>/Library/Application Support/Plex Media Server/Plug-in Support/Databases/com.plexapp.plugins.library.db"
# data dir: /var/lib/plexmediaserver (host install) or the container's volume
```
```python
con = sqlite3.connect(f"file:{DB}?mode=ro&immutable=1", uri=True) # no copy, no wal, no lock
```
`immutable=1` is the whole trick: the DB was 1.5 GiB and Plex was live — no copy, no `sqlite3` binary, no risk of touching the service. Set `busy_timeout`; expect occasional inconsistency, acceptable for a read-only census.
## Schema trap: Plex renamed the tables ("ziggurat")
The old `sections` table **does not exist** — querying it throws, which reads like "wrong DB". Real names:
| table | join | key columns |
|---|---|---|
| `library_sections` | — | `id`, `name`, `section_type` (`movie`/`show`), `agent`, `scanner` |
| `section_locations` | `library_section_id` | `root_path`, `available` |
| `metadata_items` | `parent_id` → own `id` | `metadata_type`, `title`, `original_title`, `edition_title`, `year`, `indexed`, `deleted_at` |
| `media_items` | `metadata_item_id` | `container`, `video_codec`, `audio_codec`, `width`, `height` |
| `media_parts` | `media_item_id` | **`file`** (full path), `size`, `duration`, `deleted_at` |
Census section coverage first (`count(*) where file like '<root>/%'`), then dump:
```sql
select p.file, mi.title, mi.original_title, mi.edition_title, mi.year, m.video_codec, m.width, m.height
from media_parts p
join media_items m on m.id = p.media_item_id
join metadata_items mi on mi.id = m.metadata_item_id
where p.file like '<root>/%' and p.deleted_at is null and mi.deleted_at is null
```
For a movie library the leaf *is* the film; `parent_id` is NULL, so a `left join` on parent returns the same title. `metadata_type`: 1 = movie, 2 = show (a show section riding the same root means episodes live in the MOVIES folder — worth reporting).
## Using it in a renamer
- **One film, one name:** match any copy → apply that title to the whole group. Keyed by `file` path, so the map is exact.
- **Override, don't replace:** the parser still names the unmatched (extras, disc trees, TV, unscanned). Keep it as a fallback tier.
- **Bucket on the old key, name with Plex:** group/score sidecars with the parser's words (a `.srt` matches on the year token), format the folder from Plex: `f"{title} ({year})"`.
- **Sanitize for the FS:** Plex titles carry `:` and `&` (`2001: A Space Odyssey`, `Rock & Rule`) — those must go through the same sanitizer as filename-derived names.
## Cross-check before trusting a parser
Score the parser against the cache: count titles matching `2160p|1080p|x26[45]|bluray|hevc|10bit` — Plex returned **0**. Every parser mismatch is a real bug class: year-echo pseudo-titles (`1917.2019.2160p…`), two-year titles (`2001…1968`), numeric titles eaten as track indexes (`12 Monkeys`, `007 Skyfall`, `000 Miles to Graceland`, `0 - First Dates` — keep the number unless it's 1–2 digits glued to the words with a year present, i.e. `01. Bye Bye Baby`).
@@ -1,58 +0,0 @@
---
name: radiance-calib-tap-sizing
description: "Decide whether a radiance calibration tap is needed, how large a token floor it wants, and which step/window shape makes it fast — before booking a multi-hour card window."
---
# Sizing a radiance calibration tap
Read this before promising a tap run (hours to days of exclusive cards). It answers three questions:
*what is the tap for*, *how many tokens*, and *which shape makes it fast*.
## 1. The tap is for the Hessian, not the imatrix
`tools/rad_convert.cpp` help: `--imatrix F` is *"the expert-popularity profile, the column
importance"* — a diagonal summary. GPTQ's input is separate: the recipe names `gptq calib=<dir>`,
and `<dir>` is the tap's gram set: **d × d fp32 second moments (`E[x xᵀ]`) per expert per layer**.
- An external/published imatrix **cannot** substitute. It carries no second moments, it is keyed by
another model's tensor names, and radiance warns that *"a legacy-format imatrix cannot carry
[per-expert counts] at all — the planner will start cold."*
- The imatrix you need is a free by-product: `scripts/grams-to-imatrix.py <tap dir> -o imatrix.gguf`.
- Without a tap the experts can only take the recipe's non-GPTQ rule (`rtn`) — a visibly worse arm.
So "we only want an imatrix" is not a reason to skip it, and "just fetch one" is not a path.
## 2. The shape decides the throughput — window and step are one decision
The arch prices a step against the window: **QSA score plane ≈ rows × (window ÷ 4) × 4 bytes**.
| window | step (rows) | plane | verdict |
|---|---|---|---|
| 200,000 | 16,384 | 3,125 MiB | refused at boot (`the QSA score plane would be … Lower either`) |
| 32,768 | 16,384 | 512 MiB | fits |
| 32,768 | 32,768 | 1 GiB | fits |
Tap prefill is bound by **expert re-reads per step**, not by tokens as such, so the small window is the
lever: calibration corpora are re-chunked to ≤7,168-token documents and need nowhere near a 200k
window, yet copying the production window is the reflex that disqualifies a wide step and pins the run
to 2,048-token steps (≈70 tok/s on 2× R9700 = ~48 h for 12 M tokens). A 8,192–16,384-token step at a
32,768 window re-reads experts 4–8× less often per token.
Always boot once and read the ledger (`activation arena`, `experts`, `kv cache`) before believing any
speed estimate: a wider step grows the arena and steals from the experts.
## 3. Before you spend the cards
- **Arming proof**: the boot must print `recorded passes: off, the program accumulates calibration
Grams (gram_accum)`. A build that prints nothing writes nothing, however healthy `/health` and
`/stats` look (a 17-hour run on a shipped pin produced zero files).
- **The rig must own the socket**: `ss -ltnp 'sport = :7770'` → pid, and that pid must appear in
`podman top <rig> pid`. After the runner stops production, the dying process answers `/health` for a
moment while the new container is still loading — "container running + health 200" is not the rig.
- **Liveness is completions, not log mtime**: the engine logs one line per *completed* request, so a
4–8 minute prefill looks like silence while `kv_tokens_used` steps in 2,048-token chunks.
- **Space**: one seal writes ~40 GiB in one burst; the preflight needs ≥60 GiB free in the tap dir.
- **The floor is a campaign decision, not a vendor default**: the engine default is 65,536 tokens; a
12 M floor over a ~9.31 M-token corpus seals in round 1 plus part of round 2, and a round that seals
nothing is normal (the floor is pooled — never treat a flat round as a failure).
- **Exit means success**: at the seal the engine exits by itself; a mid-flight client gets
`429 rate_limit_error: the server is shutting down`.
@@ -1,68 +0,0 @@
---
name: radiance-calib-window-orchestrator
description: "Run the fleet's radiance calibration window through the repo orchestrator (scripts/qwen38-calib-window.sh): its gates, the arming proof, the seal contract, and the traps that make a tap look healthy while writing nothing."
---
# Run the calibration window through the orchestrator, never a hand-rolled driver
`serverAppConfigs/scripts/qwen38-calib-window.sh` is the only sanctioned driver for a radiance
calibration tap. Run it from the deploy checkout on the inference host:
```sh
cd /home/dank/serverAppConfigs
bash scripts/qwen38-calib-window.sh # add --dry-run to print the plan only
```
## What it owns (do not re-implement any of it)
| Gate | Detail |
|---|---|
| Inputs | reference recording `kld-ref/kld.json`, the BF16 `.rad`, the corpus (`Q38_WINDOW_CALIB_CORPUS`, default `calib-7168.jsonl` — the re-chunked file the rig's pool can hold) |
| **Space** | tap dir must have `Q38_CALIB_MIN_FREE_MIB` (default **61440** MiB) free **before a card is touched** |
| Rig | boots `scripts/run-qwen38-bf16.sh calib`, waits `/health` within `Q38_WINDOW_HEALTH_TIMEOUT` (3600 s), else kills the rig, restores the service, exits non-zero |
| Marker | the runner writes `calib/.rig-boot`; `calib-push.py` refuses without it, refuses if it names another container, refuses if that container is not running |
| Bank | the BF16 reference container moves to `~/.AGENTTRASH/<date>-qwen38-bf16-rad/` **only** if both phases exit 0 |
A hand-rolled `podman run` + push loop skips the space gate and the corpus choice — that is how a
43 GB seal lands on a volume with 35 GB free.
## Prove the tap ARMS before spending an hour
A build can carry every `RADIANCE_CALIB_*` string, seal message and plugin, and still never arm.
The arming proof is the boot ledger line:
```
I[0] recorded passes: off, the program accumulates calibration Grams (gram_accum)
```
No such line ⇒ nothing will be written, however healthy `/health`, `/stats` and the push look
(measured: 17 h, zero bytes, on a pin that lacked it). Diff the *sources* first if in doubt — the tap
code can be identical across versions while one shipped build drops it.
## Reading a live tap run
- **A quiet log is not a hang.** The engine logs one line per *completed* request. At 2–4-way
concurrency a 7.5 K-token prefill takes 4–8 minutes; `kv_tokens_used` steps in 2,048-token
chunks and `W step: nothing has been logged for 21 s` is step-level silence. Count completed
requests (`req cmpl`), never log mtime — a log-mtime watchdog kills healthy cycles, and every
reboot discards the accumulated tap.
- **Throughput is capped by expert streaming** (~60–70 tok/s on a disk-tier payload); concurrency
lengthens requests instead of raising throughput.
- **A port that answers is not the rig.** A dying production process answers `:7770` for seconds
after the stop. Gate on `podman ps` naming the rig container.
## The seal contract
- One seal writes **~40 GiB** (24,768 `gram.r<rank>.blk.<L>.ffn_down_exps.<E>.bin` files).
- `RADIANCE_CALIB_TOKENS` is a **pooled floor**: the engine sums Grams over prefill tokens and writes
at that count (engine default 65,536; the fleet runner defaults 1,500,000). The vendor's own
measurement: 10 M tokens beat 1 M by under 1 %.
- After writing, **the engine exits by itself**; in-flight clients get
`429 rate_limit_error: the server is shutting down` — that is success, not failure.
## Card exclusivity
A leftover engine keeps `/dev/kfd`; the next boot dies `E startup stopped: device error` with the
reason buried above the startup report. Kill by pid — `for p in $(sudo fuser /dev/kfd); do kill -9 $p; done` —
and never `pkill -f <name>` (the pattern matches your own command line). Verify the device is empty
before any boot and after any kill.
@@ -1,85 +0,0 @@
---
name: radiance-calib-window-serve-gate
description: "Diagnose a radiance calibration window that dies at second zero or refuses its push: the health answer coming from the dying production socket, the listener-identity gate, the preflight trio (space, re-chunked corpus, KEEP_RAD), the 12M floor and the wide-step knob."
---
# radiance calibration window — the serve gate and its preflight
Friction-earned 2026-10-30 on `dank/serverAppConfigs` (`scripts/qwen38-calib-window.sh`,
`scripts/run-qwen38-bf16.sh`). A 12 M-token window died at second zero; the cause was the gate,
not the engine.
## The signature, and what it really means
```
phase=calib health ok waited=0s
the rig container qwen38-bf16 is not running <- pusher refuses
phase=calib push end rc=2 ... FAIL: serve rc=1 push rc=2
```
`waited=0s` is the tell. The runner stops `frank-archon` **first**; the dying production process keeps
its listener for a moment, while the freshly created rig container is already `State.Running=true` but
has not bound the port yet. So `podman inspect` (container running) **and** `/health` 200 (production's
socket) both hold for the wrong process.
**The gate must prove the listener belongs to the rig:**
```bash
lpid=$(ss -H -ltnp 'sport = :7770' | grep -o 'pid=[0-9]*' | head -1 | cut -d= -f2)
[ -n "$lpid" ] || return 1
podman top "$RIG" pid | grep -qw "$lpid"
```
Landed as PR #90 with two red-first tests (foreign listener ⇒ no push, window fails; in-container
listener ⇒ proceeds and banks). If you see `waited=0s` on a checkout older than that PR, the window
there is pre-fix: do not run a long window on it.
## Preflight trio — the window refuses correctly, so do not "fix" it
1. **Tap dir ≥ 60 GiB free.** One seal writes **43 GB in 24,768 files**
(`gram.r<rank>.blk.<l>.ffn_down_exps.<e>.bin`). Point it at a 35 GiB root and the seal fills the
filesystem to zero. `Q38_CALIB_MIN_FREE_MIB` overrides — for *tests only* (fixtures on `/tmp` tmpfs).
2. **The re-chunked corpus.** `Q38_WINDOW_CALIB_CORPUS` defaults to `calib-7168.jsonl`; pushing
`calib.jsonl` instead runs un-chunked documents through a window sized for chunks.
3. **`Q38_WINDOW_KEEP_RAD=1` when two windows run back to back.** The window banks the BF16 reference
container into `~/.AGENTTRASH/` on success; without this, stage 2 has no reference to tap.
## Floor and the "add more tokens later" trap
`RADIANCE_CALIB_TOKENS` is the campaign's number, not the vendor's: the reference container this arm
competes with was calibrated on 10 M tokens, so the fleet floor is **12 M**. Engine default is 65,536.
**A sealed tap cannot be extended.** Hessians are VRAM accumulators; the seal writes and drops them.
So "run 1 M to verify, then add 11 M" is two windows — 1 M then a fresh 12 M — not an append. Budget
accordingly, and rotate the verification tap to `.AGENTTRASH/`.
One round of the re-chunked corpus is **9.31 M tokens**, so a 12 M floor seals in round 1 plus part
of round 2; a flat round is normal (the floor is pooled; `round_verdict` says "continue").
## The wide-step knob (the tap's real cost)
`Q38_CALIB_MNBT` / `Q38_CALIB_SEQS` set the calibration step and slots; everything else inherits
production. The tap is **read-bound**: a 7,168-token document cut into four 2,048-token steps
re-streams each layer's routed expert union four times, so a wider step is worth up to ~4× (the
runner's own header says this).
Wide steps are refused by the arch, not the budget, and the refusal scales with the window:
```
QSA score plane = rows x (--max-model-len / 4) x 4 bytes
16,384 rows @ 200,000 window -> 3,125 MiB -> REFUSED at boot
16,384 rows @ 32,768 window -> 512 MiB -> legal
```
The calibration corpus is chunked to 7,168 tokens, so **32,768 is the window to calibrate at** —
copying production's 200,000 window is what disqualifies the wide step.
## Judging a run
- **A quiet log is not a hang.** The engine logs one line per *completed* request; at 2-way a 7.5 K
prefill takes ~4 min. Watch `kv_tokens_used` (steps of the chunk size) and the `req cmpl` count.
A watchdog on log mtime kills healthy cycles and throws away accumulated tokens.
- **Sealing is success.** The engine exits by itself; a mid-flight client gets
`429 rate_limit_error: the server is shutting down`.
- **Kill by pid** (`sudo fuser /dev/kfd`), never `pkill -f`: an orphaned engine keeps the device and
the next boot dies in ~20 s with `E startup stopped: device error`, which reads like a bad shape.
@@ -1,85 +0,0 @@
---
name: radiance-calibration-tap-latest
description: "Capture radiance calibration Grams for GPTQ (RADIANCE_CALIB_DIR) on the R9700 rig: the tap only arms on latest, the fitting shape, the 12M floor rule, and how to read a run that looks hung."
---
Friction-earned 2026-10-30 on `dank/serverAppConfigs` (qwen38-w5nl64 campaign, 2×R9700 TP=2).
## The tap arms only on the latest radiance image
A 17-hour calib run on the *production* pin (1.8.1) wrote **zero bytes** into `RADIANCE_CALIB_DIR`
while `/health`, `/stats` and the pusher all reported healthy. The tap's arming code is byte-identical
in both trees — the shipped 1.8.1 build drops it. The tell is the boot line:
```
I[0] recorded passes: off, the program accumulates calibration Grams (gram_accum)
```
Present on latest (v1.14.0 = `:latest` 2026-10-30), absent on 1.8.1. Verify with
`podman inspect IMG --format '{{json .Labels}}'` → `org.opencontainers.image.version` /
`revision`, and pin the **index digest**, not the tag: `:latest` moves under you, and a local stale
`:latest` can be a 3-week-old `dev` build (check `.Created`).
Run the tap from a report container of the *same* model, but the tap needs `--ngram-placement disk`
for a calibration boot: the n-gram table pinned to RAM (`ram`) OOMs the rig's boot
(`startup stopped: out of memory`) while kldref/score boots succeed at the same pool.
## The fitting shape (vendor recipe does not boot 2×32 GiB)
The vendor's calibration line (`--max-num-batched-tokens 65536 --max-num-seqs 64`, ~930 tok/s)
assumes a bigger box: the activation arena alone is 12.83 GiB of 27.95 GiB claimable and every
variant stops in the VRAM resolver (short 3.19 GiB → 279 MiB → 59 MiB → 86 MiB → cache starved).
Wider steps are refused by the arch itself, pre-allocation:
```
qwen4exp: the QSA score plane would be 3125 MiB -- 16384 rows (--max-num-batched-tokens 16384)
by --max-model-len 200000 / compress ratio 4. Lower either.
```
What boots and seals: 4 slots, `--max-num-batched-tokens 2048` (the file-tier ceiling),
`--kv-cache-dtype fp8`, `--weights-disk-tier`, `--placement expert_tiered`,
`--ngram-placement disk`, `--max-model-len 200000`, `--num-speculative-tokens 0`, `--deterministic`.
Ledger at that shape: `elastic 22.8 GiB — experts first, kv cache 1.50 GiB … 1.0 full-length`
(pool ≈ 199,948 tokens). On 1.8.1 the same payload's pool was 7,436 tokens — the resolver, not the
window, is what changed.
## Floor
`RADIANCE_CALIB_TOKENS` is a **pooled** token floor. Engine default 65,536. Fleet rule (Frank,
2026-10-30): **12,000,000**, because the served quant this arm competes with was calibrated on a
10 M-token set. The vendor's "10 M beats 1 M by under 1 %" measures *their* plate, not ours.
Consequence: a ~9.3 M-token corpus round seals in round 1 plus part of round 2, and a round that
seals nothing is NORMAL (`round_verdict` → continue, never failure).
## Reading a run (the trap that cost two cycles)
- Throughput is **streaming-bound**, ~60–70 tok/s whatever the slot count; concurrency only lengthens
each request (2-way ≈ 4 min per 7.5 K-token prefill, 4-way ≈ 8).
- The engine logs **one line per completed request**. A log quiet for 8 minutes is therefore normal;
`W step: nothing has been logged for 21 s` is step-level silence. Liveness = **completed-request
count**, never log mtime. A watchdog on mtime kills healthy cycles and every false positive throws
away accumulated tap tokens.
- One seal writes **~43 GB** (24,768 `gram.r<rank>.blk.<l>.ffn_down_exps.<e>.bin`) and the engine
then **exits by itself**; a mid-flight client gets `429 rate_limit_error: the server is shutting
down` — success, not failure.
- A port that answers is not the rig: after the runner stops production the dying process answers
`:7770` briefly. Gate on `podman ps` naming the rig container, and on the `.rig-boot` marker.
## Orchestration and space
Use the repo's orchestrator, never a hand-rolled driver:
`bash scripts/qwen38-calib-window.sh` (gates: reference present, `.rad` present, tap dir ≥ 60 GiB,
healthy rig, marker, then push; banks the reference container only on success). Knobs:
`FRANK_QWEN38_CALIB_TOKENS`, `Q38_WINDOW_CALIB_CORPUS`, `Q38_WINDOW_ROUNDS`,
`Q38_CALIB_MIN_FREE_MIB`, `Q38_WINDOW_KEEP_RAD`.
A hand-rolled driver skips the space preflight; on this host the tap dir sat on `/` with 35 G free
while one seal writes 43 GB — the orchestrator refuses that, which is the point. Frank's rule: keep
artifacts on `/`, never `/mnt/entropy`.
## Then
`gram_files → scripts/grams-to-imatrix.py DIR -o imatrix.gguf` (stdlib-only, skips non-gram names
like `.rig-boot`) → `scripts/convert-qwen38-w5nl64.sh --plan-only` (space bill, `--plan-only` first)
→ convert (card-less, CPU BLAS) → `score` each arm + the served payload at the **production shape** →
`scripts/kld-gate-qwen38.sh` (mean KL ≤ 0.03, p99.9, top-1 ≥ 95 %, every `by_source` slice).
@@ -1,63 +0,0 @@
---
name: radiance-tap-run-observability
description: "Judge a live radiance calibration-tap run: which pin actually arms the tap, why a quiet engine log is not a hang, the watchdog that does not throw away accumulated grams, the seal's side effects, and the container-vs-port gate before pushing."
---
# Judging a live radiance calibration-tap run
Use when a tap (layer Hessians for `rad-convert`'s `gptq:calib=`) is running for hours on a drained
endpoint and you must tell progress from a stall without destroying the accumulation.
## Which image actually arms the tap
- **Verify `:latest` by digest, not by tag or by a local label.** `podman pull` then
`podman inspect … --format '{{.Labels}} {{.Created}}'`: a working box can hold a *stale* local
`:latest` whose label reads `version=dev/revision=unknown` while the registry's `latest` is a real
release (on 2026-10-30 that was v1.14.0, revision `4a61d7296bc5`). Compare the two before blaming
a version.
- The arming line is the only ground truth that the tap is in the program:
`I[0] recorded passes: off, the program accumulates calibration Grams (gram_accum)`.
Its absence means no tap, however healthy `/health`, `/stats` and the pusher look — that is how a
17-hour run on a production pin wrote zero bytes while everything reported fine.
- Reading source proves nothing about a build: the tap code was byte-identical in the version that
arms and the one that never writes. Judge the shipped image.
## A quiet log is not a hang
- The engine logs **one line per completed request** (`I req cmpl-… prompt=<n> tok`). At 60–70 tok/s
of prefill a 7.5 K-token document takes ~2 min alone, ~4 min at 2-way and ~8 min at 4-way
concurrency, and **nothing is logged in between**.
- Concurrently, `W step: nothing has been logged for 21 s` fires on *step*-level silence. It is a
diagnostic, not a verdict: `/health` stays 200, `/stats` keeps reporting `running: N`, and
`kv_tokens_used` still steps in batch-sized chunks.
- So a watchdog keyed on log mtime kills healthy cycles, and every kill discards the accumulation
(grams only materialise at the seal). **Key the watchdog on the completed-request count** and give
it a threshold far above the worst legitimate request latency (45 min for a 1.5 M-token floor).
- Cheap liveness triple: `grep -c "req cmpl" log` climbing, `kv_tokens_used` moving, `podman ps`
showing the rig container. Throughput is bounded by expert streaming, so raising concurrency
lengthens requests without raising total tokens/s.
## The seal and what it does
- One seal is ~43 GB (24,768 `gram.r<rank>.blk.<L>.ffn_down_exps.<e>.bin` files) — size the tap
filesystem before starting, and rotate any previous tap out first.
- The engine **exits by itself when the tap seals**. A client mid-flight then sees
`429 rate_limit_error: the server is shutting down`: that is success, not failure.
- The seal line is `radiance: calibration wrote layer Hessians after <n> tokens to /data/calib`.
Since the tap materialises all at once, "grams > 1000 files" is a valid post-seal trigger for a
chained imatrix → convert pipeline.
## Gate on the container, not the port
After a script stops the production service, the dying process still answers `:7770` for seconds, so
a health check can pass against the wrong thing. Require `podman ps` to name the rig container
**and** health to pass. A pusher that trusts the port alone pushes into a dead rig.
## Cards must be exclusive
An orphaned engine keeps `/dev/kfd`, and the next container boot dies with
`E startup stopped: device error` — which looks like a bad argument and is really a busy device.
Kill by pid from `sudo fuser /dev/kfd`, never `pkill -f`; verify `fuser` is empty before the next
boot. A genuinely wedged driver is a different failure: the *production* service then fails to serve
too, and only a reboot clears it — so prove the card healthy by restarting production and taking one
real chat turn before blaming your own shape.
@@ -1,16 +0,0 @@
---
name: read-managed-skill-before-update
description: "Before manage_skill update (which overwrites the whole body), read the existing SKILL.md via skill:// and merge; recover lost bodies from the git repo tracking managed-skills."
---
# Never overwrite a managed skill blind
`manage_skill action:"update"` replaces the **entire** `SKILL.md` body. Issuing it without the current text in hand silently destroys whatever the earlier session learned.
## Rule
1. Read first: `read skill://<name>` (or the file at `~/.omp/agent/managed-skills/<name>/SKILL.md`) in the SAME turn as the update call, then re-issue the body as existing-content + additions.
2. Keep additions append-shaped: a new section at the end, not a rewrite of prose that already worked. Near-duplicate sections are cheaper to clean up than lost knowledge.
3. If a body was already clobbered, recover before rewriting: `~/.omp/agent` (or the agent home) is commonly git-tracked — `git -C ~/.omp/agent log --oneline -- managed-skills/<name>/SKILL.md` then `git -C ~/.omp/agent show HEAD:managed-skills/<name>/SKILL.md`. If untracked, the version is gone; say so instead of pretending the rewrite preserved it.
Detection detail: a clobber usually shows up as a skill that still reads coherently but has lost the concrete env vars, paths, or trap list that made it actionable — i.e. the version you rewrote from memory of a sibling skill.
@@ -1,4 +1,4 @@
{
"v": 1,
"lastConsolidatedAt": 1793403795365
"lastConsolidatedAt": 1792536631688
}
@@ -1,6 +1,6 @@
{
"v": 1,
"lastConsolidatedAt": 1793399844747,
"lastConsolidatedAt": 1793314235443,
"lastResult": {
"at": 1791599865775,
"sessions": 1,
@@ -1,4 +1,4 @@
{
"v": 1,
"lastConsolidatedAt": 1793408441828
"lastConsolidatedAt": 1793396927634
}
@@ -1,13 +1,4 @@
## Structural Standards
Quant quality must hold regardless of implementation choice.
Container versions must be digest-pinned to latest snapshot.
## Model Corpus Constraints
Use the 1M corpus threshold for payload serving decisions.
## Merging Strategy
Merge anything that hasn't been merged yet, and let us try that faster shape.
@@ -2,4 +2,4 @@
## Behavioral Updates
Merge fixes immediately.
Merge any fixes immediately.
@@ -1,8 +1,8 @@
{
"v": 1,
"lastConsolidatedAt": 1793409834839,
"lastConsolidatedAt": 1793398614775,
"lastResult": {
"at": 1793403514528,
"at": 1793397014344,
"sessions": 1,
"deltas": 1,
"model": "frank-zealot"
@@ -1 +0,0 @@
## Style Standards
@@ -1,2 +0,0 @@
## Builds
- clear out those harbor builds
@@ -1,10 +1,4 @@
{
"v": 1,
"lastConsolidatedAt": 1793402531972,
"lastResult": {
"at": 1793400701944,
"sessions": 1,
"deltas": 1,
"model": "frank-zealot"
}
"lastConsolidatedAt": 1793398691951
}
@@ -1,6 +1,6 @@
{
"v": 1,
"lastConsolidatedAt": 1793401788576,
"lastConsolidatedAt": 1793398788553,
"lastResult": {
"at": 1793397235249,
"sessions": 1,
@@ -1,2 +0,0 @@
## Product behavior
- Normalize monitor rotation: hdmi monitor left of eDP
@@ -1,10 +0,0 @@
{
"v": 1,
"lastConsolidatedAt": 1793402665716,
"lastResult": {
"at": 1793401119393,
"sessions": 1,
"deltas": 1,
"model": "frank-zealot"
}
}
@@ -1,8 +0,0 @@
# Product Decisions
## File Organization
- Reorganize files to fit Jellyfin standard, clean filenames
## Constraints
- Do not delete any files
- Do not overwrite any files
@@ -1,10 +0,0 @@
{
"v": 1,
"lastConsolidatedAt": 1793409947332,
"lastResult": {
"at": 1793405994161,
"sessions": 1,
"deltas": 2,
"model": "frank-zealot"
}
}
@@ -1,4 +0,0 @@
{
"v": 1,
"lastConsolidatedAt": 1793409916309
}
@@ -1,4 +0,0 @@
{
"v": 1,
"lastConsolidatedAt": 1793410002774
}
@@ -4,9 +4,6 @@
* stylelint-disable at-rule-no-unknown
*/
/* App-scoped CSS of enabled community GTK extensions; Noctalia maintains this file. */
@import url("noctalia-extensions.css");
@define-color accent_color #f43841;
@define-color accent_bg_color #f43841;
@define-color accent_fg_color #181818;
@@ -4,9 +4,6 @@
* stylelint-disable at-rule-no-unknown
*/
/* App-scoped CSS of enabled community GTK extensions; Noctalia maintains this file. */
@import url("noctalia-extensions.css");
@define-color accent_color #f43841;
@define-color accent_bg_color #f43841;
@define-color accent_fg_color #181818;
@@ -107,11 +107,11 @@ widget_order = [
[lockscreen_widgets.widget."lockscreen-login-box@DP-2"]
box_height = 196.0
box_width = 810.0
cx = 959.99981689453125
cy = 1018.0001220703125
cx = 599.9998779296875
cy = 1628.8001708984375
output = "DP-2"
placement_height = 1200.0
placement_width = 1920.0
placement_height = 1920.0
placement_width = 1200.0
rotation = 0.0
type = "login_box"
@@ -135,11 +135,11 @@ widget_order = [
[lockscreen_widgets.widget."lockscreen-login-box@HDMI-A-1"]
box_height = 196.0
box_width = 720.0
cx = 960.0
cy = 1018.0
cx = 600.0
cy = 1628.800048828125
output = "HDMI-A-1"
placement_height = 1200.0
placement_width = 1920.0
placement_height = 1920.0
placement_width = 1200.0
rotation = 0.0
type = "login_box"
+16
View File
@@ -22,6 +22,22 @@ not restated here — entry, then pointer. That is what the 2026-10-21 audit pas
newest twelve entries: the facts stayed, the duplicates went, `git log --follow` keeps the
long form.
## 2026-10-30
### A desk with no matching profile is silently misconfigured
`desk-2-portable.kdl` stopped being a skeleton: the two PM1601 16" panels are
identity-keyed (`PNP(XYM) PM1601 0000000000000`, `… demoset-1`), laid out
landscape either side of the laptop, and `display.kdl` points at it. Law: a rig
with no eligible profile does not run "unchanged" — it runs whatever the *last*
profile said. Here that was the home profile, whose Sceptre identity block never
matched and whose connectors still carried `3440x1440@180.000`, so niri fell back
to preferred and every panel in force had a mode written for another desk.
`desk.sh match` reporting "no desk profile matches connected outputs" is the
defect signature, and `blankpad-desk-autoswitch` exits at its initial match —
check `is-active` after any profile edit. Page:
[tools/niri/displays.md](tools/niri/displays.md).
## 2026-10-29
### A vendor-cloud provider must be named the way the harness names it
+51 -4
View File
@@ -8,7 +8,7 @@ tags:
- monitors
- rotation
- desk-profiles
verified: 2026-10-21
verified: 2026-10-30
---
# Display Configuration
@@ -49,7 +49,7 @@ the dock and the cable, which is why the config keys on the EDID long name inste
| Panel | EDID long name | Keyed by | Notes |
|-------|----------------|----------|-------|
| Sceptre M34 ultrawide | `Sceptre Tech Inc Sceptre M34 Unknown` | identity | `mode "3440x1440@180.000"`, **deliberate** — the panel also offers `3440x1440@239.998`, `144.001` and `120.000`; 180 is driven while the link is degraded (see the law below) — `focus-at-startup` |
| PM1601 portrait | *not captured* | connector `HDMI-A-1` | dock-dependent; run `desk.sh capture` when plugged in, then re-key |
| PM1601 portrait | captured 2026-10-30 — the two panels are `PNP(XYM) PM1601 0000000000000` and `PNP(XYM) PM1601 demoset-1` | connector `HDMI-A-1` in *this* profile; identity on the portable desk | dock-dependent; the identity key is known now, so re-key the block when the panel next rotates through a dock |
| eDP-1 laptop panel | `Lenovo Group Limited 0x40A9 Unknown` | identity | scale **1.0** (deliberate; Ghostty grid at 1.25 rejected 2026-10-09), second `focus-at-startup` |
VRR: the M34 supports it (its connector today is `DP-1`); **left disabled by
@@ -58,6 +58,48 @@ decision** (2026-10-09). Add `variable-refresh-rate` per output if that ever cha
`focus-at-startup` on both M34 and eDP-1: niri focuses in config order, so
the ultrawide wins when present and the laptop panel is the fallback.
## The desk-2 rig (portable)
Laptop flanked by the two 16" PM1601 panels, all three landscape. Profile:
`cfg/desk-2-portable.kdl` (`display.kdl` symlinks to it).
```
┌──────────┬──────────────┬──────────┐
│ PM1601 │ │ PM1601 │ HDMI-A-1 1920x1200 normal x=0,y=0
│ 1920x1200│ eDP-1 │ 1920x1200│ eDP-1 1920x1080 normal x=1920,y=60
│(HDMI-A-1)│ 1920x1080 │ (DP-2) │ DP-2 1920x1200 normal x=3840,y=0
│ │ │ │
└──────────┴──────────────┴──────────┘ The laptop is 60 px down so it is
x=0,y=0 x=1920,y=60 x=3840,y=0 centred against the taller panels.
HDMI-A-1 sits to the LEFT of the laptop by request (2026-10-30); DP-2 keeps the
right. Earlier the same rig ran both panels in portrait (rot 90, 1200x1920) with
the laptop and both panels to its right — the rotation is `normal` now, so the
panels are 1920 wide and every x moved.
```
Positions are absolute in logical space, so rotating a panel changes its logical
width (1200 → 1920) and every x to its right; recompute the whole row, never just
the rotated block.
All three blocks are identity-keyed and pin their mode string verbatim. The
`requires:` list names all three panels, so the matcher only claims this profile
when the whole rig is present — unplug one 16" panel and the watcher leaves the
current layout alone rather than shuffling the survivors.
**The stale-profile trap this fixed (2026-10-30).** Before this profile existed,
the portable rig ran with `display.kdl` → `desk-1-home.kdl`, whose identity
blocks belong to the Sceptre ultrawide that is *not* connected and whose
connector-keyed fallbacks carried `mode "3440x1440@180.000"`. niri found no such
mode on a 1920x1200 panel, logged the fallback, and drove it at preferred — so
the panels looked fine while every position and refresh value in force had been
written for a different desk. `desk.sh match` reported "no desk profile matches
connected outputs" and `blankpad-desk-autoswitch.service` therefore died at its
initial match, leaving the laptop pinned to the wrong profile indefinitely.
Law: **a desk with no matching profile is not "unchanged", it is silently
misconfigured** — `desk.sh match` saying "no match" is a defect to fix before
anything else on that desk behaves strangely.
## Desk switching
```mermaid
@@ -80,8 +122,13 @@ graph LR
- **Manual override:** `Mod+Shift+D` cycles profiles (wrong auto-pick, or
same panels, new layout).
- **New desk:** `desk.sh capture` prints connected long names → copy into a
new `desk-N-<name>.kdl` (see `desk-2-portable.kdl` skeleton, portable
monitor EDID pending first capture).
new `desk-N-<name>.kdl` with one identity block per panel (template:
[the desk-2 rig](#the-desk-2-rig-portable)).
- **A dead watcher is a signal, not a crash to ignore:** the unit exits at
startup when `desk.sh match` finds no eligible profile, so "no match" on a
desk that clearly has panels means a missing profile, not an idle daemon.
Check `systemctl --user is-active blankpad-desk-autoswitch` after touching
desk profiles.
- Unit: `blankpad-desk-autoswitch.service` (systemd user,
`ConditionHost=blankpad`).
+1 -1
View File
@@ -18,7 +18,7 @@ Niri is a scrollable-tiling Wayland compositor. Windows are arranged in columns
├── autostart.kdl # Startup applications (noctalia, handy --start-hidden)
├── display.kdl # SYMLINK to the active desk-*.kdl profile (see displays)
├── desk-1-home.kdl # Desk profile: home rig (EDID-keyed)
├── desk-2-portable.kdl # Desk profile: portable monitor (skeleton)
├── desk-2-portable.kdl # Desk profile: portable rig (2x PM1601 portrait)
├── input.kdl # Keyboard, touchpad, mouse
├── keybinds.kdl # All key bindings
├── layout.kdl # Gaps, column widths, struts