live: heartbeat sync 2026-10-18T17:49:00-07:00 (Refs #11)
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
---
|
||||
name: deployment-checkout-trunk-sync
|
||||
description: Use when the live deployment checkout (~/.omp main) must merge a moved origin/main while dirty tracked files block the merge and workspace policy blocks git checkout.
|
||||
---
|
||||
|
||||
---
|
||||
name: deployment-checkout-trunk-sync
|
||||
description: Use when the live deployment checkout (~/.omp main) must merge a moved origin/main while dirty tracked files block the merge and workspace policy blocks git checkout.
|
||||
---
|
||||
|
||||
# Sync a dirty live checkout to a deployment (checkout blocked)
|
||||
|
||||
Situation: the checkout IS the deployment; months of machine/hand writes sit
|
||||
uncommitted; `origin/main` has moved; `git checkout`/discard is BLOCKED by
|
||||
workspace policy ("edit forward"; exemption only during active conflict).
|
||||
|
||||
## Procedure
|
||||
1. `git fetch`; trial `git merge --no-commit --no-ff origin/main` to enumerate
|
||||
exact blockers (dirty files the merge would overwrite + untracked collisions).
|
||||
Untracked blockers: `cp -a` to `.AGENTTRASH/pre-merge-<date>/`, then `mv` aside.
|
||||
2. **Preservation commit:** `git add` ONLY the blocking paths (not the whole
|
||||
tree) and commit them as-is: `chore: adopt pre-existing working-tree state
|
||||
before trunk merge`. Nothing is discarded; the secret gate still runs.
|
||||
Include EVERY dirty deployment-state path (dotfiles/live tiers too) — a
|
||||
dirty file that is neither committed nor trashed is unprotected from step 3.
|
||||
3. `git merge --no-ff origin/main`. Conflicts are now resolvable — the policy
|
||||
exemption is active during a merge.
|
||||
4. Resolution law:
|
||||
- **live machine config** (`agent/config.yml`, `models.yml`, dotfiles live
|
||||
state like noctalia `settings.toml`) → `--ours`: the running box is its
|
||||
own authority; the remote copy is a snapshot.
|
||||
- **prose/law/docs** (`AGENTS.md`, KB) → `--theirs` (upstream canonical;
|
||||
local variants survive in the preservation commit).
|
||||
- **append-mostly logs** → union: cut only the three marker lines
|
||||
(`<<<<<<<`/`=======` range + `>>>>>>>`), keep both sides' entries.
|
||||
5. `git commit --no-edit`, push trunk, verify `main == origin/main`.
|
||||
6. Pre-merge byte-compare every dirty file vs `origin/main`
|
||||
(`git show origin/main:$f | diff -q - $f`): SUPERSEDED (identical → discardable),
|
||||
DIVERGED (resolve), untracked-in-upstream (collision).
|
||||
|
||||
## Gotchas
|
||||
- **NEVER `git stash` to unblock a deployment sync, NEVER `git stash clear`/
|
||||
`drop` afterward.** Observed 2026-10-18: a sync stashed the dirty tree, the
|
||||
stash entry was later destroyed, and months of GUI-built bar layout
|
||||
(noctalia settings.toml, never committed) vanished with it — recovered only
|
||||
via dangling-object archaeology (below). Step 2's preservation commit exists
|
||||
precisely so stashing is never needed.
|
||||
- **Stash-and-retry loops are a red flag**: three `WIP on main` stash commits
|
||||
within ~60s of each other means the operator was fighting the dirty tree,
|
||||
not resolving it.
|
||||
- **Recovery from destroyed stashes** (objects survive entry deletion until gc):
|
||||
`git fsck --lost-found` → `git log -1 --format='%ci %s' <dangling>` to date
|
||||
them → `git show <c>:<path>` to dump candidates; newest `WIP on` commit is
|
||||
usually the last good state. Validate before deploying (e.g.
|
||||
`noctalia config validate`), back up the clobbered file to `.AGENTTRASH/`,
|
||||
then restore through the symlink target.
|
||||
- Never wrap `git checkout` in `2>/dev/null` here: the policy-block error was
|
||||
swallowed and the loop silently no-op'd; the merge then failed identically.
|
||||
- `git add` a directory pathspec stages its untracked children (fine for
|
||||
allowlist dirs; never for prose dirs).
|
||||
- The live-sync heartbeat (#11) commits machine tiers hourly, so this sync is
|
||||
a rare operation post-deployment — mostly memories/dotfiles stragglers.
|
||||
Remember the heartbeat can lag up to an hour behind GUI edits: the tree
|
||||
between heartbeats is the ONLY copy of fresh GUI work.
|
||||
- **Frank's accepted risk posture (2026-10-18):** hourly sweep + the
|
||||
preservation-commit law + fsck recovery is defense-in-depth ENOUGH. Do not
|
||||
re-propose faster heartbeats or path/event triggers to shrink the
|
||||
GUI-edit-to-commit window; he chose status quo after measuring (heartbeat
|
||||
itself is 57 ms CPU — cost was never the objection; un-debounced
|
||||
DirectoryNotEmpty triggering was, and path-unit proposals are vetoed).
|
||||
@@ -0,0 +1,125 @@
|
||||
---
|
||||
name: frank-tts-add-cloned-voice
|
||||
description: "Add or repair a cloned voice on the dank fleet: /home/dank/voices/<name> pack, name rules, boot-warm registration, and the under-a-second proof"
|
||||
---
|
||||
|
||||
Use for "give frank-tts a new voice", "voice X is missing/unknown", or "the first
|
||||
request of the day takes 5 s". Runbook of record: vault pages [[Frank TTS]] (voice
|
||||
packs, naming) and [[Frank TTS Engine]] (the warm registry). Everything below runs
|
||||
as the deploy user on **Dankclaw** (`ssh -o BatchMode=yes dankclaw '…'`); the
|
||||
containers are **rootless** (`systemctl --user …`, `podman …` without sudo).
|
||||
|
||||
## Naming (the question that gets asked first)
|
||||
|
||||
A voice is addressed **by its directory name** — `voice=narrator1` on
|
||||
`POST /v1/audio/speech` (omp's TTS device may send `voice_id`, same value). Use:
|
||||
|
||||
- `^[a-z0-9][a-z0-9._-]*$`, **lowercase**, ≤ 64 chars. No slash, no `..`, no
|
||||
space, no uppercase; must not start with `.`/`_`/`-`.
|
||||
- Why lowercase rather than just "alphanumeric": the shim accepts
|
||||
`[A-Za-z0-9][A-Za-z0-9._-]{0,63}` (`app.py`, `VOICE_NAME_RE`) but the **engine
|
||||
lowercases names**, so `Narrator1` resolves on one side and not the other. The
|
||||
boot warmer refuses a mixed-case pack loudly instead of half-working.
|
||||
- Names are user-visible: a 400 carries them in `available_voices`. Fleet
|
||||
convention is `narrator1`, `narrator2`, … for the open-notebook podcast
|
||||
speakers — open-notebook is configured with exactly `narrator1/2/3`, so renaming
|
||||
those breaks podcast rendering, not just TTS — and a lowercase personal name for
|
||||
a person's own voice.
|
||||
|
||||
## Pack layout (host state — never commit it)
|
||||
|
||||
`/home/dank/voices/<name>/` is a voice **iff** it holds `reference.wav`:
|
||||
|
||||
```
|
||||
reference.wav 24 kHz mono pcm_s16le, 18-25 s of clean single-speaker audio
|
||||
reference.spk engine x-vector: enc_dim 2048 f32 = 8192 B
|
||||
reference.rvq RVQ codes: 22 B per 12.5 Hz frame (5456-6864 B)
|
||||
manifest.json source path + sha256, crop start/end, engine commit, name
|
||||
```
|
||||
|
||||
Build it; do not hand-assemble and never fabricate `.spk`/`.rvq` from a WAV:
|
||||
|
||||
```bash
|
||||
scripts/build-voice-pack.sh --all /home/dank/voices # everything in VOICE_PACKS
|
||||
scripts/build-voice-pack.sh --source SRC --offset SEC --duration SEC --sha256 HEX NAME /home/dank/voices
|
||||
scripts/build-voice-pack.sh --dry-run ... # validate + crop, no extractor
|
||||
```
|
||||
|
||||
Knobs live in `scripts/voice-packs.env` (one `_SOURCE`/`_OFFSET`/`_DURATION`/
|
||||
`_SHA256` set per voice). Three things it enforces that an operator gets wrong:
|
||||
|
||||
1. **The 10-second gate.** A crop starting inside the first 10 s of the source is
|
||||
refused before anything is written (project source-material law), and the
|
||||
manifest is re-checked against the source's sha256 afterwards.
|
||||
2. **The effective crop window is 19.84-25 s, not 18-25 s.** An 18.0 s crop is a
|
||||
legal `reference.wav` but yields only 22 × 225 = 4 950 B of `.rvq`, under the
|
||||
5 456 B floor the codec config implies — so the builder says so up front.
|
||||
3. **It never deletes.** An existing pack is refused unless `--force`, and
|
||||
`--force` moves the old directory to `$PACKS_DIR/.AGENTTRASH/<voice>-<UTC>/`.
|
||||
|
||||
Files must be world-readable (the container runs as uid 10000): `644` files,
|
||||
`755` dirs. `mkdir -p /home/dank/voices` as the deploy user if it is absent —
|
||||
rootless podman auto-creates a missing bind source **root-owned**, and the
|
||||
endpoint then sees no voices at all.
|
||||
|
||||
## Register and warm (the part people forget)
|
||||
|
||||
The engine keeps its voice registry **in memory**, and the **first synthesis of a
|
||||
registered voice decodes the whole reference clip** to snapshot codec state:
|
||||
6.2-8.0 s, first audio byte at ~5 530 ms. Later requests restore in 0.5 ms.
|
||||
Re-registering does **not** warm — only synthesizing does. So:
|
||||
|
||||
```bash
|
||||
# registers every pack that has a reference.wav, then speaks the warm sentence once
|
||||
podman exec frank-tts-engine /app/warm-voices.sh
|
||||
systemctl --user --no-pager --full status frank-tts-engine.service | tail -20
|
||||
```
|
||||
|
||||
The unit's
|
||||
`ExecStartPost=-/usr/bin/podman exec frank-tts-engine /app/warm-voices.sh` does
|
||||
the same at every boot, so a fresh boot is already warm; the explicit call above
|
||||
is for **adding one voice to a running engine** and needs no restart — a restart
|
||||
drops every registration and re-pays the cold cost for the whole library. The
|
||||
`podman exec` form is not cosmetic: the engine **publishes no port**, so there is
|
||||
no host address to curl.
|
||||
|
||||
## Proof (do not report "added" without it)
|
||||
|
||||
```bash
|
||||
curl -s localhost:7772/v1/audio/voices # name is listed
|
||||
time curl -s -o /tmp/a.wav localhost:7772/v1/audio/speech \
|
||||
-H 'content-type: application/json' \
|
||||
-d '{"model":"tts-1","voice":"NAME","input":"Checking the cloned voice.","response_format":"wav"}'
|
||||
time curl -s -o /tmp/b.wav localhost:7772/v1/audio/speech \
|
||||
-H 'content-type: application/json' \
|
||||
-d '{"model":"tts-1","voice":"NAME","input":"And the second request.","response_format":"wav"}'
|
||||
file /tmp/a.wav /tmp/b.wav # really audio
|
||||
```
|
||||
|
||||
The warmer's journal lines carry `voice NAME cold=…ms warm=…ms` — quote that pair.
|
||||
Note that streaming and buffered output differ at the same seed (92.2 % of samples,
|
||||
-41.6 dB RMS): never compare two paths or two "identical" renders, only that each
|
||||
is audio and that the second request is fast.
|
||||
|
||||
## Failure modes worth naming
|
||||
|
||||
- `400 unknown voice …; available voices: …` → the name is not a pack: wrong
|
||||
spelling/case, missing `reference.wav`, or `/home/dank/voices` root-owned/empty.
|
||||
Trust the 400's list over any manifest.
|
||||
- 400 with `(none: no voice pack with a reference.wav in /voices)` → mount problem,
|
||||
not a voice problem.
|
||||
- First request slow **after** a boot → the warm pass failed; read the unit's
|
||||
journal (the warmer exits 0 and logs when there is no voice directory, so
|
||||
silence there is not success either).
|
||||
- Endpoint answers but synthesis fails 502 → the façade must dial
|
||||
`http://frank-tts-engine:8880` over the shared bridge
|
||||
(`frank-tts-engine.network`); a `host.containers.internal` / `127.0.0.1:8880`
|
||||
upstream can never work between containers, because netavark DNATs published
|
||||
ports only.
|
||||
|
||||
## Never
|
||||
|
||||
Never write under `/voices` from the endpoint (mounted `:ro` by design), never
|
||||
`git add` a voice pack, never `rm` a pack (repo policy: move to `.AGENTTRASH/`),
|
||||
and never benchmark against this endpoint while it serves — stop and mask the
|
||||
units first.
|
||||
@@ -1,14 +1,15 @@
|
||||
---
|
||||
name: niri-config-edit-validate
|
||||
description: "Locate, edit, validate, and commit niri compositor config on Frank's workstations: desk/display profiles, EDID-keyed outputs, layout, keybind grepping, dotfiles mirror, forced hot-reload."
|
||||
description: "Locate, edit, validate, and commit niri compositor config on Frank's workstations: desk/display profiles, EDID-keyed outputs, layout, keybind grepping, dotfiles mirror, noctalia.kdl last-include color override, forced hot-reload."
|
||||
---
|
||||
|
||||
# Editing niri config on Dank workstations
|
||||
|
||||
## Layout of the config
|
||||
- Live config: `~/.config/niri/config.kdl` is a thin file of `include "./cfg/*.kdl"` lines. **Never conclude "not bound" from grepping only `config.kdl`** — keybinds live in `cfg/keybinds.kdl`, rules in `cfg/rules.kdl`, layout in `cfg/layout.kdl`, etc. Grep `~/.config/niri/cfg/` recursively.
|
||||
- **Include order wins:** `config.kdl` ends with `include "noctalia.kdl"` — a hand-written accent overlay at `~/.config/niri/noctalia.kdl` that re-declares `layout { focus-ring/border/tab-indicator/insert-hint }` + `recent-windows.highlight` colors and BEATS every `cfg/layout.kdl` color edit (observed 2026-10-18: focus-ring color edits to layout.kdl appeared to "not reload" while this file silently repainted them red). Before changing any accent/outline color, grep the WHOLE `~/.config/niri/` tree (not just `cfg/`) for the property and fix the LAST includer. Tracked in repo at `dotfiles/zz-blankpad/niri/.config/niri/noctalia.kdl`; live file is NOT a symlink — sync by `cp` and `diff` like the rest.
|
||||
- niri notation: `Mod` = Super/Win key. Chords like `Mod+R { switch-preset-column-width; }`.
|
||||
- `~/.config/niri` is a REAL directory, not a symlink; the version-controlled copies are a partial mirror under `~/.omp/dotfiles/zz-blankpad/niri/.config/niri/`. Trees can drift: diff them before/after edits. (Hardlink wiring is FRAGILE — editor/agent atomic-rename writes silently break hardlinks; verify live↔mirror by CONTENT diff, never by link trust.)
|
||||
- `~/.config/niri` is a REAL directory, not a symlink; the version-controlled copies are a partial mirror under `~/.omp/dotfiles/zz-blankpad/niri/.config/niri/`. Trees can drift: diff them before/after edits. (Hardlink wiring is FRAGILE — editor/agent atomic-rename writes silently break hardlinks; verify live↔mirror by CONTENT diff, never by link trust.) On blankpad the `cfg/*.kdl` entries are symlinks into the repo (`dotfiles/niri/...` base + `zz-blankpad` overrides) — edit through them freely; `noctalia.kdl` and `config.kdl` are the plain-file exceptions.
|
||||
|
||||
## Desk/display profiles (monitor arrangement)
|
||||
- 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.
|
||||
@@ -21,21 +22,22 @@ description: "Locate, edit, validate, and commit niri compositor config on Frank
|
||||
- Preset list order = cycle order for `Mod+R` (`switch-preset-column-width`); `Mod+Shift+R` cycles backwards. WITH custom preset lists the cycle contains ONLY the configured presets — no implicit full/maximize tail (verified: [0.25, 0.5] cycles period 2). Maximize = separate `Mod+F` `maximize-column`. Built-in defaults without the node: 1/3, 1/2, 2/3.
|
||||
- WITHOUT `preset-window-heights`, `switch-preset-window-height` FALLS BACK to the width presets — user sees width-fractions as heights; define the node explicitly to stop it.
|
||||
- Ground truth for node placement: the binary's embedded template — `grep -a -B4 -A8 "preset-column-widths {" $(which niri)`.
|
||||
- Window outlines: base geometry (`width`, `off`) lives in `cfg/layout.kdl` `focus-ring`/`border`; COLORS effectively live in `noctalia.kdl` (see include-order law). `border { off }` + focus-ring active = the visible outline.
|
||||
|
||||
## Workflow
|
||||
1. Edit the live file under `~/.config/niri/cfg/`.
|
||||
1. Edit the live file under `~/.config/niri/cfg/` (symlinked = repo file; except `noctalia.kdl`, edit live then `cp` to mirror).
|
||||
2. `niri validate` (exit 0 + "config is valid") — mandatory before done.
|
||||
3. **Hot-reload is NOT reliable after harness edits** (atomic-rename writes are missed by niri's watcher; user then tests against the OLD config and reports "no change"). ALWAYS follow with `niri msg action load-config-file` (NOT `reload-config`).
|
||||
4. Sync changed file into the mirror: `cp ~/.config/niri/cfg/<f>.kdl ~/.omp/dotfiles/zz-blankpad/niri/.config/niri/cfg/<f>.kdl` (repo root is `~/.omp`, branch `main`, remote = dankclaw Gitea `dank/command-center`; push there is pre-authorized). Verify by `diff`, not by cp success.
|
||||
5. Untracked mirror files: `config.kdl`, `noctalia.kdl`, `animation.kdl`, `display.kdl`, `input.kdl` — flag drift, don't silently add.
|
||||
5. Untracked mirror files: `config.kdl`, `noctalia.kdl`, `animation.kdl`, `input.kdl`, `display.kdl` (symlink target) — flag drift, don't silently add; when a live-only file turns out to be load-bearing (like `noctalia.kdl` was), commit the first durable copy into `dotfiles/zz-blankpad/niri/.config/niri/`.
|
||||
6. Mirror files often carry pre-existing uncommitted drift (desk.sh captures); a commit of your edit folds it in — say so in the report/commit body.
|
||||
|
||||
## Live-behavior verification without disturbing Frank's windows
|
||||
- `niri msg --json <subcmd>` — the `--json` flag goes BEFORE the subcommand.
|
||||
- `niri msg --json <subcmd>` — the `--json` flag goes BEFORE the subcmd.
|
||||
- Targeting differs per action: WINDOW-scoped actions take `--id <window-id>` (NOT `--window`) — e.g. `switch-preset-window-height --id N`. COLUMN-scoped actions (`switch-preset-column-width`, `set-column-width`, `maximize-column`) have NO id — they hit the FOCUSED column; only `set-column-width <px|±N%>` positional CHANGE, no flags.
|
||||
- Spawned test apps do NOT reliably take focus, so blind `spawn + action` cycles resize whatever Frank is looking at. For column actions you must `focus-window --id N` first — and that STEALS focus; if Frank is actively testing chords, your probes and his keystrokes interleave and corrupt both. STOP probing, deliver config + validate, and have him press the chord himself.
|
||||
- `niri msg --json focused-window` can report a different window than assumed (focus moves); for per-window measurement read `niri msg --json windows` and filter by `id`. Present height of proportion p with gaps g over work-area H renders ≈ p*(H+g) − g.
|
||||
- If Frank reports "still seeing old values" after a config edit: run `load-config-file` first, then verify empirically before believing anything else.
|
||||
- `niri msg --json focused-window` can report a different window than assumed (focus moves); for per-window measurement read `niri msg --json windows` and filter by `id`. Present height of proportion p with gaps g over work-area H renders ≈ p*(H+g) − g. NOTE (measured 2026-10-18): this build's `windows`/`focused-window` JSON returns `x/y/width/height = null` — pixel-sampling the outline color from screenshots is not viable; ask Frank to eyeball after `load-config-file` instead of burning turns on capture forensics.
|
||||
- If Frank reports "still seeing old values" after a config edit: run `load-config-file` first, then verify empirically before believing anything else — and check `noctalia.kdl` include-order override BEFORE suspecting the reload.
|
||||
|
||||
## "What does chord X do?" triage
|
||||
For "mod+<key>" questions, check in order: `~/.config/niri/cfg/keybinds.kdl` (compositor wins first), terminal configs (`~/.config/ghostty`, `foot`, `kitty`), tmux, then omp TUI chords (`omp://keybindings.md`; modifiers are ctrl/shift/alt/super only, no `Mod`; `Ctrl+R`=history search, `Alt+R`=app.retry).
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: noctalia-bar-layout-edit
|
||||
description: "Edit/verify Noctalia v5 bar layout and clock widgets (multi-timezone instances, capsule groups); GUI→repo propagation chain and settings.toml symlink audit on Frank's dotfiles workstations"
|
||||
description: "Edit/verify Noctalia v5 bar layout and clock widgets (multi-timezone instances, capsule groups); GUI→repo propagation chain, settings.toml symlink audit, and clobber-recovery via dangling stashes on Frank's dotfiles workstations"
|
||||
---
|
||||
|
||||
# Noctalia v5 bar layout edit
|
||||
@@ -17,9 +17,14 @@ Bar widget layout on Frank's noctalia v5 workstations is GUI-override state, not
|
||||
## GUI→repo propagation (checkout-is-deployment)
|
||||
- `~/.config/noctalia/config.toml` is symlinked into the repo → GUI settings writes land in the repo working tree automatically.
|
||||
- `~/.local/state/noctalia/settings.toml` can SILENTLY become a plain file (observed 2026-10-17; hardlink/symlink wiring decays) → GUI bar-layout changes then diverge from the repo with no signal. AUDIT: `readlink ~/.local/state/noctalia/settings.toml` must print the repo path; if it's a plain file and repo copy is content-identical, re-arm: `ln -sfn <repo-path> <live-path>` + `noctalia config validate`.
|
||||
- Landing in the working tree ≠ propagated: other boxes only see it after commit + push to Gitea main. After GUI changes, check `git -C ~/.omp status` and commit (Noctalia's runtime churn — wallpaper path, nightlight, panel positions — rides along; normal).
|
||||
- Landing in the working tree ≠ propagated: other boxes only see it after commit + push to Gitea main. `live-sync.path` (PR #15; DirectoryNotEmpty on `dotfiles/`) commits dotfiles-tier writes within ~2 s once deployed — until it is active on a box, the hourly heartbeat leaves up to 59 min of exposure, and on 2026-10-18 exactly that window let a trunk-sync stash destroy months of GUI layout work. Until verified active (`systemctl --user status live-sync.path`), commit GUI layout edits the same session yourself. Noctalia's runtime churn (wallpaper path, nightlight, panel positions) rides along; normal.
|
||||
- niri has NO settings GUI; niri "GUI changes" don't exist, and `niri msg output ...` runtime overrides are not persisted. niri cfg mirror↔live links also decay (all inodes decoupled once) → audit by CONTENT: `diff` every `~/.config/niri/cfg/*.kdl` against its `dotfiles/zz-blankpad/niri/...` mirror, never trust links.
|
||||
|
||||
## If the bar got wiped (recovery)
|
||||
- The working-tree file is often the ONLY copy of GUI work; a `git checkout`/stash/merge clobber is survivable because git objects outlive stash entries: `git fsck --lost-found`, inspect `git log -1 --format='%ci %s' <dangling>` (newest `WIP on` stash commits hold the last good settings.toml), dump with `git show <c>:dotfiles/noctalia/.local/state/noctalia/settings.toml`.
|
||||
- Before restoring: back up the clobbered file to `.AGENTTRASH/`; validate the candidate in a sandbox (`XDG_CONFIG_HOME=… XDG_STATE_HOME=… noctalia config validate` — plugin-widget "unrecognized type" warnings there are offline-CLI blindness, not blockers); restore via `cp` onto the repo path (cp writes through the symlink; `mv` would replace it); prove visually with `noctalia msg screenshot-fullscreen`; commit immediately.
|
||||
- Cross-check the recovered layout against Frank's old screenshots in `~/Pictures/screenshot_*.png` (crop top ~7% with magick) — they are ground truth for what the bar should look like.
|
||||
|
||||
## Apply/verify loop (no restart, no reload IPC)
|
||||
1. Edit the repo `settings.toml`.
|
||||
2. `noctalia config validate` (checks merged layers; also `noctalia config export [full] | yq -p toml` to inspect effective values).
|
||||
@@ -41,5 +46,5 @@ Bar widget layout on Frank's noctalia v5 workstations is GUI-override state, not
|
||||
|
||||
## Law
|
||||
- Full widget/setting tables: https://docs.noctalia.dev/noctalia/bar/ and .../bar/widgets/<name>/ . Commit bar changes atomically to the command-center repo (checkout is the deployment).
|
||||
- **Ask before applying.** Frank asks "how do I X" questions about the live bar; answer with the exact snippet + file, apply ONLY on explicit go. A settings.toml edit is a live-deployment change, not a demo.
|
||||
- **Ask before applying.** Frank asks "how do I X" questions about the live bar; answer with the exact snippet + file, apply ONLY on explicit go. A settings.toml edit is a live-deployment change, not a demo. (Exception: restoring Frank's own layout after data loss, on his ask.)
|
||||
- Git `M` on settings.toml usually carries noctalia's own runtime churn (wallpaper path, nightlight, panel positions) alongside your edit — mention it when committing.
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
---
|
||||
name: omp-models-yml-runner-api-placement
|
||||
description: Add or edit a non-chat (tts/image/embedding) model row in ~/.omp/agent/models.yml without disabling every custom provider
|
||||
---
|
||||
|
||||
# Scope
|
||||
|
||||
`~/.omp/agent/models.yml` (or `models.yaml`) on a workstation: adding a **runner**
|
||||
model row (TTS / image / embedding / rerank / video) alongside chat providers.
|
||||
|
||||
# The landmine
|
||||
|
||||
Provider-level `api` accepts **only chat/judge transports**:
|
||||
`openai-completions`, `openai-responses`, `openai-codex-responses`,
|
||||
`azure-openai-responses`, `anthropic-messages`, `bedrock-converse-stream`,
|
||||
`google-generative-ai`, `google-gemini-cli`, `google-vertex`,
|
||||
`openrouter-decisions`, `typesafe`.
|
||||
|
||||
Naming a runner API (`openai-speech`, `openai-images`, `openai-embeddings`, …)
|
||||
at **provider** level fails whole-file validation, and the failure mode is not
|
||||
scoped to your provider:
|
||||
|
||||
```
|
||||
Warning: models.yml validation failed — custom providers disabled
|
||||
Failed to load config file models, Schema error: providers.<id>.api: must be "openai-completions", …
|
||||
```
|
||||
|
||||
Every custom provider disappears from the registry (fleet chat endpoints stop
|
||||
resolving) and `omp models` lists only built-in locals. Measured 2026-10-18.
|
||||
|
||||
# Correct shape
|
||||
|
||||
Runner api goes on the **model entry only**; `auth: none` satisfies the
|
||||
credential requirement; omit `contextWindow`/`maxTokens` (the positivity check
|
||||
fires only when they are provided — `omp models` still prints filled defaults,
|
||||
which the runner transport never reads).
|
||||
|
||||
```yaml
|
||||
"frank-tts (dankclaw)":
|
||||
baseUrl: http://192.168.0.143:7772/v1 # no `api:` here
|
||||
auth: none
|
||||
models:
|
||||
- id: frank-tts
|
||||
name: "frank-tts (dankclaw) — cloned voices"
|
||||
api: openai-speech # runner api on the model
|
||||
input: [text]
|
||||
```
|
||||
|
||||
Roles live in `config.yml` (`modelRoles`), not in `models.yml`. Kind roles
|
||||
(`image`, `web`, `speech`, `dictation`, `judge`) take no `:thinking` suffix.
|
||||
|
||||
# Verify after every edit (never assume the file loaded)
|
||||
|
||||
```bash
|
||||
omp models --kind tts --no-extensions # the new row must appear
|
||||
omp models --no-extensions | grep -E '<each fleet chat model>' # and still be listed
|
||||
omp models | grep -c 'validation failed' # must be 0
|
||||
```
|
||||
|
||||
A run that lists only built-ins means your row broke the file — revert before
|
||||
yielding.
|
||||
|
||||
# Speech transport contract (`openai-speech`)
|
||||
|
||||
Body is `{model, input, response_format, voice?, speed?}`; `voice` is forwarded
|
||||
only when explicitly set; `sample_rate`/`bit_rate` **throw**
|
||||
(`openai-speech does not support sampleRate`). Codec comes from the
|
||||
`output_path` suffix; 15 000-character cap. An unknown `voice` name is a server
|
||||
error, not a fallback — and `auth: none` makes a dead endpoint read as
|
||||
*available*, so point `modelRoles.speech` at such a provider only after it has
|
||||
answered a real synthesis.
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792359756091,
|
||||
"lastConsolidatedAt": 1792368375152,
|
||||
"lastResult": {
|
||||
"at": 1791599865775,
|
||||
"sessions": 1,
|
||||
|
||||
+1
-1
@@ -115,4 +115,4 @@ This rule exists because subagent hangs were observed in early deployments (2026
|
||||
|
||||
Before declaring a subagent deployed: verify the heartbeat endpoint returns within the timeout window, and confirm the recovery path is wired (retry/fallback/escalation logic exists in the codepath).
|
||||
|
||||
A subagent without a heartbeat or recovery path is a gap: wire the heartbeat, add the recovery path, then deploy.
|
||||
A subagent without a heartbeat or recovery path is a gap: wire the heartbeat, add the recovery path, then deploy.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792367186349,
|
||||
"lastConsolidatedAt": 1792370846394,
|
||||
"lastResult": {
|
||||
"at": 1792361576391,
|
||||
"at": 1792370515419,
|
||||
"sessions": 1,
|
||||
"deltas": 1,
|
||||
"model": "frank-zealot"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792367076712,
|
||||
"lastConsolidatedAt": 1792367676725,
|
||||
"lastResult": {
|
||||
"at": 1792366721197,
|
||||
"sessions": 1,
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
## top bar
|
||||
- Preserve top bar customizations; customizations lost or obliterated on subsequent runs
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792370776395,
|
||||
"lastResult": {
|
||||
"at": 1792370420509,
|
||||
"sessions": 1,
|
||||
"deltas": 1,
|
||||
"model": "frank-zealot"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
## window outlines
|
||||
- Window outline color is amber, and outline thickness is thickened
|
||||
@@ -254,3 +254,49 @@ providers:
|
||||
# prior assistants' <think> blocks from history. Keep them —
|
||||
# multi-turn tool traffic on this endpoint is the majority.
|
||||
preserve_thinking: true
|
||||
|
||||
# Speech endpoint — the fleet's TTS, NOT a chat provider. `api:
|
||||
# openai-speech` is a runner API (catalog kind `tts`), so this row never
|
||||
# enters the chat picker, the Ctrl+P cycle, or any chat role: it is
|
||||
# reachable only through the `speech` model role and the tts device.
|
||||
# frank-tts (host port 7772) is the OpenAI /v1/audio/speech façade that
|
||||
# resolves a voice BY NAME; its engine (frank-tts-engine, container-internal
|
||||
# 8880, never published) is arm A qwentts.cpp 5916bf7 — measured 0.55-0.67
|
||||
# RTF warm and 86-451 ms to the first audio byte on Dankclaw's CPU, with
|
||||
# mainline llama-tts (TTS_BACKEND=llama-tts-cli) kept as the rollback.
|
||||
# Transport contract, read out of the harness itself: the request body is
|
||||
# {model, input, response_format, voice?, speed?}; `voice` is forwarded ONLY
|
||||
# when explicitly set, and sampleRate/bitRate are REJECTED outright (the
|
||||
# transport throws "openai-speech does not support sampleRate"), so the
|
||||
# codec comes from the output_path suffix and the only tunable that matters
|
||||
# is the voice name — which must be a real pack name (narrator1/2/3); an
|
||||
# unknown name is an engine error, not a fallback.
|
||||
# DELIBERATELY NOT WIRED YET: config.yml `modelRoles.speech` still points at
|
||||
# local/kokoro. Flip it to "frank-tts (dankclaw)/frank-tts" only after the
|
||||
# service is deployed and has answered a real synthesis, because `auth: none`
|
||||
# makes the provider read as AVAILABLE even while the container is down — the
|
||||
# role would then fail instead of falling back to Kokoro. The same flip must
|
||||
# re-key `speech.voice` (now am_fenrir, a Kokoro voice) to a pack name.
|
||||
# SCHEMA LAW, learned the hard way (2026-10-18): provider-level `api` must be
|
||||
# a CHAT transport — the models.yml schema accepts only the chat/judge APIs
|
||||
# there, and naming a runner API (`openai-speech`) at provider level fails the
|
||||
# file's validation, which DISABLES EVERY custom provider: all three fleet
|
||||
# endpoints vanish from the registry, not just this one (`omp models` prints
|
||||
# "models.yml validation failed — custom providers disabled" and lists only
|
||||
# local/kokoro). The runner api belongs on the MODEL entry only, which is what
|
||||
# the block below does — `auth: none` covers the credential requirement, and
|
||||
# `contextWindow`/`maxTokens` stay unset because the check only fires when they
|
||||
# are provided (the bundled Kokoro row is null on both). `omp models --kind tts`
|
||||
# still prints 128K/16K for this row — the registry fills display defaults — and
|
||||
# the speech transport never reads them: its whole request body is the five
|
||||
# fields named above. Verified 2026-10-18 with `omp models --kind tts` (row
|
||||
# present) and `omp models` (frank-tts absent from the chat roster, all three
|
||||
# fleet providers still listed).
|
||||
"frank-tts (dankclaw)":
|
||||
baseUrl: http://192.168.0.143:7772/v1
|
||||
auth: none
|
||||
models:
|
||||
- id: frank-tts
|
||||
name: "frank-tts (dankclaw) — cloned voices"
|
||||
api: openai-speech
|
||||
input: [text]
|
||||
|
||||
@@ -275,13 +275,13 @@ transition = [ "fade" ]
|
||||
interval_seconds = 1200
|
||||
|
||||
[wallpaper.default]
|
||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_oxex69.jpg"
|
||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_73qev3.jpg"
|
||||
|
||||
[wallpaper.last]
|
||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_oxex69.jpg"
|
||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_73qev3.jpg"
|
||||
|
||||
[wallpaper.monitors.DP-1]
|
||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_oxex69.jpg"
|
||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_73qev3.jpg"
|
||||
|
||||
[wallpaper.monitors.DP-2]
|
||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_oxex69.jpg"
|
||||
@@ -290,7 +290,7 @@ transition = [ "fade" ]
|
||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_v9e2w3.jpg"
|
||||
|
||||
[wallpaper.monitors.eDP-1]
|
||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_oxex69.jpg"
|
||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_73qev3.jpg"
|
||||
|
||||
[weather]
|
||||
unit = "imperial"
|
||||
|
||||
Reference in New Issue
Block a user