From 57e255f930c70b6e7886ef962ef356fc8115c1d3 Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 18 Oct 2026 17:49:00 -0700 Subject: [PATCH] live: heartbeat sync 2026-10-18T17:49:00-07:00 (Refs #11) --- .../deployment-checkout-trunk-sync/SKILL.md | 70 ++++++++++ .../frank-tts-add-cloned-voice/SKILL.md | 125 ++++++++++++++++++ .../niri-config-edit-validate/SKILL.md | 16 ++- .../noctalia-bar-layout-edit/SKILL.md | 11 +- .../SKILL.md | 71 ++++++++++ .../sharpshooter/omp-32c11n2wnglul/state.json | 2 +- .../architecture.md | 2 +- .../state.json | 4 +- .../state.json | 2 +- .../architecture.md | 0 .../product.md | 2 + .../state.json | 10 ++ .../style.md | 2 + agent/models.yml | 46 +++++++ .../.local/state/noctalia/settings.toml | 8 +- 15 files changed, 352 insertions(+), 19 deletions(-) create mode 100644 agent/managed-skills/deployment-checkout-trunk-sync/SKILL.md create mode 100644 agent/managed-skills/frank-tts-add-cloned-voice/SKILL.md create mode 100644 agent/managed-skills/omp-models-yml-runner-api-placement/SKILL.md create mode 100644 agent/memories/sharpshooter/wt-20261018-170616-7c8a99d-3bf6g32a51amg/architecture.md create mode 100644 agent/memories/sharpshooter/wt-20261018-170616-7c8a99d-3bf6g32a51amg/product.md create mode 100644 agent/memories/sharpshooter/wt-20261018-170616-7c8a99d-3bf6g32a51amg/state.json create mode 100644 agent/memories/sharpshooter/wt-20261018-170616-7c8a99d-3bf6g32a51amg/style.md diff --git a/agent/managed-skills/deployment-checkout-trunk-sync/SKILL.md b/agent/managed-skills/deployment-checkout-trunk-sync/SKILL.md new file mode 100644 index 0000000..dfd2531 --- /dev/null +++ b/agent/managed-skills/deployment-checkout-trunk-sync/SKILL.md @@ -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-/`, 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' ` to date + them → `git show :` 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). diff --git a/agent/managed-skills/frank-tts-add-cloned-voice/SKILL.md b/agent/managed-skills/frank-tts-add-cloned-voice/SKILL.md new file mode 100644 index 0000000..104bea3 --- /dev/null +++ b/agent/managed-skills/frank-tts-add-cloned-voice/SKILL.md @@ -0,0 +1,125 @@ +--- +name: frank-tts-add-cloned-voice +description: "Add or repair a cloned voice on the dank fleet: /home/dank/voices/ 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//` 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/-/`. + +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. diff --git a/agent/managed-skills/niri-config-edit-validate/SKILL.md b/agent/managed-skills/niri-config-edit-validate/SKILL.md index afd844f..638a940 100644 --- a/agent/managed-skills/niri-config-edit-validate/SKILL.md +++ b/agent/managed-skills/niri-config-edit-validate/SKILL.md @@ -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--.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/.kdl ~/.omp/dotfiles/zz-blankpad/niri/.config/niri/cfg/.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 ` — the `--json` flag goes BEFORE the subcommand. +- `niri msg --json ` — the `--json` flag goes BEFORE the subcmd. - Targeting differs per action: WINDOW-scoped actions take `--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 ` 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+" 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). diff --git a/agent/managed-skills/noctalia-bar-layout-edit/SKILL.md b/agent/managed-skills/noctalia-bar-layout-edit/SKILL.md index e311ddc..2fcb333 100644 --- a/agent/managed-skills/noctalia-bar-layout-edit/SKILL.md +++ b/agent/managed-skills/noctalia-bar-layout-edit/SKILL.md @@ -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 ` + `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' ` (newest `WIP on` stash commits hold the last good settings.toml), dump with `git show :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// . 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. diff --git a/agent/managed-skills/omp-models-yml-runner-api-placement/SKILL.md b/agent/managed-skills/omp-models-yml-runner-api-placement/SKILL.md new file mode 100644 index 0000000..777e267 --- /dev/null +++ b/agent/managed-skills/omp-models-yml-runner-api-placement/SKILL.md @@ -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..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 '' # 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. diff --git a/agent/memories/sharpshooter/omp-32c11n2wnglul/state.json b/agent/memories/sharpshooter/omp-32c11n2wnglul/state.json index 2ae9a06..732a982 100644 --- a/agent/memories/sharpshooter/omp-32c11n2wnglul/state.json +++ b/agent/memories/sharpshooter/omp-32c11n2wnglul/state.json @@ -1,6 +1,6 @@ { "v": 1, - "lastConsolidatedAt": 1792359756091, + "lastConsolidatedAt": 1792368375152, "lastResult": { "at": 1791599865775, "sessions": 1, diff --git a/agent/memories/sharpshooter/wt-20261017-181502-6d1fc1f-1oq2osmoyw03k/architecture.md b/agent/memories/sharpshooter/wt-20261017-181502-6d1fc1f-1oq2osmoyw03k/architecture.md index 41685cb..a64cefa 100644 --- a/agent/memories/sharpshooter/wt-20261017-181502-6d1fc1f-1oq2osmoyw03k/architecture.md +++ b/agent/memories/sharpshooter/wt-20261017-181502-6d1fc1f-1oq2osmoyw03k/architecture.md @@ -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. \ No newline at end of file +A subagent without a heartbeat or recovery path is a gap: wire the heartbeat, add the recovery path, then deploy. diff --git a/agent/memories/sharpshooter/wt-20261017-181502-6d1fc1f-1oq2osmoyw03k/state.json b/agent/memories/sharpshooter/wt-20261017-181502-6d1fc1f-1oq2osmoyw03k/state.json index 4c8b9b2..fb012fe 100644 --- a/agent/memories/sharpshooter/wt-20261017-181502-6d1fc1f-1oq2osmoyw03k/state.json +++ b/agent/memories/sharpshooter/wt-20261017-181502-6d1fc1f-1oq2osmoyw03k/state.json @@ -1,8 +1,8 @@ { "v": 1, - "lastConsolidatedAt": 1792367186349, + "lastConsolidatedAt": 1792370846394, "lastResult": { - "at": 1792361576391, + "at": 1792370515419, "sessions": 1, "deltas": 1, "model": "frank-zealot" diff --git a/agent/memories/sharpshooter/wt-20261018-132036-7c8a99d-3uyyfeiw1304m/state.json b/agent/memories/sharpshooter/wt-20261018-132036-7c8a99d-3uyyfeiw1304m/state.json index 6bb8be4..006a301 100644 --- a/agent/memories/sharpshooter/wt-20261018-132036-7c8a99d-3uyyfeiw1304m/state.json +++ b/agent/memories/sharpshooter/wt-20261018-132036-7c8a99d-3uyyfeiw1304m/state.json @@ -1,6 +1,6 @@ { "v": 1, - "lastConsolidatedAt": 1792367076712, + "lastConsolidatedAt": 1792367676725, "lastResult": { "at": 1792366721197, "sessions": 1, diff --git a/agent/memories/sharpshooter/wt-20261018-170616-7c8a99d-3bf6g32a51amg/architecture.md b/agent/memories/sharpshooter/wt-20261018-170616-7c8a99d-3bf6g32a51amg/architecture.md new file mode 100644 index 0000000..e69de29 diff --git a/agent/memories/sharpshooter/wt-20261018-170616-7c8a99d-3bf6g32a51amg/product.md b/agent/memories/sharpshooter/wt-20261018-170616-7c8a99d-3bf6g32a51amg/product.md new file mode 100644 index 0000000..9801d4a --- /dev/null +++ b/agent/memories/sharpshooter/wt-20261018-170616-7c8a99d-3bf6g32a51amg/product.md @@ -0,0 +1,2 @@ +## top bar +- Preserve top bar customizations; customizations lost or obliterated on subsequent runs diff --git a/agent/memories/sharpshooter/wt-20261018-170616-7c8a99d-3bf6g32a51amg/state.json b/agent/memories/sharpshooter/wt-20261018-170616-7c8a99d-3bf6g32a51amg/state.json new file mode 100644 index 0000000..8725d35 --- /dev/null +++ b/agent/memories/sharpshooter/wt-20261018-170616-7c8a99d-3bf6g32a51amg/state.json @@ -0,0 +1,10 @@ +{ + "v": 1, + "lastConsolidatedAt": 1792370776395, + "lastResult": { + "at": 1792370420509, + "sessions": 1, + "deltas": 1, + "model": "frank-zealot" + } +} diff --git a/agent/memories/sharpshooter/wt-20261018-170616-7c8a99d-3bf6g32a51amg/style.md b/agent/memories/sharpshooter/wt-20261018-170616-7c8a99d-3bf6g32a51amg/style.md new file mode 100644 index 0000000..acb13b6 --- /dev/null +++ b/agent/memories/sharpshooter/wt-20261018-170616-7c8a99d-3bf6g32a51amg/style.md @@ -0,0 +1,2 @@ +## window outlines +- Window outline color is amber, and outline thickness is thickened \ No newline at end of file diff --git a/agent/models.yml b/agent/models.yml index 5b37976..c06e53d 100644 --- a/agent/models.yml +++ b/agent/models.yml @@ -254,3 +254,49 @@ providers: # prior assistants' 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] diff --git a/dotfiles/noctalia/.local/state/noctalia/settings.toml b/dotfiles/noctalia/.local/state/noctalia/settings.toml index 93e7c5c..2869baa 100644 --- a/dotfiles/noctalia/.local/state/noctalia/settings.toml +++ b/dotfiles/noctalia/.local/state/noctalia/settings.toml @@ -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"