Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d627368f71 | ||
|
|
d03fe57bbd | ||
|
|
8112433122 | ||
|
|
69f983f94d | ||
|
|
674a22be48 | ||
|
|
92c127ca08 |
No files matched your search
@@ -1,9 +1,9 @@
|
||||
---
|
||||
name: agenttrash-purger-invocation
|
||||
description: "Invoke the .AGENTTRASH purger (scripts/purge-agenttrash.sh) — plus its two refusal traps: ENOSPC masquerading as a stale lock, and an unreadable container-storage directory poisoning the whole root scan."
|
||||
description: "Invoke the .AGENTTRASH purger (scripts/purge-agenttrash.sh) — plus its refusal traps (ENOSPC as a fake stale lock, unreadable dirs poisoning a root) and the per-root scope trap."
|
||||
---
|
||||
|
||||
# Purging .AGENTTRASH — invocation, and the two traps that make it refuse
|
||||
# Purging .AGENTTRASH — invocation, and the traps that make it refuse or over-reach
|
||||
|
||||
`scripts/purge-agenttrash.sh` (serverAppConfigs, merged in PR #79 `fad87aa`) is the tool form of the fleet's deletion policy: files land in `.AGENTTRASH`, and an approved purge runs this instead of being improvised per session.
|
||||
|
||||
@@ -19,6 +19,10 @@ sh scripts/purge-agenttrash.sh --execute /some/root # scope to one root (defaul
|
||||
- Every printable line is fixed at plan time in `$HOME/.agenttrash-purge.lock/plan`, so dry-run and `--execute` output cannot drift. Exit `0` ok · `1` any refusal or failure · `2` usage.
|
||||
- Deleting a big staged item by hand (e.g. `find <dir> -depth -delete`) is the fallback **only** when the tool cannot run at all — see trap 1 — and the operator's purge approval covers exactly that.
|
||||
|
||||
## Trap 0: the approval is per item, the scope is per root
|
||||
|
||||
The plan verb `empty <root>/.AGENTTRASH` means **the whole trash directory**, not the one item the operator approved. So before `--execute`, list the root and prove it holds only the approved staged item — `ls -A <root>/.AGENTTRASH && du -sh <root>/.AGENTTRASH/*` — and note that a second `.AGENTTRASH` deeper in the tree is also in scope (`plan remove` on it = it was empty, safe). Measured 2026-10-30 on dankserver: `/mnt/entropy/.AGENTTRASH` held exactly the approved `20261030-movies-monk` (124 files, 32 G) plus an empty `qwen38-w5nl64/.AGENTTRASH`; plan → `--execute /mnt/entropy` reported `removed=2 deleted=2 refused=0` and free space rose 187 G → 218 G.
|
||||
|
||||
## Trap 1: a full filesystem makes it lie about a lock (measured 2026-10-30)
|
||||
|
||||
With **0 bytes free**, three invocations refused with
|
||||
@@ -39,4 +43,4 @@ and no lock directory existed anywhere. The script writes its lock *and* its pla
|
||||
|
||||
## Verified good path (dankserver, 2026-10-30)
|
||||
|
||||
After freeing space by hand: `/tmp`-scoped plan → `--execute` deleted the staged item and reported `removed=1 deleted=1`; a second `--execute /mnt/entropy` cleaned that volume's root (`refused=0`). The cleanup log carries one dated line per run; read it instead of re-deriving what happened.
|
||||
After freeing space by hand: `/tmp`-scoped plan → `--execute` deleted the staged item and reported `removed=1 deleted=1`; a second `--execute /mnt/entropy` cleaned that volume's root (`refused=0`). A 32 G staged item on an 87 T volume scans + purges in ~90 s per pass (the scan, not the deletion, is the slow part). The cleanup log carries one dated line per run — both the dry run and the execute log the verb `plan`, so tell them apart by `removed=`; read it instead of re-deriving what happened.
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
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.
|
||||
@@ -0,0 +1,82 @@
|
||||
---
|
||||
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.
|
||||
@@ -0,0 +1,94 @@
|
||||
---
|
||||
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.
|
||||
@@ -0,0 +1,76 @@
|
||||
---
|
||||
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,58 +1,48 @@
|
||||
---
|
||||
name: dankvoice-bundle-swap-red-proof
|
||||
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."
|
||||
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.
|
||||
---
|
||||
|
||||
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/`.
|
||||
# Red-proofing a dankvoice behavior test against the pre-change bundle
|
||||
|
||||
## Why
|
||||
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.
|
||||
|
||||
`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
|
||||
## Route A — `DV_EXT` at the old committed bundle
|
||||
|
||||
```bash
|
||||
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
|
||||
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
|
||||
```
|
||||
|
||||
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.
|
||||
Build the driver harness by extracting the heredoc `setup()` writes into `tests/behavior.bats`:
|
||||
|
||||
## Traps, each one cost a turn
|
||||
```bash
|
||||
sed -n '<first-body-line>,<last-body-line>p' tests/behavior.bats > "$W/driver.ts"
|
||||
```
|
||||
|
||||
- **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.
|
||||
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.
|
||||
|
||||
## Reading the result
|
||||
Copy the project's real player stub, `wpctl` fixture, `models.yml` and `ompconfig.yml` verbatim; a simplified stub prints a false `PLAYS 0`.
|
||||
|
||||
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.
|
||||
**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.
|
||||
|
||||
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.
|
||||
## 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.
|
||||
@@ -0,0 +1,85 @@
|
||||
---
|
||||
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,24 +1,42 @@
|
||||
---
|
||||
name: line-edit-anchor-discipline
|
||||
description: "Avoid the recurring edit-tool line-op traps: empty PUT deletes lines, PUT ranges swallowing multi-line block delimiters, and anchors landing mid-expression. Use whenever issuing CUT/PUT line edits on tagged files."
|
||||
description: "Avoid the recurring edit-tool line-op traps: empty PUT deletes lines, PUT ranges swallowing neighbours, wrong-anchored PUTs replacing the wrong line, literal markdown bullet rows arriving verbatim as \"+-\"."
|
||||
---
|
||||
|
||||
# Line-edit anchor discipline (CUT/PUT ops on tagged files)
|
||||
# Line-edit anchor discipline
|
||||
|
||||
Five self-inflicted syntax wounds in one session (franksign, 2026-10-21) all trace to three rules.
|
||||
The `edit` tool addresses lines, not intent. Three traps cost real time; all three
|
||||
fail *silently* or with a repair notice that is easy to skim past.
|
||||
|
||||
## Rules
|
||||
1. **An empty PUT body DELETES the targeted lines** (tool warns: "Interpreted an empty PUT body as deletion"). Twice it ate a whole log entry / a closing `}`. For no-ops use nothing; for deletes use explicit `CUT`.
|
||||
2. **Never PUT a range whose endpoints cut through a multi-line block.** If a `| {`-style union variant, a frontmatter `---` fence, or a `test(...)` closer straddles the range boundary, the replacement swallows the opener/closer and strands the body. Pre-condition: line N-1 and M+1 must be block-complete. Fix-after-the-fact pattern: read the broken region raw, then `PUT k.=k:` to restore the single swallowed line (e.g. the missing `---`, the orphaned ` | {`, `}` → `});`).
|
||||
3. **Search-hit reads do not authorize PUT.** A grep/folded-summary read marks lines as "never displayed"; the edit tool refuses and echoes actual content. That refusal is a free verification — re-check the echoed lines match intent, then straight-retry with the same tag.
|
||||
## 1. Empty PUT deletes the range
|
||||
|
||||
## Costly mistakes seen
|
||||
- Empty `PUT 7-7:` deleted a just-written log entry; restoring required re-issuing the full entry text.
|
||||
- `PUT 6.=7` replacing `timestamp`+`---` dropped the frontmatter close fence → lint-breaking file silently.
|
||||
- `PUT 10.=11` on a union type swallowed ` | {` opening a 12-line variant → cascade of LSP "unexpected label" warnings.
|
||||
- `CUT` of a stranded opener left its dangling `.json())` behind; balance errors surface only at EOF (`expected ')' but file ends`) — locate by reading regions raw, not by widening the payload.
|
||||
`PUT n.=m:` with no body rows deletes lines n..m. To insert, use `PUT >n:`
|
||||
(after line n) or `PUT <n:` (before line n). Never send a PUT range whose body is
|
||||
empty unless deletion is the goal.
|
||||
|
||||
## Discipline
|
||||
- After any edit that reports a syntax error or a warning: read the touched region with `:raw` before the next op; fix the single wrong line, do not resend the block.
|
||||
- "no change: byte-identical" means your anchor math is off — re-read, don't append more content.
|
||||
- Prefer replacing whole coherent blocks (start line = block start, end line = block end) over edits that end mid-statement.
|
||||
## 2. PUT ranges must be the complete final content of exactly those lines
|
||||
|
||||
- Replacing one line: `PUT n.=n:` with exactly one body row. If the body carries
|
||||
the *previous* line as its first row (a copy-paste of the anchor), that line is
|
||||
duplicated and the real target line is overwritten — no error, and the
|
||||
"auto-repaired replacement boundary" note is the only hint.
|
||||
- Anchor only on lines the current `[path#tag]` display actually showed. If the
|
||||
tool answers "anchors to lines you never displayed", the line numbers are
|
||||
wrong: re-read that range, then re-issue. Do not retry blind.
|
||||
- Multi-op edits: order ops bottom-up (highest line numbers first) so earlier
|
||||
inserts cannot shift later anchors.
|
||||
|
||||
## 3. Literal `- ` rows (markdown bullets) cannot go through `edit`
|
||||
|
||||
A body row starting with `-` parses as a CUT op. The error says to prefix it with
|
||||
`+` (`+- item`) — **that advice is wrong in practice**: the file ends up
|
||||
containing the literal `+- item`. When the content legitimately needs leading
|
||||
`- ` bullets (prompt law text, changelog entries, lists), write it with `write`
|
||||
after a full `read` of the file, or restructure the line to avoid the leading
|
||||
dash.
|
||||
|
||||
## Verification habit
|
||||
|
||||
After any PUT that replaces a range, `read` the touched range (not the whole
|
||||
file) and confirm the neighbouring lines survived — especially the line after the
|
||||
range, which is what a swallowed boundary eats. Cheap; catches the silent cases.
|
||||
@@ -0,0 +1,57 @@
|
||||
---
|
||||
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.
|
||||
@@ -0,0 +1,75 @@
|
||||
---
|
||||
name: magic-number-provenance-audit
|
||||
description: "Audit every numeric threshold in a change before shipping it: classify provenance (inherited port / declared authority / invented), delete invented arithmetic, and replace budgets with derivations quoted from the repo's own config. Use when adding or reviewing a cap, window, timeout, ratio, or size ladder, or when asked \"what's with the magic numbers?\"."
|
||||
---
|
||||
|
||||
# Magic-number provenance audit
|
||||
|
||||
A threshold with no source is a bug with a comment on it. The deliverable is either a
|
||||
derivation from an authority the repo already trusts, or deletion of the arithmetic.
|
||||
|
||||
## 1. Census, do not defend
|
||||
|
||||
Grep the changed paths for every numeric literal (exclude the module being rewritten so
|
||||
its own digits don't hide the pattern):
|
||||
|
||||
```
|
||||
grep -rn "1536\|4000\|700\|6000\|n_ctx\|contextWindow\|maxTokens\|tokens" src/*.ts
|
||||
```
|
||||
|
||||
Table per number: value | file:line | **provenance** | verdict. Provenance is exactly one of:
|
||||
|
||||
- **inherited port** — copied from upstream, pinned by a contract test. Keep, but say so in
|
||||
the doc comment.
|
||||
- **declared authority** — a field the runtime/fleet config already publishes.
|
||||
- **derived** — computed from beats it guards (e.g. lease TTL = 3 heartbeats).
|
||||
- **invented** — yours. This is the finding. Say "mine, invented", not "chosen".
|
||||
|
||||
Also flag the *application* of a number, not just its value: a slice like
|
||||
`text.slice(0, MAX_INPUT)` is a **head-only silent drop** (loses the tail of a dump);
|
||||
a floor is a different animal from a cap even at the same digit.
|
||||
|
||||
## 2. Find the authority before writing a digit
|
||||
|
||||
Rank of acceptable sources, best first:
|
||||
|
||||
1. **The runtime's own published field.** `~/.omp/agent/models.yml` model rows carry
|
||||
`contextWindow` (PER-SLOT for llama.cpp: it divides `--ctx-size` across slots) and
|
||||
`maxTokens`. The fleet's own clamp law is quoted in the comments there — reuse its shape:
|
||||
`chars = (contextWindow - maxTokens - 4096) * 4 - lawChars` (`4` = the file's `chars/4`
|
||||
token estimator, `4096` = its clamp margin). Then every figure in the derivation is a
|
||||
quotation, not a guess.
|
||||
2. **A shipping style/behaviour law in the repo.** e.g. "two or three sentences, one breath
|
||||
each" → the ceiling belongs in the prompt as *words the model reads* ("at most three
|
||||
sentences; fewer if little happened"), not as `sqrt(chars / 1200)` in code.
|
||||
3. **A physical constant of the medium** (speech rate, packet size), measured against the
|
||||
actual device. Last resort: it is still a number you must defend.
|
||||
|
||||
Traps when parsing that config: commented decoy lines (`# contextWindow: 200,000` with a
|
||||
comma) must be skipped, so strip only lines whose trimmed form starts with `#`; the row
|
||||
scanner that returns on the `id:` line never sees the fields below it — keep scanning to the
|
||||
next `- id:`/provider key.
|
||||
|
||||
## 3. Encode the fix so the digit cannot come back
|
||||
|
||||
- Make the budget a **required parameter** of the pure module (no default: a module with no
|
||||
default cannot guess); the caller that resolved the endpoint does the derivation.
|
||||
- A derived budget that comes out non-positive is a **refusal** (`null`, zero requests),
|
||||
never a truncation — the caller's cursor/queue must hold and retry.
|
||||
- When an old cap must survive as a fallback for endpoints that declare nothing, rename it
|
||||
to what it is (`INHERITED_FLOOR`) and comment: floor, used only when nothing is declared.
|
||||
|
||||
## 4. Tests that pin the law, not the digit
|
||||
|
||||
- **Size-invariance:** assert the instruction/behaviour is *identical* for a tiny input and a
|
||||
huge one, proving volume no longer drives the removed ladder.
|
||||
- **Honours caller budget:** same span folded under an injected small window.
|
||||
- **Refusal, not truncation:** zero-window → null, zero model calls.
|
||||
- **DELETE** tests that pinned the deleted arithmetic; never re-pin them (house rule).
|
||||
- Pinning shipped prompt *words* is legitimate where the repo already does (prompt-contract
|
||||
test precedent); pinning an env/config value is a config assertion and belongs nowhere.
|
||||
|
||||
## 5. Report
|
||||
|
||||
Table first, verdict per number, and the net list of digits that remain after the change —
|
||||
each with its quoted source. If any digit survives with no source, that is the finding.
|
||||
@@ -0,0 +1,47 @@
|
||||
---
|
||||
name: media-journal-chain-verification
|
||||
description: "Verify a journal-guarded media relayout end to end: counts/bytes/size-mismatch from the JSONL journals, plus cross-journal accounting that proves \"missing\" destinations were moved onward rather than lost."
|
||||
---
|
||||
|
||||
# Verifying a media relayout across a chain of journals
|
||||
|
||||
`scripts/media-apply.py` (serverAppConfigs) writes a write-ahead JSONL journal beside the library root — one line per intent, `op` ∈ `mkdir|move`, a `move` record carrying `src`, `dst`, `bytes`, `mode`. Multiple runs over the same library produce **several journals** (relayout, then TV move, then duplicate staging), and the same file legitimately appears in more than one. Verifying one journal in isolation then reads as data loss.
|
||||
|
||||
## Verify counts and bytes from the journals, never from scratch files
|
||||
|
||||
`/tmp` and shell history are not records; the journals are. Read-only pass over every journal:
|
||||
|
||||
```python
|
||||
import json, os
|
||||
def recs(j): return [r for r in (json.loads(l) for l in open(j) if l.strip()) if r.get("op")=="move"]
|
||||
for j in journals:
|
||||
m = recs(j)
|
||||
gone = sum(1 for r in m if not os.path.exists(r["src"]))
|
||||
at = sum(1 for r in m if os.path.exists(r["dst"]))
|
||||
bad = sum(1 for r in m if os.path.exists(r["dst"])
|
||||
and os.path.getsize(r["dst"]) != r.get("bytes"))
|
||||
print(os.path.basename(j), len(m), sum(r.get("bytes",0) for r in m)/2**30, gone, at, bad)
|
||||
```
|
||||
|
||||
Assertions that matter: `src-gone == moves` (nothing half-moved), `size-mismatch == 0` (no truncation or clobber), `dst-present == moves` **for the newest journal only**.
|
||||
|
||||
## The chain step: account for every missing destination
|
||||
|
||||
An older journal's `dst` disappears when a later journal moves the file onward (episodes shelled out to the TV library, duplicates staged into `.AGENTTRASH`). Prove it instead of asserting it:
|
||||
|
||||
```python
|
||||
later = {}
|
||||
for j in later_journals:
|
||||
for r in recs(j): later.setdefault(r["src"], []).append(os.path.basename(j))
|
||||
missing = [r["dst"] for r in older_recs if not os.path.exists(r["dst"])]
|
||||
print(len(missing), sum(1 for d in missing if d in later),
|
||||
[d for d in missing if d not in later])
|
||||
```
|
||||
|
||||
Expect `unexplained = 0`. Measured 2026-10-30 on dankserver: 8,189 movie moves / 22,380 GiB with 174 destinations gone — exactly 124 (duplicate staging) + 6 (hand-filed strays) + 44 (mover episodes), `accounted-by-later-journal=174 unexplained=0`, zero size mismatches across all three journals.
|
||||
|
||||
## Post-purge consequence to state in the report
|
||||
|
||||
After an approved `.AGENTTRASH` purge the staging journal still parses, but `dst-present=0`: `media-apply.py rollback` on it recreates the folders **empty**. Say so — that journal is a directory-shape undo, not a byte restore; the keeper copy is the one that stayed in the library.
|
||||
|
||||
Do not trust a `dst-present` shortfall as loss until the cross-journal check has run, and never report counts from a `| grep -c` pipeline — mask-free exit status only.
|
||||
@@ -0,0 +1,67 @@
|
||||
---
|
||||
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,26 +1,28 @@
|
||||
---
|
||||
name: monolith-split-under-dist-pin
|
||||
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."
|
||||
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.\""
|
||||
---
|
||||
|
||||
# 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/`.
|
||||
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/`.
|
||||
|
||||
## 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.
|
||||
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.
|
||||
2. `tsc --noEmit`, then run the full suite async (`bats tests/ > /tmp/x.log 2>&1; grep -E '^1\.\.'`).
|
||||
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.
|
||||
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.
|
||||
|
||||
## 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.
|
||||
- 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.
|
||||
- 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.
|
||||
- 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)
|
||||
|
||||
@@ -34,15 +36,20 @@ 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.
|
||||
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.
|
||||
@@ -15,7 +15,9 @@ 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)`.
|
||||
- Verify: `niri validate` → hot-reload → `niri msg outputs | grep -iE 'Output |Position'` (prints `Logical position: x, y`).
|
||||
- **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.
|
||||
|
||||
## 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`.
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
---
|
||||
name: omp-session-journal-cursor-access
|
||||
description: "Read a live omp session's transcript mid-run from an extension (cursor = journal entry id via ctx.sessionManager), with the entry-type noise list, append lag, and the ctx.setInterval metronome."
|
||||
---
|
||||
|
||||
# Reading the live session journal from an extension
|
||||
|
||||
Use when an extension must summarise/narrate/index **everything said since some point**
|
||||
rather than reacting to one event payload, or when you need a durable "where I left off"
|
||||
pointer into a session. Verified 2026-10-30 with a throwaway probe under `omp -p`
|
||||
(v18.12.5), 1 s ticks over a two-tool-round run.
|
||||
|
||||
## The accessor (no plugin install needed)
|
||||
|
||||
`ctx.sessionManager` is on every `ExtensionContext` (`ReadonlySessionManager`,
|
||||
`@oh-my-pi/pi-coding-agent/dist/types/extensibility/extensions/types.d.ts:485`):
|
||||
|
||||
```
|
||||
getEntries() getBranch() getEntry(id) getLeafId() getLeafEntry()
|
||||
SessionMessageEntry = { id, type: "message", message: AgentMessage }
|
||||
```
|
||||
|
||||
- **The cursor is an entry id.** Persist `getLeafId()` (or the id of the last entry you
|
||||
actually consumed); "everything since" = entries after that id. Advance is O(1).
|
||||
- **`getEntries()` grows mid-run** — proven by a timer, with no handler help:
|
||||
`entries=3 → 6 → 7 → 9 → 11` while `idle=false`, leaf ids rotating.
|
||||
- **Append lags `message_end` by ~0.5–1 s.** So "the live edge" always under-reads by a
|
||||
second. Consume only up to the last id you actually read; the sub-second tail rides into
|
||||
the next window. Never advance past what you read.
|
||||
- **`ctx.setInterval(cb, ms)` / `ctx.setTimeout`** (`types.d.ts:555-572`): throws contained,
|
||||
`unref`'d, auto-cleared on `session_shutdown`. Use these, never raw `setInterval`.
|
||||
`ctx.isIdle()` is the cheap "is this session even working" guard.
|
||||
|
||||
## The journal is not the conversation (allowlist, never denylist)
|
||||
|
||||
`agent_end` reported `msgs=6` while the journal held `11` entries. Bookkeeping types in the
|
||||
union: `custom` (zero content, ~3 per tool round), `model_usage`, `thinking_level_change`,
|
||||
`mode_change`, `model_change`, `service_tier_change`, `label`, `title_change`,
|
||||
`ttsr_injection`, `session_init`, `credential_pin`, `reset_boundary`. Speech-relevant:
|
||||
`message` (roles `user` / `assistant` / `toolResult`) and, as context, `compaction` /
|
||||
`branch_summary`. New types appear on the harness's own schedule → an allowlist keeps them
|
||||
silent until someone decides they are speech.
|
||||
|
||||
Re-export the per-message readers instead of re-parsing content: an assistant message's
|
||||
narration-flagged `thinking` part is **answer text**, unflagged `thinking` is deliberation
|
||||
(`narration: true` discriminator).
|
||||
|
||||
## Robustness rules that came out of the failure modes
|
||||
|
||||
- Empty window ⇒ leave the cursor where it was (nothing new must not read as news).
|
||||
- Cursor id no longer in the journal (compaction, branch rewrite) ⇒ treat as "from the top
|
||||
of what is left", never as "nothing" — a lost pointer must not mute a session forever.
|
||||
`session_compact` and `session_branch` are dispatched events if you want to re-anchor eagerly.
|
||||
|
||||
## Probe recipe (proves any of the above in ~60 s, touches nothing live)
|
||||
|
||||
Project-local extensions load without installing: write `<run-dir>/.omp/extensions/probe.js`
|
||||
(default-export `function (pi)`), log to an absolute path, then
|
||||
|
||||
```bash
|
||||
: > /tmp/probe.log
|
||||
timeout 220 omp -p --thinking off --cwd <run-dir> \
|
||||
"Run these bash commands strictly one after the other: first: sleep 7; echo FIRST. then: sleep 7; echo SECOND. Then reply: FIRST SECOND"
|
||||
```
|
||||
|
||||
Register the tick inside `session_start` (`ctx.setInterval(() => stamp(info(ctx)), 1000)`),
|
||||
and give every entry you expect to exclude a speech-shaped payload with a sentinel word —
|
||||
an absence assertion with an empty producer proves nothing. Do NOT pass `--no-session`:
|
||||
the journal is the thing under test. Teardown: nothing to undo (no plugin link, no
|
||||
`~/.omp/plugins/package.json` write) — delete the run dir.
|
||||
@@ -0,0 +1,73 @@
|
||||
---
|
||||
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.
|
||||
@@ -0,0 +1,54 @@
|
||||
---
|
||||
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`).
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
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`.
|
||||
@@ -0,0 +1,68 @@
|
||||
---
|
||||
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.
|
||||
@@ -0,0 +1,85 @@
|
||||
---
|
||||
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.
|
||||
@@ -0,0 +1,85 @@
|
||||
---
|
||||
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).
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
name: radiance-endpoint-silent-wedge-triage
|
||||
description: "Diagnose a radiance (frank-archon) endpoint that is wedged or crawling while systemd says active(running) and /health answers 200 — separate engine-state wedge from config, hardware, swap and clock causes, and prove the restart fixed it."
|
||||
---
|
||||
|
||||
# Silent wedge: `active (running)` + `/health` 200, but no throughput
|
||||
|
||||
For radiance endpoints (`frank-archon`, root unit whose `ExecStart` is `exec podman wait`). The unit can only exit when the container dies, so a stalled engine loop **never** triggers `Restart=always`, `up` stays `1`, and Alertmanager stays empty (every rule keys on `up==0`, `num_requests_waiting>0`, or TTFT p95). Verified 2026-10-30 21:00 on dankserver.
|
||||
|
||||
## Triage ladder (in this order — each step kills a class)
|
||||
|
||||
1. **Front end vs engine split.** `curl -w '%{time_total}' /health` vs `/v1/models`. A 0.4 ms `/health` beside a 20 s `/v1/models` timeout = front end alive, engine loop stalled.
|
||||
2. **Unit truth, not the port.** `systemctl status frank-archon -n 0` + `systemctl show -p MainPID,DropInPaths,ExecMainStartTimestamp`. A 4 h-old unit on a 2-week-old host means a restart happened today — cross-check `journalctl -u <svc> --since <day>` for `Starting/Stopping/exit` and `git log -- scripts/<svc>.*` to see whether a deploy caused it. Non-empty `DropInPaths` = leftover bench arm (separate trap).
|
||||
3. **Container storage namespace.** The unit is `User=dank`, so the container lives in **rootless** storage: `sudo podman ps -a` lists nothing and `podman inspect` says `no such object` — query as the user, without sudo.
|
||||
4. **Engine's own voice (the decisive artifact).** `podman logs --timestamps frank-archon | grep "E device:"`. `a synchronisation has seen no queue advance for 5 s` naming `barrier-and dep[0]=1` on both ranks = the TP all-reduce never completed. Timestamp the first occurrence; that is the incident start.
|
||||
5. **Rule out the engine's own suggested causes before believing anything else:** `cat /sys/module/amdgpu/parameters/cwsr_enable` (its message blames `cwsr_enable=0`; `1` falsifies it). Swap: read `/proc/<engine-pid>/status` VmSwap — do NOT infer from `free`/`/proc/swaps` totals (host-wide swap can be 32 GiB while the engine holds 378 MiB). Clocks during load: fire a request, then `rocm-smi --showclocks --showperflevel --showpower` + `/sys/class/drm/cardN/device/gpu_busy_percent` — 100 % busy at full sclk means neither throttle nor starvation.
|
||||
6. **Quantify the collapse from counters, not vibes.** `radiance:engine_step_seconds_decode_total` ÷ `radiance:engine_steps_decode_total` = mean decode step time; diff two scrapes (Δseconds ÷ Δsteps) for the current rate. Compare to the shape's recorded baseline in the endpoint's vault page (15.5 ms at frank-archon's 4×200000 fp8 shape; 209 ms = 13× collapse).
|
||||
|
||||
## Proof of fix (same-request A/B, not a health check)
|
||||
|
||||
Submit one fixed request before and after the restart and diff the engine's own `timings` object — an 8-token probe measures only first-token noise, use `max_tokens>=128`:
|
||||
|
||||
```
|
||||
curl -sS -m 240 -o /tmp/q.json -X POST :7770/v1/chat/completions \
|
||||
-H 'Content-Type: application/json' -d @/tmp/query.json # then read timings.predicted_per_token_ms / predicted_per_second / ttft_ms / draft_n / draft_n_accepted
|
||||
```
|
||||
|
||||
Before/after on the identical request: 90.76 → 11.06 ms/token, 11.02 → 90.40 t/s, ttft 420 → 148 ms, acceptance unchanged (266/266). Then re-read the boot ledger (`elastic / kv cache / vram_weights / unclaimed / sampler rows`) and confirm it matches the page's recorded ledger — a successful restart proves nothing about the flags. Close with the restore oracle: `is-active` **and** `is-enabled` **and** no `/run/systemd/system/<unit>` mask symlink **and** container owned by the unit **and** `DropInPaths` empty.
|
||||
|
||||
## Law
|
||||
|
||||
A wedged radiance engine has **no in-place recovery** (its own wording: "a queue the driver evicted and could not restore") — `systemctl restart` is the only cure, and the fix must be proven by a rate, never by `/health`. Durable gap: no alert fires on a live-but-stalled endpoint; the missing rule is a decode-step-time ratio gated on `up==1` in `shared/prometheus/rules/llm-fleet.yml`.
|
||||
@@ -0,0 +1,63 @@
|
||||
---
|
||||
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.
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
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": 1792536631688
|
||||
"lastConsolidatedAt": 1793403795365
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1793417535449
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1793314235443,
|
||||
"lastConsolidatedAt": 1793399844747,
|
||||
"lastResult": {
|
||||
"at": 1791599865775,
|
||||
"sessions": 1,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1793396927634
|
||||
"lastConsolidatedAt": 1793419146014
|
||||
}
|
||||
@@ -1,4 +1,13 @@
|
||||
## 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 any fixes immediately.
|
||||
Merge fixes immediately.
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1793398614775,
|
||||
"lastConsolidatedAt": 1793420934894,
|
||||
"lastResult": {
|
||||
"at": 1793397014344,
|
||||
"at": 1793403514528,
|
||||
"sessions": 1,
|
||||
"deltas": 1,
|
||||
"model": "frank-zealot"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
## Style Standards
|
||||
Whitespace-only changes.
@@ -0,0 +1,2 @@
|
||||
## Builds
|
||||
- clear out those harbor builds
|
||||
@@ -1,4 +1,10 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1793398691951
|
||||
"lastConsolidatedAt": 1793402531972,
|
||||
"lastResult": {
|
||||
"at": 1793400701944,
|
||||
"sessions": 1,
|
||||
"deltas": 1,
|
||||
"model": "frank-zealot"
|
||||
}
|
||||
}
|
||||
Whitespace-only changes.
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1793398788553,
|
||||
"lastConsolidatedAt": 1793401788576,
|
||||
"lastResult": {
|
||||
"at": 1793397235249,
|
||||
"sessions": 1,
|
||||
|
||||
Whitespace-only changes.
@@ -0,0 +1,2 @@
|
||||
## Product behavior
|
||||
- Normalize monitor rotation: hdmi monitor left of eDP
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1793402665716,
|
||||
"lastResult": {
|
||||
"at": 1793401119393,
|
||||
"sessions": 1,
|
||||
"deltas": 1,
|
||||
"model": "frank-zealot"
|
||||
}
|
||||
}
|
||||
Whitespace-only changes.
Whitespace-only changes.
@@ -0,0 +1,8 @@
|
||||
# Product Decisions
|
||||
|
||||
## File Organization
|
||||
- Reorganize files to fit Jellyfin standard, clean filenames
|
||||
|
||||
## Constraints
|
||||
- Do not delete any files
|
||||
- Do not overwrite any files
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1793420867382,
|
||||
"lastResult": {
|
||||
"at": 1793418430591,
|
||||
"sessions": 1,
|
||||
"deltas": 1,
|
||||
"model": "frank-zealot"
|
||||
}
|
||||
}
|
||||
Whitespace-only changes.
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1793421016379
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1793417502833
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1793421007031
|
||||
}
|
||||
@@ -4,6 +4,9 @@
|
||||
* 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,6 +4,9 @@
|
||||
* 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 = 599.9998779296875
|
||||
cy = 1628.8001708984375
|
||||
cx = 959.99981689453125
|
||||
cy = 1018.0001220703125
|
||||
output = "DP-2"
|
||||
placement_height = 1920.0
|
||||
placement_width = 1200.0
|
||||
placement_height = 1200.0
|
||||
placement_width = 1920.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 = 600.0
|
||||
cy = 1628.800048828125
|
||||
cx = 960.0
|
||||
cy = 1018.0
|
||||
output = "HDMI-A-1"
|
||||
placement_height = 1920.0
|
||||
placement_width = 1200.0
|
||||
placement_height = 1200.0
|
||||
placement_width = 1920.0
|
||||
rotation = 0.0
|
||||
type = "login_box"
|
||||
|
||||
@@ -279,22 +279,22 @@ transition = [ "fade" ]
|
||||
interval_seconds = 1200
|
||||
|
||||
[wallpaper.default]
|
||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven-gw26v3.png"
|
||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven-mlomkm.png"
|
||||
|
||||
[wallpaper.last]
|
||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven-gw26v3.png"
|
||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven-mlomkm.png"
|
||||
|
||||
[wallpaper.monitors.DP-1]
|
||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven-gjqmel.png"
|
||||
|
||||
[wallpaper.monitors.DP-2]
|
||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven-gw26v3.png"
|
||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven-mlomkm.png"
|
||||
|
||||
[wallpaper.monitors.HDMI-A-1]
|
||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven-gw26v3.png"
|
||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven-mlomkm.png"
|
||||
|
||||
[wallpaper.monitors.eDP-1]
|
||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven-gw26v3.png"
|
||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven-mlomkm.png"
|
||||
|
||||
[weather]
|
||||
unit = "imperial"
|
||||
|
||||
@@ -22,22 +22,6 @@ 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
|
||||
|
||||
@@ -8,7 +8,7 @@ tags:
|
||||
- monitors
|
||||
- rotation
|
||||
- desk-profiles
|
||||
verified: 2026-10-30
|
||||
verified: 2026-10-21
|
||||
---
|
||||
|
||||
# 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 | 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 |
|
||||
| PM1601 portrait | *not captured* | connector `HDMI-A-1` | dock-dependent; run `desk.sh capture` when plugged in, then re-key |
|
||||
| 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,48 +58,6 @@ 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
|
||||
@@ -122,13 +80,8 @@ 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` 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.
|
||||
new `desk-N-<name>.kdl` (see `desk-2-portable.kdl` skeleton, portable
|
||||
monitor EDID pending first capture).
|
||||
- Unit: `blankpad-desk-autoswitch.service` (systemd user,
|
||||
`ConditionHost=blankpad`).
|
||||
|
||||
|
||||
@@ -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 rig (2x PM1601 portrait)
|
||||
├── desk-2-portable.kdl # Desk profile: portable monitor (skeleton)
|
||||
├── input.kdl # Keyboard, touchpad, mouse
|
||||
├── keybinds.kdl # All key bindings
|
||||
├── layout.kdl # Gaps, column widths, struts
|
||||
|
||||
Reference in new issue
Block a user