Commit Graph
100 Commits
Author SHA1 Message Date
vh 28620ba78b chore(graphify): add Granite-labeled codebase map
graphify-out/GRAPH_REPORT.md: god-nodes + call topology, labeled via
local Granite-4.1-8b ($0). Heavy artifacts gitignored; rebuild:
'graphify update .' then 'graphify label . --backend granite'.
2026-06-10 06:28:28 -07:00
vh 6e2f80e18e docs(nh3-dev): add Mac Pro migration plan; retire vestigial volva/heid daemons
- migration-plan.md: workstation-only migration of nh3-dev's dev env to
  the M2 Ultra Mac Pro Rack (on-subnet, sidecars stay on Linux).
- nh3-dev README: drop volva from live sidecars + add retired note.
  volva.service/heid.service removed by infra-ops at heid's request
  (re-architected daemon->CC-orchestrator; volva.service was crash-looping).
2026-06-07 21:45:17 -07:00
vh 5349da567c memory: snapshot — 2026-06-05 granite-FP8 cutover + Langfuse observability + worldtree #254 deferral 2026-06-05 14:14:44 -07:00
vh 9171e6a20f feat(langfuse): stand up Langfuse v3 + wire the LiteLLM trace callback
LLM observability for the fleet — pretty trace UI over the gateway: prompts,
completions, reasoning, latency, token counts. The pretty layer LiteLLM's
spend_logs lacked.

- stacks/langfuse: v3 self-host stack (web/worker/postgres/clickhouse/redis/
  minio) on ana-docker, adapted from upstream. UI on :3001 (gitea owns :3000).
  Project + API keys auto-provisioned via LANGFUSE_INIT_*. HOSTNAME=0.0.0.0 on
  langfuse-web so it's reachable via the published port while also on tnet.
- litellm: enabled success_callback/failure_callback: ["langfuse"] (the
  passthrough env was already wired); keys + host go in the litellm .env.

Verified: stack healthy, project keys authenticate, and a real gateway call
landed a litellm-acompletion trace in Langfuse within ~6s. Secrets live only in
the server .env (never committed).
2026-06-05 11:35:01 -07:00
vh 34a43a0bc5 feat(vllm): replace phi4-mini with Granite 4.1 8B summarizer + retune GPU 1
Granite 4.1 8B beat phi4-mini on precision in brokkr's R15 P03 eval, so it's
the new production summarizer/dreamer for nevermore.

- vllm-phi4 -> vllm-granite: official IBM FP8 (ibm-granite/granite-4.1-8b-fp8,
  compressed-tensors), GPU 1, 50K ctx, FP8-KV, CUDA graphs. Same :8004 slot.
- GPU 1 retune: the embed/rerank/reward trio was over-provisioned (embed ran a
  5.89x KV pool, reward 3.90x). Trimmed utils 0.20/0.20/0.30 -> 0.07/0.07/0.18,
  freeing ~10 GB so granite runs with CUDA graphs (not --enforce-eager) and
  keeps ~10 GB free as a hedge for future Granite text-LoRAs (--enable-lora).
- LiteLLM: phi4-mini model_list entry -> granite-4.1-8b (hosted_vllm @ :8004);
  explicit entry shadows the '*' wildcard's llama-swap route.
- nevermore repointed (LLAMA_SWAP_MODEL=granite-4.1-8b via the gateway) live.

Verified end-to-end: vLLM :8004 generates, gateway routes (gateway-granite-ok),
KV 86,768 tokens/1.69x at 50K, 0 restarts, GPU 1 10.3 GB free.
2026-06-05 09:34:22 -07:00
vh 89a020a3cc fix(comfyui): native allocator + fp8 text-enc to stop FLUX.2 phantom-OOM
ComfyUI's default cudaMallocAsync allocator phantom-OOMs ('allocation
would exceed allowed memory', 0 bytes allocated) when the A6000 is
shared with the pinned TTS services (~22 GB used). --disable-cuda-malloc
switches to PyTorch's native allocator. --fp8_e4m3fn-text-enc loads the
FLUX.2 Qwen3-8B text encoder as fp8 (~8.7 GB) instead of upcasting the
fp8 file to fp16 (~16 GB), matching the box's Ampere-fp8 posture.

Applied via COMFY_CMDLINE_EXTRA in the canonical compose; verified the
allocator flipped to 'native' and both flags are on the live cmdline.
2026-06-05 01:43:28 -07:00
vh 03d73a212e feat(llama-swap): serve Granite 4.1 3B + 8B instruct for brokkr R15 P03
IBM Granite 4.1 dense instruct (3B + 8B), unsloth Q4_K_M GGUF via -hf
syntax, 64K ctx with q8 KV. Auto-exposed through the LiteLLM gateway
wildcard (ana-docker:4000) and direct on llama-swap (:9292).

Replaces the retired irv-ml1 Ollama granite4.1 — Ollama is now banned
fleet-wide; serving consolidates onto the sanctioned llama-swap/vLLM
substrate.
2026-06-05 00:31:39 -07:00
vh cf35adaf6d catalog(canonical): remove chatterbox dead levers + add csm-expressiva
The asset-engine catalog source of truth. Removes the chatterbox
exaggeration/cfg_weight sliders (proven Turbo no-ops) — reconciling the
canonical with the fix previously applied only to asset-engine's vendored
copy — and adds the csm-expressiva whisper TTS entry (irv-ml1:8198).
2026-06-04 18:02:13 -07:00
vh df340c808f docs(nh3-dev): Playwright/Chromium is box-wide, not lkraven-only
Reflects the shared install: browsers in /opt/ms-playwright (root-owned,
world-readable) + PLAYWRIGHT_BROWSERS_PATH wired globally via
/etc/environment + profile.d, so any user/project resolves the shared
binary without a per-home playwright install. Includes the add-a-browser
command for future bumps.
2026-06-04 14:09:33 -07:00
vh 4f0c5f1f66 feat(inventory): bring nh3-dev under infra-ops management
Adds the NH3 dev box (10.100.10.50) as a managed host: README, first
system-details snapshot, ssh-target. Adds it to the fleet bootstrap's
Tier 1 and the CLAUDE.md servers table. infra-ops identity bootstrapped
there (operator-run) — NOPASSWD sudo + key, so root-level infra work on
the box (it runs the egress proxy, ttyd seat, mead-hall, volva, and dev
checkouts) no longer needs a per-task password.

First use: installed Playwright headless-Chromium system deps + binary
for bloom_music's OSMD browser-geometry test harness; headless launch +
real SVG geometry verified.
2026-06-04 14:03:52 -07:00
vh c7ee8884c6 feat(litellm): front z.ai GLM cloud models through the gateway
Adds explicit gateway entries for the four z.ai GLM models (glm-5.1,
glm-5-turbo, glm-4.7, glm-4.5-air) routed to api.z.ai with Z_AI_API_KEY,
plus the compose env passthrough + .env.example doc. Explicit entries
win over the llama-swap wildcard (distinct IDs, no collision). Extends
the gateway's unified logging to cloud inference, not just local
vLLM/llama-swap.

Cost note: paid API — only gateway-keyed callers reach these, but calls
spend z.ai credits (documented in config + compose comments).
2026-06-04 11:13:32 -07:00
vh 349b3614db feat(litellm): front llama-swap through the gateway via wildcard passthrough
Adds a `model_name: "*"` entry routing any unmatched model to llama-swap
(ana-ml2:9292) so its whole swappable LLM zoo logs through the gateway
without per-model registration — add/swap models in llama-swap freely,
litellm logs them all. Exact entries (phi4-mini/qwen3-embedding/
qwen3-reranker → vLLM) still win; the wildcard only catches the rest.

litellm does no inference; llama-swap keeps loading + serving. Enables
routing worldtree-personal's generative chat through the gateway for
full req/resp logging while preserving llama-swap's on-demand swapping.
2026-06-04 11:02:35 -07:00
vh 83b2ec1a8a feat(litellm): add vLLM request/response logging gateway on ana-docker
LiteLLM proxy fronting the vLLM services on ana-ml2 so every request +
response is captured and inspectable in a browser Logs UI — the
visibility vLLM itself lacks (Dozzle shows only connection metadata).

- compose: litellm (proxy + /ui Logs) + litellm-db (Postgres store)
- conf/config.yaml: routes phi4-mini (chat, :8004), qwen3-embedding
  (:8001), qwen3-reranker (:8002); store_prompts_in_spend_logs persists
  full prompt/completion text. reward classifier (:8003) stays direct
  (no first-class LiteLLM route).
- Langfuse-ready: lean first cut intentionally skips Langfuse's heavy v3
  stack; graduating is one env-var + callback step, no re-architecture.
- roadmap: mark the vLLM-observability item's first cut as shipped.

Lean first cut of docs/roadmap.md "Observability for the vLLM stack".
2026-06-04 01:17:50 -07:00
vh 1dba05d8c8 memory: snapshot — 2026-06-04 infra session (phi4/vLLM, infra-ops identity, R15/R16, brokkr svc)
Captures: phi4-mini FP8 on ana-ml2 vLLM as nevermore summarizer (granite retired,
canonical Phi-4 template after apply→revert); infra-ops NOPASSWD-sudo identity across
PFI boxes; R15/R16 brokkr-smithy stand-up + ollama 0.30.4 + A6000 UUID-pin; brokkr user
+ substrate migration + audition service; homepage incident; observability roadmap.
Foot-guns: headgun chat-template lesson, UUID GPU-pin, system-unit reaping, pyworld/
gfortran, homepage D-state. Archived 11 settled 2026-06-01 TTS-arc entries (Recent 5 +
Tried 6) to archival-memory.md; pruned 145 lines of DONE 2026-06-03 in-flight bulk.
File back under the 300-line cap (299).
2026-06-04 01:08:50 -07:00
vh 27eb53735a revert(vllm-phi4): back to canonical/official Phi-4 chat template
Operator chose option (ii): keep the OFFICIAL Phi-4 format globally rather than
impose Ollama's leaner scaffold on every phi4 consumer. Removes the
--chat-template override + the conf/phi4-chat-template.jinja file (90e08f0).
vLLM now uses the tokenizer's built-in template (system <|end|> present);
verified 7-token render via tokenize/detokenize. brokkr re-baselines its R15
canonical on the official scaffold so baseline == production.
2026-06-04 00:39:36 -07:00
vh 90e08f0502 fix(vllm-phi4): Ollama-matching chat template to recover R15 baseline
vLLM's official Phi-4 tokenizer template emits <|end|> after the system turn;
Ollama's does not. That single boundary token regressed brokkr's R15 P02
admission eval (type macro-F1 -33pp) vs the Ollama-measured canonical, while
valid_format held at 1.0. Operator chose to make vLLM match Ollama's leaner
scaffold globally (baseline == production). Adds conf/phi4-chat-template.jinja
(drops the system <|end|>) + mounts it + --chat-template on vllm-phi4. Applied
prompt verified via tokenize/detokenize; brokkr re-smokes probe_vllm.yaml.
2026-06-04 00:29:05 -07:00
vh 40a374b809 feat(vllm): add phi4-mini FP8 summarizer/dreamer on ana-ml2; retire granite from llama-swap
phi4-mini supersedes the granite-4-small llama-swap pin as the summarizer +
dreaming agent. New vllm-phi4 service: Phi-4-mini-instruct, vLLM-native FP8
(near-lossless on RTX 6000 Ada cc 8.9), 50K ctx, FP8 KV cache, GPU 1, :8004.

llama-swap: removed granite-4-small (depinned) + granite-4-micro config —
both superseded. CONFIG ONLY; the GGUFs stay on disk. Frees granite-4-small's
~24 GB (it was pinned at 120K ctx).

Placement: phi4 on GPU 1 with the embed/rerank/reward trio (~1.2 GB margin at
50K); keeps GPU 0 clear for llama-swap heavy models. docs/roadmap.md captures
the deferred vLLM observability (Langfuse req/resp tracing + Prometheus/Grafana).

Deploy order: llama-swap config (free granite) -> vllm-phi4 -> repoint nevermore.
2026-06-03 23:21:45 -07:00
vh 5cd4678d9f fix(irv-ml1): self-heal the gcc-11/gfortran-12 -lgfortran mismatch for soundgen
Default gcc is gcc-11 (CUDA/torch R14 pin) but the only gfortran is 12,
whose libgfortran-12-dev puts the unversioned libgfortran.so only in the
gcc-12 dir. R links shared objects with gcc-11 + -lgfortran, so the whole
Fortran dependency chain (quadprog/tseries/RcppArmadillo -> soundgen) failed
'cannot find -lgfortran'. Adds an idempotent step installing libgfortran-11-dev
(guarded on 'gcc -print-file-name=libgfortran.so' not resolving). Additive;
default toolchain untouched.
2026-06-03 15:16:13 -07:00
vh 1733cedab0 feat(irv-ml1): R15 + R16 brokkr-smithy probe-harness stand-up playbooks
No-sudo phase: uv, R16 world venv + pyworld (setuptools<81 pin for
pyworld's pkg_resources import). Sudo phase (run as infra-ops): R +
soundgen system deps, and pin the Ollama service to the A6000 BY UUID
(CUDA FASTEST_FIRST ordering != nvidia-smi PCI order on this box, so an
index pin is ambiguous). R15 models share the A6000's free headroom —
yt-voice-clipper + R14 keep running, card not freed.
2026-06-03 14:49:46 -07:00
vh 8c32a0540f feat(infra-ops): commission a dedicated NOPASSWD-sudo agent identity for PFI boxes
Adds a host-agnostic elway play + fleet driver that stand up an
`infra-ops` system user (dedicated ed25519 key, NOPASSWD sudo with
log_output audit, docker group) so the infra-ops agent completes
DevOps work end-to-end instead of handing sudo steps back to the
operator. Scoped to PFI-owned Linux boxes; tiered (compute/app/
sensitive-infra) with SureFire/corviduo/esh/Synology explicitly
excluded. Validated live on irv-ml1.
2026-06-03 14:49:46 -07:00
vh d8d776c7a3 docs(corviduo-dev): Worldtree deploys (demo/pinned/personal) are CI/CD-only, not infra-ops
worldtree-dev sent infra-ops a manual deploy request for v0.30.6 to
personal:8081; operator confirmed all three corviduo-dev Worldtree
deployments go through the Worldtree team's CI/CD, never a manual
infra-ops deploy. The old 'personal — manually managed' line was wrong
and misleading. docker-as-root pattern is emergency-ops (key restore)
only.
2026-06-03 13:46:05 -07:00
vh 9090fb59d7 memory: archive 13 settled 2026-05-30/31 entries (Recent 7 + Tried 6) to archival-memory.md 2026-06-03 10:49:09 -07:00
vh 23f2b63884 memory: snapshot — 2026-06-03 infra session (NH3 egress proxy, R14 provisioning, yt-voice-clipper push-to-deploy, gitea allowlist, irv-ml1 disk) 2026-06-03 10:38:47 -07:00
vh d4f180d57f docs(runbook): yt-voice-clipper push-to-deploy (gitea webhook → irv-ml1)
Auto-deploy on push to main: gitea webhook → HMAC-validated listener on irv-ml1:9008
→ git fetch/reset + docker compose up -d --build. Documents the gitea-server
ALLOWED_HOST_LIST anti-SSRF relaxation (scoped to the WG net), the irv-ml1 components
(deploy key, git-clone deploy dir preserving the proxy override/secrets, listener +
user service), and verify/debug steps.
2026-06-03 01:25:20 -07:00
vh f27ee47fac feat(nh3-egress-proxy): durable SOCKS5 egress on nh3-dev for datacenter-IP-gated colo services
YouTube (and a growing set of services) hard-flag datacenter IPs, bot-gating
even public content regardless of cookies/PO-tokens. Origin case: yt-voice-clipper
on irv-ml1 (Irvine colo) — every yt-dlp fetch returned LOGIN_REQUIRED. Confirmed
pure IP reputation: the same public video fetches cleanly (no cookies) once routed
through nh3-dev's residential egress (70.230.226.88).

- scripts/setup-nh3-egress-proxy.sh: idempotent dante (SOCKS5) install + config.
  Internal-only ACL (10.100.0.0/16), bound to the WG interface, systemd-managed.
- docs/runbooks/nh3-egress-proxy.md: purpose, usage, security model, caveats.

Reusable fleet egress, not yt-voice-clipper-specific.
2026-06-02 22:37:08 -07:00
vh 547a557d0f refactor: extract chatterbox-fast to its own repo (vh/chatterbox-fast)
chatterbox-fast is authored software with a test suite, not a config-mirror stack —
so it moves to its own MIT-licensed, versioned, CI'd repo (gitea vh/chatterbox-fast,
v0.1.0) following the sister-repo pattern. Replace stacks/chatterbox-fast/ with a
pointer README; the moved code (scheduler/app/bench/tests/Dockerfile/compose) now
lives in the new repo. The deployed :8197 service is unaffected (still runs the
legacy devnen-based image; self-contained-image migration is an optional follow-up).
The fleet catalog entry stays in docs/asset-engine/services.yaml.
2026-06-02 10:52:04 -07:00
vh 099e1d7418 catalog: promote chatterbox-fast to ready; vibevoice->down; resolve voxtral 8197 collision
- chatterbox-fast experimental -> ready: browser audition verified end-to-end
  (operator confirmed progressive playback "excellent" 2026-06-02).
- vibevoice ready -> down: no container running on irv-ml1 (connection refused);
  catalog status was stale.
- voxtral: NOT a stale typo — its stack genuinely claimed :8197, the port now held
  by the live chatterbox-fast. voxtral is down, so moved IT to :8201 (catalog
  endpoint + source_url, stacks/voxtral/.env.example + README, host .env) rather
  than disturb the live service. No live clash existed (voxtral down) but it was a
  latent deploy-time collision I introduced by placing chatterbox-fast on 8197.

No catalog_version bump (status changes + endpoint correction, additive). Validates
against the schema.
2026-06-02 09:10:57 -07:00
vh fa56718ba0 memory: shelve fish-streaming (sub-realtime 0.80x); standing fp4+Blackwell revisit trigger 2026-06-02 08:59:17 -07:00
vh e4b786d242 catalog(chatterbox-fast): add streaming TTS service entry (streamable:true)
Land chatterbox-fast in the asset-engine catalog as an additive service, per
asset-engine-dev's shipped streaming-audition path (asset-engine v0.1.17-19):
- streamable:true -> UI routes Generate to an ephemeral progressive-<audio>
  audition (no Job/Asset); re-run on `chatterbox` to keep output.
- New service-level `streamable` bool added to services.schema.json (additive,
  default false; mirrors the Pydantic model asset-engine-dev regenerates).
- Fields: text, voice (select via /voices), temperature/top_p/top_k/
  repetition_penalty/seed, format (pcm default; UI forces wav). exaggeration/
  cfg_weight omitted — Turbo ignores them.
- status experimental until the first real browser audition verifies progressive
  playback (the one thing asset-engine-dev couldn't machine-verify).
- reproducibility + audit entries added. No catalog_version bump (additive).
Validates against the updated schema.
2026-06-02 00:58:57 -07:00
vh 875033ff00 feat(chatterbox-fast): add seed for reproducible one-shot output
TTSRequest gains `seed` (0=random); seeded once per request under the lock via
torch.manual_seed + cuda.manual_seed_all. One-shot output is then byte-reproducible
for a fixed seed+params (verified: seed=42 -> identical sha256 across runs).
Streaming stays non-reproducible by design — adaptive-chunk boundaries depend on
live-measured RTF. Needed for the asset-engine catalog reproducibility contract
(parity with the chatterbox sibling, which exposes seed).
2026-06-02 00:58:57 -07:00
vh 65a0ef67cf fix(chatterbox-fast): correct-length WAV header for one-shot responses
stream=false + format=wav emitted the streaming 0xFFFFFFFF-length header, so a
buffered consumer reading a complete wav got bogus RIFF/data sizes. One-shot knows
the full length, so emit correct sizes; streaming keeps the open-ended header
(length genuinely unknown up front). Verified remote: one-shot wav data size ==
bytes-44, python wave.open() reads 2.20s cleanly; streaming still 0xFFFFFFFF.
2026-06-01 23:52:05 -07:00
vh 02e5ea5894 memory: chatterbox-fast deployed (Phases 1-3) — :8197 A6000, fp32 5.34GB, priming reverted 2026-06-01 23:37:38 -07:00
vh e3ff4cd40d docs(chatterbox-fast): Phase 3 deployed — A6000, measured 5.34 GB fp32
Deployed on irv-ml1 beside live chatterbox (:8196): healthy on :8197, TTFB ~0.5s,
no starvation. Measured VRAM 5.34 GB (fp32) settles the placement: the 3090's
~3.8 GB free does NOT fit, A6000 (device 1) is the only viable card.
2026-06-01 23:37:16 -07:00
vh 5c8d174f8e feat(chatterbox-fast): Phase 3 scaffold — Dockerfile, compose, .env.example
Container artifacts to deploy alongside the live chatterbox (:8196) on irv-ml1.
- Dockerfile: thin overlay FROM local/chatterbox:v1 (sibling's image, has the
  chatterbox lib + torch + fastapi) + COPY scheduler.py app.py; runs uvicorn.
- compose.yaml: mirrors the sibling chatterbox stack (runtime: nvidia +
  NVIDIA_VISIBLE_DEVICES; host IP:port, no traefik-net — these GPU TTS services
  aren't traefik-fronted). Port 8197, /health healthcheck, homepage labels,
  reuses /worktank/chatterbox/{cache,reference_audio}.
- .env.example: GPU default device 1 (A6000) — turbo is fp32, 3090 free VRAM is
  tight; port reservations; perf-lever toggles.

Not yet deployed — awaiting operator go (shared GPU host, runs beside production).
2026-06-01 23:30:36 -07:00
vh a95aa75947 memory: record context-priming discard-cut abandonment (chatterbox-fast) 2026-06-01 23:27:16 -07:00
vh 090e70aed5 revert(chatterbox-fast): drop context-priming (§1.6) — discard-cut leaks context
Revert the priming feature from d707439. Live A/B caught an audible artifact: the
context-priming discard-cut left part of the throwaway prefix in the output, so a
clause ("...without a trace of sarcasm,") was spoken an extra time.

Root cause is structural: generate() returns one finished waveform with no marker
for where the prefix ends, and the model renders the same prefix with different
timing when followed by content than when generated solo — so the duration-estimate
+ energy-minimum cut is a guess and can leave a sliver (or a whole clause) of prefix
in. A reliable cut would need token-level access (the abandoned native-streaming
arc) or a per-chunk ASR/alignment pass (heavy, still imperfect, eats the latency
budget). Fails the agreed bar: "keep only if it closes the gap without a seam."

Kept from d707439: the .gitignore (build artifacts). NOT re-applied: the bundled
margin_first fix — wiring it would shrink chunk 1 (more joins = worse coherence),
against the operator's priority, and margin=0.8 there is already starvation-safe.

Coherence loss at joins stays an accepted limitation; cold streaming was judged
"really good". Phase 1 + Phase 2 parity/perf untouched. Next: Phase 3 deploy.
2026-06-01 23:26:56 -07:00
vh d707439041 feat(chatterbox-fast): context-priming at joins (§1.6, opt-in)
Prime early joins by prepending the prior sentence as backward prosodic context,
generating context+content together, then discarding the context audio. The cut
snaps to the inter-sentence pause (energy-minimum search around the context's
solo duration) with a 5ms fade-in to kill any seam click (app: _cut_at_pause /
_fade_in / Engine.generate_primed). Opt-in via request `prime` (default off).

Scheduler: priming is AFFORDABILITY-GATED so it can never starve. A primed chunk
costs ~(2·context + content)/rtf (a 2nd context-solo pass); a chunk is only primed
when buffer ≥ prime_buffer_factor (1.5) × that cost, else it falls back to a cold
generate. Consequences proven in the GPU-free sim (17 tests):
  - fires on early joins for any GPU at/above rtf_prior (3.4 = 3090; A6000 ~3.8-4.0)
  - self-skips (degrades to cold) on a slower-than-fleet GPU rather than starving
  - never primes chunk 0 (latency-critical)
Also fixed a latent Phase-1 bug: margin_first was applied at chunk 0 (budget always
0 there) so it never did anything — now applied at chunk 1 (the first transition).

Live A/B on irv-ml1 (A6000, GLaDOS): TTFB unaffected (445 vs 467ms), no starvation;
priming fired on chunk 2 (gen 1.6s for the doubled pass). On typical text exactly
ONE early join safely primes — priming chunk 2 flattens the buffer so later/larger
chunks no longer clear the safety gate. Samples: ~/chatterbox-ab/_p2_{cold,primed}.wav.
2026-06-01 23:11:21 -07:00
vh 3a92fcd943 feat(chatterbox-fast): Phase 2 parity + perf levers
- /voices endpoint lists predefined voice stems (excludes `_`-prefixed bench/A-B
  scratch wavs); shared _predefined_wavs() also feeds default-voice discovery.
- Perf levers: TF32 matmul/cudnn + flash/mem-efficient SDPA, default ON, env-gated
  (CBF_TF32 / CBF_SDPA_FLASH). Startup logs model dtype.

Measured on irv-ml1 (turbo, A6000): the model loads FLOAT32 (not the fp16 older
notes assumed). TF32+SDPA do NOT move TTFA (489->514ms, noise) — first-sentence
latency is bound by the sequential AR token decode at batch-1, not matmul
throughput. bf16 (the lever that would help) is DEFERRED: from_pretrained() has no
dtype arg and turbo's fp32 conditioning path + dtype-sensitive vocoder make a
clean cast nontrivial; not worth the quality risk at ~0.5s TTFA. torch.compile
also deferred (batch-1 regression). Findings recorded in README.

Voice management parity (predefined dir + per-request clone refs) was already in
the Phase-1 resolve path; /voices completes the surface.
2026-06-01 22:57:09 -07:00
vh 7cd39001b2 feat(chatterbox-fast): Phase 1 streaming server — adaptive-chunk scheduler
Build the streaming TTS server MVP per docs/design/chatterbox-fast-plan.md §4.

- scheduler.py: adaptive buffer-ratchet chunker (the meat) — GPU-free pure
  logic. First sentence emitted alone for low TTFA, then chunks ratchet ~3x by
  packing whole sentences to margin x buffered-audio; drives off measured RTF +
  sec/char (EMA). relieve_leader() clause-splits a too-big mid-stream sentence
  to avoid starvation (joins land on commas); a long comma-less sentence is the
  one honored-but-flagged limitation.
- test_scheduler.py: GPU-free simulation, 13 tests — asserts no-starvation
  (incl. overestimated RTF) and the ratchet.
- app.py: FastAPI model holder + POST /tts StreamingResponse (raw PCM s16le
  default, wav optional, stream/oneshot) + GET /health.
- bench.py: client — ground-truth TTFB + real 1x-consumer starvation check.

Live test on irv-ml1 (turbo, A6000, GLaDOS voice): streaming TTFB 499ms vs
oneshot 5230ms (~10x), stayed ahead of a 1x player (no starvation), ratchet
1.64->4.08->8.60->8.60s audio, measured RTF self-corrected 3.38->4.01.

Kill the superseded docs/design/chatterbox-fast.md — its §5 windowed-token
streaming was the abandoned native-frame-streaming arc; the adaptive-chunk plan
supersedes it. Repoint persistent-memory + README at the canonical plan.
2026-06-01 22:42:28 -07:00
vh 4b9bd109bf docs(chatterbox-fast): add executable plan-of-attack (durable, survives reboot vs /tmp)
Self-contained build plan for the chatterbox-fast streaming engine: the
adaptive buffer-ratchet chunking design, validated turbo API + facts, the
GPU-1 dev/test container pattern, 4 build phases, the base-fork A/B, and
watch-outs (incl. native-turbo-streaming is abandoned). Intended for a
fresh-context session to execute at full strength.
2026-06-01 22:23:17 -07:00
vh 19308ff5aa memory: snapshot — chatterbox-fast streaming build (adaptive buffer-ratchet chunking); native-turbo-streaming abandoned; fish reference_id fix + glados transcript + NFS/smithy + references-togglable; add design doc 2026-06-01 22:21:17 -07:00
vh c98a12baf4 catalog(fish-s2): opt references into togglable; catalog_version 1->2
asset-engine shipped the per-field enable-toggle (v0.1.14/.15) — the
durable fix for the "form submits untouched fields" family. A field
marked togglable:true renders with an OFF-by-default switch: while off
the control is disabled (excluded from submission) AND the server skips
injecting its default, so it is genuinely not sent until the user opts
in.

Per operator direction, opt fish-s2's `references` (inline-base64
Custom-clone) field in — it already satisfies the togglable-requires-
optional validator (optional:true, no default). The advanced clone
field now renders dormant and can never silently override the Voice
dropdown again.

This is a SCHEMA change (new CatalogField property), so:
- services.schema.json: add `togglable` (boolean, default false),
  mirroring the asset_engine Pydantic model that generates this schema.
- catalog_version 1 -> 2 (header: bump on schema changes).
- CATALOG-CONTRACT.md: consumer pin note -> catalog_version=2.

Scoped to `references` only. The chatterbox/dia2 clone fields are the
same family but NOT toggled: dia2 deliberately defaults voice_mode=clone
+ a clone ref as its stable out-of-box voice, and toggling that field
would change dia2's default-voice behavior (the earlier 404 fix).

Validated: jsonschema accepts togglable; additionalProperties:false
guard still rejects unknown props.
2026-06-01 18:05:56 -07:00
vh c5bbb90980 fix(fish-s2): reference_id was a silent no-op — populate per-voice dirs + guard the regression
reference_id=<name> resolves against the DIRECTORY references/<name>/
(audio + same-basename .lab), not a flat references/<name>.wav. Voices
were staged flat with the per-name dirs left empty, so every
reference_id resolved to nothing and Fish fell back to its default
speaker — every dropdown voice produced byte-identical audio (proven:
Abigail == Imogen == no-ref, same text+seed). This was the real "no
accent" root cause, independent of the asset-engine "undefined" select
bug.

Server fix (applied to irv-ml1): populated references/<name>/<name>.wav
+ <name>.lab for all 32 voices; re-test confirms Imogen/Eleanor/
Beatrice/Abigail/no-ref now all distinct.

Durable hardening + record correction:
- playbook: normalize-layout step (flat <name>.wav -> nested dir, cp -u
  idempotent, when-gated on count mismatch) + an A/B verify gate that
  hard-fails the deploy if two reference_ids yield identical output.
- services.yaml: correct the reference_id resolution doc (dir + .lab,
  not flat wav).
- README + persistent-memory: correct the "reference_id-by-name is THE
  working path, verified" claim — it was a no-op until this fix; the
  prior ECAPA 0.79 result came through the inline base64 path.
2026-06-01 16:42:30 -07:00
vh 3b54519d60 memory: correct Fish-cloning finding — Fish clones competently (ECAPA 0.79); 'not British' was the undefined bug, not Fish/transcript 2026-06-01 15:26:58 -07:00
vh 7f9dc2dcf1 memory: snapshot — CSM teardown, Chatterbox v2 + Fish v6 reworks, undefined-bug root cause, dia2 restart; archive 8 (2026-05-27) entries 2026-06-01 15:07:55 -07:00
vh 38d9e3b7aa catalog(fish-s2): drop Imogen_Contralto (pitch-shift variants abandoned)
Pitch-shifted deepening (rubberband, formant on/off) sounded bad at every
depth (tuba / over-gravelly), so abandoned. Removed Imogen_Contralto from the
dropdown + deleted the staged variants (fish + chatterbox). Plain unmodified
Imogen remains. version 5->6.
2026-06-01 14:40:09 -07:00
vh 43e9616dd1 catalog(fish-s2): add Imogen_Contralto to voice dropdown
Imogen pitch-shifted -3 semitones (rubberband, formants follow) -> deeper
contralto register. Staged + verified cloning; added to reference_id options
(33 voices). version 4->5.
2026-06-01 13:34:29 -07:00
vh f52558dc0d catalog(fish-s2): add 3 British-female VCTK voices (Imogen/Eleanor/Beatrice)
Staged consenting VCTK Southern-England female speakers (p225/p228/p229, CC BY
4.0) as subtle-British-accent clone voices — repurposed from the on-host kyutai
tts-voices cache. Named neutrally; NOT modeled on or representing any public
figure. Added to the reference_id dropdown (32 voices total). version 3->4.
2026-06-01 13:24:50 -07:00
vh 284ec5b4c4 catalog(fish-s2): voice dropdown via reference_id (29 staged voices)
Staged 28 single-speaker dia voices + glados into /worktank/fish-s2/references/
(internal research use). Discovered the path-form references shape 500s on this
build; reference_id (by name) is the working voice path (verified live). So:
reference_id -> select 'Voice' with the 29 staged names (default Emily, female);
references demoted to advanced inline-base64 custom clones with the path->500
caveat documented. vram 10->24 (measured ~25GB in use); version 2->3.
2026-06-01 13:15:15 -07:00
vh f3c260e010 catalog(fish-s2): expose full control surface (was text + references only)
Fish-S2 rendered as an essentially blank form — only text + a references JSON
blob — despite being the fleet's richest-control engine. Expose the real
fish-speech ServeTTSRequest levers: temperature/top_p/repetition_penalty,
latency (normal|balanced), seed, format (wav/mp3/opus), + advanced cloning
(references/reference_id) and chunking (max_new_tokens/chunk_length/normalize/
use_memory_cache). Defaults from upstream schema.py, verified live 2026-06-01
(no /openapi.json; Kui server). Sections basic/sampling/advanced.

Also corrected: seedable false->true (/tts has seed); cold_start 8s->240s
(measured compile warmup); vram 4->10GB (~9GB BF16 weights); dropped the
misleading '~150ms TTFB' for honest ~realtime throughput. version 1->2.
2026-06-01 01:41:19 -07:00
vh 93cedd6951 catalog(chatterbox): route to /tts, expose emotion levers + sane defaults
Chatterbox was producing poor output because the catalog pointed at the thin
OpenAI /v1/audio/speech endpoint, which exposes none of Resemble's emotion/
pacing knobs — and the devnen server's shipped default exaggeration is 1.3
(tuned for its theatrical demo presets), which over-acts.

Re-point to the wrapper's richer /tts and expose the real control surface
(exaggeration, cfg_weight, temperature, speed_factor, seed, voice_mode),
mirroring the sibling dia stack (same devnen author). Defaults sourced live
2026-06-01: exaggeration + cfg_weight = 0.5 (Resemble README 'works well for
most prompts'), temperature 0.8 / speed 1.0 / seed 0 (server generation_
defaults). The shipped 1.3 exaggeration is deliberately NOT adopted.

Voices: expose the 28 built-in predefined voices via /get_predefined_voices
(default Emily.wav, the server default_voice_id) + clone via /get_reference_
files — replacing the wrong 'OpenAI aliases only' claim. Corrected seedable:
false -> true (/tts has seed) and image_tag_mutable -> true (:latest). Bumped
service version 1 -> 2 (breaking field-shape change); status down -> ready
(live + healthy). catalog_version unchanged (no new field types).
2026-06-01 01:18:59 -07:00
vh c54ab13d3c catalog: remove Sesame CSM stack — open csm-1b verified not deploy-worthy
Tear down the parked CSM stack (status: down, never successfully built).
Bring-up attempts failed at the image build: upstream
phildougherty/sesame_csm_openai pins no huggingface_hub version, which now
resolves to 1.17.0 where the `huggingface-cli` the Dockerfile relies on has
been removed (replaced by `hf`). Building would require vendoring + patching
the upstream Dockerfile.

Deep-research verdict (primary + community sourced) confirmed it isn't worth
that: the acclaimed Maya/Miles demo runs a fine-tuned, larger CSM variant
Sesame never open-sourced; the open csm-1b is the un-fine-tuned 1B base
(only the smallest of 1B/3B/8B shipped, no newer checkpoint as of mid-2026).
Ships no usable voices, can't generate text, English-only, can't stream
real-time out of the box; absent from current TTS leaderboards and dominated
by Kokoro/Dia2/Fish-S2/IndexTTS for narration.

Removes: stacks/csm/, playbooks/deploy-csm.yaml, the csm catalog service +
reproducibility_audit entries. Host state (compose dir, /worktank/csm) torn
down on irv-ml1; no container/image existed.
2026-06-01 00:58:56 -07:00
vh b74da99634 memory: snapshot — Dia2 two-instance deploy, /tts rework, zonos adapter
Capture this session's durable state: Dia2 live as dia2-2b/dia2-1b (legacy
dia retired), dia2 catalog on /tts with full controls + stable-voice default,
zonos REST adapter (built, stack down), schema regen, all pushed. New
foot-guns: dia2 image-build quirks, predefined-voices-not-in-/tts, voice_mode
clone 'undefined' 404. Archived 6 older entries (4 decisions, 2 foot-guns).
2026-05-31 22:16:40 -07:00
vh 5c4784339c catalog(dia2): default to a working stable voice (fix undefined-ref 404)
Reported: dia2-1b 404 'Reference audio file not found: undefined' when
accepting defaults — voice_mode=clone with no clone_reference_filename made
the UI submit the literal 'undefined'. Fix: voice_mode now defaults to clone
AND clone_reference_filename defaults to Abigail.wav (a staged voice), so the
out-of-box request is a valid, stable voice. Reproduced the 404 and verified
the Abigail.wav path returns 200. Folded into v2 (not yet consumed downstream).
2026-05-31 15:27:56 -07:00
vh 55602b7251 catalog(dia2): expose full /tts control surface + stable predefined voices
Repoint both dia2 entries from /v1/audio/speech to the wrapper's richer /tts
endpoint (CustomTTSRequest), exposing the levers that fix the random-voice
problem: voice_mode, clone_reference_filename, cfg_scale, temperature, top_p,
cfg_filter_top_k, speed_factor, seed, split_text, chunk_size, transcript,
max_tokens. All defaults are the wrapper's Pydantic blessed values (cfg 3.0 /
temp 1.3 / top_p 0.95 / top_k 35 / speed_factor 0.94 / chunk 300). Fields
grouped (basic/sampling/advanced). dia2 -> version 2 (field-shape change).

Voice stability: Dia2 samples a random speaker per call unless anchored. The
43 curated voices baked at /app/voices aren't reachable from /tts's clone path
(reference_audio dir only), so they're staged into reference_audio; the
clone_reference_filename picker now sources /get_reference_files. voice_mode=
clone + a reference filename pins voice/gender. Verified /tts clone end-to-end
(HTTP 200, Ogg/Opus 24 kHz). README documents the staging + two-instance shape.
2026-05-31 15:18:53 -07:00
vh e97d80cb8e catalog: re-source today's engine defaults from canonical upstreams
Replace the invented sample inputs / topk with canonically-sourced values
(per CATALOG-CONTRACT source precedence + cite-your-source rule), inline-cited:
  * dia2-2b/dia2-1b input -> '[S1] Hello Dia2!'  (nari-labs/dia2 README example)
  * zonos input -> 'Zonos uses eSpeak for text to phoneme conversion!'
      (upstream gradio_interface.py blessed Textbox default)
  * csm input -> 'Hello from Sesame.'  (sesame/csm-1b model card example)
  * csm topk -> default REMOVED (the card recommends no top-k default; the
    wrapper's internal default applies when omitted).
2026-05-31 14:52:16 -07:00
vh 373a0046b1 catalog: fill sensible field defaults for today's engines
Pre-fill the bare input textareas with format-demonstrating samples
(dia2-2b/dia2-1b: [S1]/[S2] dialogue + a nonverbal; csm: conversational;
zonos: expressive multilingual) and give csm.topk a standard default (50).
Addresses asset-engine-dev's best-practice-defaults request so the picker
forms pre-fill usefully instead of empty. No version bumps — these entries
ship fresh in this batch (no prior consumer-pinned shape).
2026-05-31 14:46:12 -07:00
vh db15638d8e catalog: replace dia with dia2-1b/dia2-2b; move zonos-api 8201->8203
Replace the single dia entry (legacy Dia 1.6B, retired) with two fixed-model
Dia2 entries (dia2-2b :8200, dia2-1b :8202), status ready (both exercised),
image local/dia:v2. Matching reproducibility_audit rows. catalog_version
unchanged (add/remove services = no vocab change).

Also fix a port collision I introduced earlier: the zonos-api adapter and
csm both claimed 8201 — move zonos-api to 8203 (catalog endpoint + voices
source_url, zonos .env.example, README).

NOTE FOR CONSUMERS: removing the dia id is a breaking catalog change for
asset_engine (it vendored dia in v0.1.4) — re-vendor + drop the dia tile,
add the two dia2 tiles.
2026-05-31 14:41:11 -07:00
vh 3139e81e8a feat(dia): two-instance Dia2 deployment (dia2-1b + dia2-2b)
The devnen wrapper is single-model and ignores the OpenAI model field, so
offering both Dia2 models to asset-engine as real per-request choices means
one fixed-model instance per model. Rework the dia stack to run two services
from a dia2-capable image:
  * dia2-2b (:8200, best quality), dia2-1b (:8202, streaming) — both GPU 0
  * each pins its model via a mounted /opt/docker/conf/dia2-*/config.yaml
Retire the legacy Dia 1.6B service.

New dia2-image/Dockerfile builds local/dia:v2 = upstream devnen wrapper +
the dia2 package (copied into site-packages; its pyproject build backend
yields an empty UNKNOWN wheel under the base's old setuptools) + the three
missing runtime deps (transformers/sphn/whisper-timestamped); torch 2.12 /
numpy 2.2 in the base already satisfy Dia2. Both instances verified
end-to-end (HTTP 200, Ogg/Opus 24 kHz).
2026-05-31 14:41:11 -07:00
vh 02b26f45a6 catalog: add zonos service entry; fix dia voice picker options
zonos: new tts entry routing to the REST adapter on 8201, JSON-envelope
response with reproducibility.seed_field=seed (seedable+deterministic),
lifecycle block, section groups. Matching reproducibility_audit row.

dia: voice select had a default (S1) but no options/source_url -> empty
picker; add options [S1, S2, dialogue] (per asset-engine-dev), bump dia
to version 2. Closed select drops clone-by-filename free-text; flagged
inline for a future source_url-backed picker. catalog_version unchanged
(add-service + field-options = no vocab change).
2026-05-31 13:59:10 -07:00
vh b7b2130784 catalog-schema: add CatalogLifecycle + reproducibility.seed_field
Resolves the stale-schema gap asset-engine-dev flagged (the published
schema rejected the lifecycle field 12/14 live services already use) and
adds reproducibility.seed_field so a seedable engine can declare which
response key carries the seed used. Authoritative regen path remains
dump_schema.py against asset_engine/catalog.py; reconcile there.
2026-05-31 13:59:10 -07:00
vh 81efa8da96 feat(zonos): OpenAI-ish REST adapter for asset-engine routing
Upstream Zonos ships only Gradio + Python SDK — no REST surface — so
asset-engine (which routes a clean JSON POST to /v1/audio/speech) can't
target it directly. Add a thin FastAPI adapter (stacks/zonos/adapter/):
POST /v1/audio/speech in front of the Zonos SDK, built FROM local/zonos
to reuse torch/CUDA/SDK. Returns a JSON envelope {audio, audio_format,
seed} — the seed rides back so asset-engine regenerate/fork can pin it
(Zonos is the fleet's first genuinely seedable TTS). compose gains a
zonos-api service on 8201; .env.example gains the port + voices dir.
2026-05-31 13:59:10 -07:00
vh 71df6f7474 fix(zonos): add missing gradio launch command
Upstream Zyphra/Zonos ships no CMD in its Dockerfile (it launches the
app from its own compose), so our container ran the NVIDIA entrypoint,
printed the CUDA banner, exited 0, and restart-looped — nothing ever
bound 7860/8199. Add command: python3 gradio_interface.py to match
upstream, plus an explicit GRADIO_SHARE=False. Built + deployed to
irv-ml1; 8199 now serves HTTP 200 and the transformer model loads.
2026-05-31 13:34:33 -07:00
vh 8eac39b6ce catalog: add dia + csm service entries (status: down) + repro audits
dia (:8200) — OpenAI-compat /v1/audio/speech, seedable (not byte-exact),
Apache-2.0 weights. Clean catalog fit; flip to ready after first exercise.

csm (:8201) — OpenAI-compat, but NO seed + temperature-sampled =
non-reproducible (contract's fix-before-adding case), catalogued by
operator direction with a reproducibility caveat + gated-license warning;
belongs at experimental once running.

Fields read from each wrapper's API docs (2026-05-31), to confirm against
live OpenAPI/Pydantic at deploy. No catalog_version bump (add-service =
no bump). NOTE: services.schema.json is stale (pre-existing — 13 errors;
live catalog uses lifecycle, schema predates it); regen via dump_schema.py.
2026-05-31 11:18:46 -07:00
vh a4b8c2a9f4 stacks: add csm (Sesame Conversational Speech Model) to the TTS bench
Sesame CSM-1B via phildougherty/sesame_csm_openai — OpenAI-compat
/v1/audio/speech, context-aware conversational speech (voice-agent
layer, not a plain reader). Port 8201 on irv-ml1. Gated model:
requires CSM_HF_TOKEN (license acceptance) — placeholder in .env.example,
real token only in host .env.
2026-05-31 10:59:22 -07:00
vh 666f7f31e0 stacks: add dia + zonos to the TTS bench
dia: Nari Labs dialogue TTS (Dia 1.6B / Dia2-1B / Dia2-2B) via
devnen/Dia-TTS-Server — OpenAI-compat, fills the multi-speaker
dialogue-scene slot for skaldsong. Port 8200 on irv-ml1.

zonos: Zyphra Zonos-v0.1 (Apache-2.0, 44kHz, emotion sliders) via the
official Gradio interface. Audition surface only — no OpenAI-compat
endpoint yet (needs the FastAPI fork to become skaldsong-pluggable).
Port 8199 on irv-ml1.

Both follow the chatterbox/fish-s2 convention: local image built from a
pinned wrapper SHA via buildx git-context, .env-driven port/GPU, python
healthcheck, homepage labels.
2026-05-31 10:53:27 -07:00
vh 53157b193d playbooks: fix drop-in filename collision in esh NFS boot-ordering
override.conf already existed on esh-docker-vm (dockerd ExecStart), so
the creates: guard skipped the step and the After=remote-fs.target
directive was never written. Use a distinct filename
(10-after-remote-fs.conf) that systemd merges alongside override.conf,
and guard on content rather than file existence so a stale file can't
silently skip.
2026-05-30 14:55:31 -07:00
vh c0458d974d playbooks: fix esh-docker-vm NFS boot ordering (paperless exit-255 on reboot)
Adds _netdev,nofail to the 4 10.0.50.50 NFS lines in /etc/fstab and a
docker.service drop-in (After=remote-fs.target) so Docker waits for the
NFS mounts before starting NFS-bind containers. Root cause of paperless
Exited(255) on every reboot. Idempotent; backs up fstab; no reboot.
2026-05-30 14:45:56 -07:00
vh c3aae2e435 skaldsong: declare VibeVoice TTS env vars for issue #73 dual-engine reader 2026-05-27 20:41:41 -07:00
vh 4a4a6fc803 llama-swap: artemis-31b-v1i — add --reasoning on + deepseek format
Worldtree-dev's --reasoning-format gemma suggestion isn't supported in
the deployed llama.cpp build (accepts only none|deepseek|deepseek-legacy).
Falling back to deepseek, which also populates reasoning_content — the
field Worldtree's GemmaProvider fallback path checks. Verified via test
inference: 608 reasoning_content deltas + 199 content deltas + 0 raw
<|channel> marker leaks.
2026-05-27 00:06:54 -07:00
vh 7f918271ac llama-swap: add artemis-31b-v1i (BeaverAI Gemma 4 fine-tune, Q6_K) 2026-05-25 22:42:25 -07:00
vh 8db180885f skaldsong: drop PYTHONASYNCIODEBUG=1 — SSE-wedge diagnostic expired 2026-05-25 00:43:17 -07:00
vh 532356fcbe corviduo-dev: log Worldtree v0.25.2 config-as-bind-mount landing (#205) 2026-05-24 20:28:46 -07:00
vh 5528769211 dockge: parameterize stacks-root path + deploy on corviduo-dev
Made the host-stacks bind-mount path configurable via
DOCKGE_HOST_STACKS_ROOT (default /opt/docker, unchanged for the
existing five hosts). Override on corviduo-dev to /home/vh/docker
because that host's /opt/ is owned by deploy:deploy (Worldtree team)
and vh lacks passwordless sudo for the fleet-standard path — same
reasoning as the beszel + dozzle agent placement earlier today.

Deployed to corviduo-dev. Reachable at http://10.250.50.152:5001
(first probe 200 — Docker's port-mapping route through iptables
worked without firewall changes, unlike beszel's network_mode: host).

Scoped to PFI-managed stacks only (/home/vh/docker/compose/) — does
NOT see /opt/worldtree*/ deployments. Keeps the management boundary
clean: dockge can restart/recreate PFI's beszel+dozzle+itself but
not the Worldtree-team-owned containers.
2026-05-23 21:23:44 -07:00
vh 1cd79e8274 corviduo-dev: deploy beszel + dozzle monitoring agents
Operator-approved fleet monitoring extension. Both agents up + healthy:
- beszel-agent on host port 45876 (KEY-mode, hub at ana-docker:8090
  SSH-polls inbound; seeded with hub's ed25519 pubkey).
- dozzle-agent on host port 7007 (mTLS auto-generated; hub at
  ana-docker:8088 connects inbound).

Compose lives at /home/vh/docker/compose/{beszel,dozzle-agent}/ rather
than the fleet-standard /opt/docker/compose/{...}/ because corviduo-dev's
/opt/ is owned by deploy:deploy (Worldtree team) and vh lacks
passwordless sudo to create the fleet path. Functionally identical;
documented in servers/corviduo-dev/README.md so future infra-ops
sessions find them.

Created an empty traefik-net external docker network on corviduo-dev
as a side effect of dozzle-agent's compose (which declares it external).
Future PFI services landing here can reuse it.

Dozzle hub on ana-docker had 10.250.50.152:7007 appended to
DOZZLE_REMOTE_AGENT (a host-side change to a non-tracked .env, not
canonical). Beszel hub still needs corviduo-dev added via the UI's
"Add System" action — one-time operator step, flagged in the corviduo
README.
2026-05-23 18:13:59 -07:00
vh 92554221be skaldsong: enable PYTHONASYNCIODEBUG to surface orphaned-task warnings
Temporary diagnostic for the class of bug story 83ff386d47c6 hit
2026-05-23: POST /generation/start returned 202, then total silence
— no log, no DB state update, py-spy showed event loop idle with no
GenerationRunner frame anywhere. Strongly suggests a created_task()
result not held → GC'd → silent destroy.

PYTHONASYNCIODEBUG=1 emits "Task was destroyed but it is pending"
and "Task exception was never retrieved" warnings to stderr; that
should distinguish lost-task from cancelled-task on the next attempt.

Per skaldsong-dev's note, remove once they wire proper task-exception
capture upstream.

Diagnosis thread: althing 01KSBGKQBXA756JWW1KD4MPGXM
2026-05-23 16:04:55 -07:00
vh 307b01e8a1 snapshot: capture skaldsong env-var-name footgun (CD wiping state)
Same lesson family as the /app/web/dist mismatch — encoding
container-internal contract (paths OR env var names) in compose
needs to be verified against the Dockerfile + app, not against
design-doc shorthand. Wrong env var names silently no-op; app
falls back to Dockerfile defaults which orthogonally miss the
bind mount, and state goes to ephemeral layer until next recreate.
2026-05-20 21:57:51 -07:00
vh 52e98fa2d0 skaldsong: fix CD-wipes-state bug (env var names didn't match app)
The compose set SKALDSONG_DB_PATH + SKALDSONG_RUNS_DIR, but skaldsong's
app reads SKALDSONG_HOST_SQLITE_PATH + SKALDSONG_HOST_RUNS_ROOT (per
its Dockerfile ENV defaults). Our values were orthogonal — the app
fell back to Dockerfile defaults pointing at /app/data/... which is
NOT bind-mounted, so every --force-recreate wiped the SQLite DB +
runs/ tree along with the ephemeral container layer.

Surfaced by skaldsong-dev (althing thread 01KS4DPF6SXTBP4Q360JZVWPNT)
after the operator noticed stories vanishing on every deploy.

Confirmed on ana-docker: container had a 40KB skaldsong-ui.db sitting
in /app/data/, while /opt/docker/conf/skaldsong/db/ on the host was
empty. Rescued the live DB to the bind-mount target before recreate.

Fix: rename env vars to match what the app reads. Bind targets stay
at /app/state/{db,runs} (parent-dir mount for SQLite WAL+SHM).
2026-05-20 21:55:48 -07:00
vh 1a0059f589 snapshot: roll persistent-memory.md from 2026-05-17 → 2026-05-20
Captures the May 18-20 session arc:

- Skaldsong CD shipped on ana-docker:8300 via registry-pull pattern
  (differs from althing/asset-engine which build-on-host); three
  first-deploy footguns surfaced + canonical-patched.
- Worldtree CD disk-hygiene PR #184 (vh/Worldtree) — watermark gate,
  eager post-deploy SHA prune, set -e hardening in SSH blocks.
  Approved by worldtree-dev, pending merge.
- mead-hall Bifrost v0.3 wire-layer + data-flow end-to-end smoke
  green (task #32 closed) — resolves the "stalled mid-Worldtree"
  state in the prior snapshot.
- Volva systemd install — three-stage debug (PATH, ReadWritePaths,
  handle pinning); impersonation footgun caught via PID/journal
  cross-reference.
- Per-project user keys minted against personal Worldtree (:8081)
  for skaldsong, althing, mead-hall + skaldsong-prod. Personal
  becomes multi-consumer dev iteration instance; demo stays for
  isolation.

File now at 434 lines (over 300 soft cap); next /snapshot run will
surface archival candidates.
2026-05-20 21:51:36 -07:00
vh c6864996f0 skaldsong playbook: verify container is Up, not 'healthy' (was racing start_period)
The verify step ran 0.09s after `docker compose up -d --force-recreate`,
well before the container's 30s start_period elapsed — so 'healthy'
isn't yet in docker ps's Status string and the grep failed. False
negative; container was operationally up (verify 1/2 already confirmed
/health 200).

Greping for '^Up' instead catches the case verify 2/2 actually wants
to catch (container exited entirely) without racing the healthcheck.
2026-05-18 23:47:51 -07:00
vh 4b8dc9f7e1 skaldsong: fix first-deploy footguns (SPA path + cors_origins shape)
Two corrections surfaced by the first end-to-end deploy that didn't
land in the pre-flight align:

- SPA static assets are at /app/spa, not /app/web/dist (Dockerfile
  COPYs the SvelteKit build output flat into /app/spa, not into
  /app/spa/dist). Mismatch caused /health to 500 with
  "RuntimeError: File at path /app/web/dist/index.html does not
  exist."

- SKALDSONG_HOST_CORS_ORIGINS must be a JSON array literal in .env.
  Pydantic-settings parses complex-typed env vars via json.loads();
  bare URL string fails first-boot with SettingsError.

Container now reports Up (healthy) on ana-docker; /health 200.
2026-05-18 23:35:13 -07:00
vh 8a4276d6b5 skaldsong: align env-var contract with app reality (pre-first-deploy)
skaldsong-dev surfaced three contract corrections before the first
deploy:

- WORLDTREE_TOKEN (outbound HTTP Bearer) was missing — separate code
  path from SKALDSONG_BIFROST_JWT_KEY (inbound HS256 verify) but
  same secret value.
- WORLDTREE_BASE_URL replaces SKALDSONG_WORLDTREE_API_URL (the
  former is what the app actually reads).
- SKALDSONG_HOST_WIZARD_AGENT_ID was missing entirely — must pin to
  skaldsong:wizard-v2 to inherit the existing Worldtree agent slot;
  blank would burn another slot of the 50-per-key Heimdall quota.
2026-05-18 23:10:37 -07:00
vh 55e2e836a8 skaldsong: scaffold compose stack + deploy playbook for ana-docker
Registry-pull pattern matching Worldtree: CI on vh/skaldsong builds and
pushes gitea.phasefinal.com/vh/skaldsong:<sha>, this playbook pulls +
recreates. SHA-pin only per current preference; no :latest moving-tag
advance yet (revisit once /health exercises Worldtree + Kokoro reach).

Host port 8300 (host) → 8000 (container). Persistent state under
/opt/docker/conf/skaldsong/{db,runs}.

Bifrost endpoint URL 10.250.50.70:8300 will need a paired
BIFROST_CLIENT_ALLOWED_HOSTS update on corviduo-dev Worldtree at first
deploy.
2026-05-18 22:01:29 -07:00
vh 8f50d702d1 homepage: add Notes group to Main tab (4-column row) 2026-05-17 22:46:41 -07:00
vh 9e9bdf9810 deploy-stack: add -n to ssh calls to prevent stdin slurp in loops
Without -n, ssh inherits the surrounding loop's stdin and consumes
the heredoc that feeds $changed / $deleted, silently truncating the
diff output to the first file only.
2026-05-17 22:46:41 -07:00
vh 4b986f0b23 snapshot: roll persistent-memory.md from 2026-05-08 → 2026-05-17
Captures 9 days of multi-stack work:
  - asset-engine catalog (lifecycle blocks for orchestrator,
    defaults audit, voice probe of voxtral/qwen3-tts/kyutai-tts)
  - asset-engine stack scaffold + LAN-direct deploy
  - corviduo-dev inventory (Worldtree-team dev VM)
  - Worldtree outage recovery + :latest tag drift fix
  - vor key injection
  - vllm-qwen3 → vllm rename + vllm-reward (Skywork-Reward-V2 AWQ)
  - pull-hf-repo.yaml (model+dataset+space puller)
  - selene-1-mini-8b judge model in llama-swap
  - /tend-docs first pass (delete infinity, archive design-brief,
    fix pfi-pve VM list, drop stale postgres-rotate section)
  - althing-chamber stack (chamber + forseti + agent-runner + valkey
    across Phase 1/2/3.1)
  - Worldtree admin workflow shift (infra-ops permanent admin key
    replaces docker-as-root + bootstrap admin)
  - mead-hall Bifrost consumer registration
  - skaldsong CI bootstrap (3 captured gotchas)
  - Phase 3 + Phase 3.1 streaming activation

Sections grew: Tools and conventions (+sister-repo rows, admin auth
note, docker-as-root pattern), Recent decisions (+12), Tried and
abandoned (+5). Current state / in-flight rewritten to today's
posture (Worldtree handshake gap, hygiene-debt rotation list, prior
open items still active).

294 lines total; under the 300 soft cap. No archival this run.
2026-05-17 14:25:56 -07:00
vh 5704fbb08f althing-chamber: add valkey for Phase 3.1 cross-process streaming
Phase 3.1 closes the cross-process gap the Phase 3 smoke surfaced —
streaming events (msg_start/thinking/delta/complete/curated) flow
from agent-runner → chamber via valkey pub/sub rather than the
SQLite bridge (too high-volume + ephemeral for the DB).

New service: `althing-valkey` (stock `valkey/valkey:8-alpine`).
Internal-only — no exposed port, no volume. chamber + agent-runner
reach via docker DNS at `valkey:6379` on the compose default
network. healthcheck via `valkey-cli ping` (5s interval). chamber
+ agent-runner gain `depends_on: valkey: service_healthy` so the
bridge is up before either side starts publishing or subscribing.

Forseti unchanged — never publishes Phase 3 events.

Operational properties (per forseti's deployment notes):
  - Mixed-state safe at every step. Missing valkey.url config key
    → chamber + runner stay on v3.0 / Phase 2 equivalent paths.
  - Backward path is single config-key delete + restart.
  - streaming_enabled: true (set on agent-runner 2026-05-17) is
    unaffected by this change.

README's services table + playbook header + verify section all
extended to reflect the four-service shape. Forseti's contract
at vh/althing:docs/contracts/phase3_1_valkey_bridge.contract.md
carries the wire-protocol spec.
2026-05-17 08:36:46 -07:00
vh 59899e4e0f althing-chamber: add althing-agent-runner as third compose service
Phase 2 daemon added to the althing-chamber stack per forseti's request
(vh/althing@5cd088a..ad1d025). Polls floor_grants WHERE consumed_at IS
NULL AND agents.driver='worldtree', claims via atomic UPDATE, calls
Worldtree's conversation API, posts the response back through the bus
as a broadcast.

Shape matches the existing forseti daemon:
  - Same ${ALTHING_IMAGE} (the binary is already in [project.scripts]
    as of ad1d025)
  - command: ["althing-agent-runner"]
  - Same shared SQLite bind-mount at /app/data
  - No port, no healthcheck (CLI doesn't expose one; same liveness
    story as forseti)

Safe to enable preemptively per forseti — when no driver=worldtree
handles are declared in config, the runner sleeps at
poll_interval_seconds. Multi-instance safe via the atomic claim
primitive (no flock needed).

Compose top comment, README "Services in this stack" table, playbook
header + verify steps all extended to reflect the three-service
shape. Will land on ana-docker on vh/althing's next push (compose
deployed via the elway playbook's upload step; image already carries
the binary).
2026-05-16 20:01:57 -07:00
vh 91d5417b0b althing-chamber: scaffold deploy stack on ana-docker
Two-service compose (chamber + forseti sidecar daemon) sharing a single
SQLite store via bind-mount under /opt/docker/conf/althing-chamber/data.
eventbus.bridge_from_db is the cross-process glue — forseti's commits
reach chamber's SSE subscribers via the bridge.

Pattern matches task-board's build-on-host deploy:
  - elway playbook clones vh/althing into /opt/docker/build/
  - docker build -t althing-chamber:local . (no registry)
  - playbook uploads compose + seeds .env one-time, brings both
    services up, polls /health
  - Gitea Actions workflow lives in vh/althing; reference copy here.

Internal tooling — host port 7881 (chamber's default of 7878 collides
with task-board). LAN-direct, no Traefik. Container always listens on
8000 internally.

Scaffold will fail to bring the chamber container up healthy until
galdrabok-side commits land:
  - Dockerfile at vh/althing repo root (two-stage: uv-bookworm-slim
    build → python:3.12-slim runtime, locked per open_questions §2
    of the v1 contract).
  - GET /health endpoint on the chamber app (200, no DB read).
  - ALTHING_BIND / ALTHING_PORT env-var support in
    core.cli.chamber_serve / core.chamber.cli (env > config.yaml >
    defaults precedence).

Coordinated via althing thread 01KRMAK7RD7TP6C8DF4KXV31RT.
2026-05-14 15:54:19 -07:00
vh c27761f608 servers/pfi-pve: full VM/CT enumeration from live qm list
Previously a partial list "from vzdump logs + servers/ dirs". Replaced
with the full output of `qm list` on pfi-pve (2026-05-14), cross-
referenced against `servers/`. VMs that have a server dir are
annotated with the path; ones that don't are flagged "not yet in
`servers/` inventory" so the gap is discoverable.

Resolved:
- VM 100 = pbs-ana (was "VM (TBD)" in prior version)
- VM 106 = corviduo-dev (added 2026-05-12; was missing from the list)

Surfaced (new findings, not yet inventoried):
- VM 101 — PFI-ANA-DC (Active Directory domain controller)
- VM 103 — PFI-SlaveBot (purpose unclear from name alone)
- VM 104 — PFI-Mongo (MongoDB host, separate from pfi-postgres)

The three new-to-inventory VMs deserve `servers/<name>/` directories
with READMEs, but that's a follow-up — creating new inventory entries
is out of tend-docs scope.
2026-05-14 15:38:34 -07:00
vh 1f37857acc servers/pfi-postgres: drop stale "Known weak password (rotate)" section
Section claimed gitea + paperless-ng Postgres passwords were "currently
in use" with trivial values and that rotation was pending. Per
STATUS.md: "Rotate exposed secrets — done 2026-04-23. All six rotated:
vaultwarden/gitea/paperless-ng Postgres passwords (hardcoded
compose.yaml literals moved to gitignored .env files in the process)..."

Rotation happened; literals are no longer in compose.yaml; passwords are
no longer trivial. Removing the section rather than amending — once
fixed, there's no value in carrying a "we have weak passwords"
section that lies about the current state.

Surfaced by /tend-docs audit 2026-05-14.
2026-05-14 15:34:02 -07:00
vh 4e0059f388 servers/pfi-pve: pfi-postgres PGDATA is on local VM disk (not NFS)
VM 105's annotation said "PGDATA on NFS from ana-nas" — postgres
migrated off NFS to local VM disk on 2026-04-23 per STATUS.md ("DB data
on local disk, not NFS. pfi-postgres migrated 2026-04-23"). Updated to
reflect current reality with the migration date as the rot detector.

Still-TBD note for VM 33: `pbs-ana` is listed as "VM (TBD)" — was
deployed and has its own server dir, but its VM ID isn't recorded
here. `qm list | grep pbs` on pfi-pve would resolve, but my SSH to
pfi-pve as lkraven is currently password-required so I can't pull
this myself. Flag for next pfi-pve console session.

Surfaced by /tend-docs audit 2026-05-14.
2026-05-14 15:33:45 -07:00
vh 54d1e7137e docs: archive asset-engine design-brief.md
Moved docs/asset-engine/design-brief.md → docs/archive/asset-engine/design-brief.md
with a 12-line archival header pointing at the live implementation
artifacts (vh/asset-engine source, stacks/asset-engine/ deploy,
CATALOG-CONTRACT.md, services.yaml).

The brief explicitly framed itself as a pre-implementation handoff
("Hand this to a design agent before any pixels"). Implementation
shipped 2026-05-12; the brief's role is past. Kept for the design
rationale it carries (why Asset is first-class, why v1 is synchronous,
v2/v3 seam reasoning) — future contributors benefit from finding it
when wondering "why is it this way."

Surfaced by /tend-docs audit 2026-05-14.
2026-05-14 15:33:15 -07:00
vh 391244b18b docs: delete stacks/infinity (retired, replaced by vllm)
Stack was retired and replaced by the vllm stack (originally vllm-qwen3,
renamed 2026-05-13). Its README still framed it as a current solution
while ana-ml2's README + vllm's README both documented the retirement.
stacks/vllm/README.md "Migrating off Infinity" step 3 explicitly said
"Delete stacks/infinity/ from this workspace" — actioning that now.

No backwards-compat shims (PRACTICES §4): contract of a deleted system
has no historical value the next contributor needs; the replacement
path is documented in stacks/vllm/README.md.

Surfaced by /tend-docs audit 2026-05-14.
2026-05-14 15:32:43 -07:00
vh 7e7130172e vllm: rename stack from vllm-qwen3 → vllm + add Skywork reward classifier
Two related changes shipped together. The stack rename is independent
but adding `vllm-reward` to the existing `vllm-qwen3` would have made
that name actively misleading.

**Rename:** `stacks/vllm-qwen3/ → stacks/vllm/`. Updated all in-repo
references (README.md root, servers/ana-ml2/, stacks/llama-swap/,
configs/restic/ana-ml2/, docs/runbooks/disaster-recovery.md). Two
intentional history mentions retained (servers/ana-ml2 + stacks/vllm
README).

**Add `vllm-reward` service:** serves Skywork-Reward-V2-Llama-3.1-8B-AWQ
on port 8003. The AWQ output is a locally-quantized model (not from HF),
so bind-mounts `/tank/aimodels/llm:/local-models:ro` rather than the
shared HF cache. Model config.json declares LlamaForSequenceClassification
which vLLM's pooling runner picks up automatically — produces a single
reward score per input via /classify.

**Flag note:** the user's spec listed `--task classify`, but vLLM 0.19.1
deprecated --task in favor of --runner pooling (model architecture in
config.json drives the classification head). Compose uses --runner
pooling with a comment explaining the substitution.

**GPU memory:** no rebalance needed — production had already tuned
EMBED/RERANK down from 0.40 to 0.20 each (canonical .env.example now
matches reality). Adding REWARD at 0.30 totals 0.70, leaving ~14 GB
headroom on the 48 GB Ada.

**Server-side:** brought existing vllm-qwen3 down, mv'd
/opt/docker/compose/vllm-qwen3 → /opt/docker/compose/vllm, appended
REWARD_* lines to existing .env (preserving API_KEY/HF_TOKEN), deployed
new compose via scripts/deploy-stack.sh, brought all 3 services up.

**Smoke tests:**
- /health on 8001/8002/8003 → 200
- /v1/models on 8003 → lists Skywork/Skywork-Reward-V2-Llama-3.1-8B-AWQ
  with max_model_len 16384
- /classify with a sample conversation → returns LABEL_0 with prob 0.9999
  (single-output regression-style reward score, expected shape for a
  reward model)
2026-05-13 22:00:26 -07:00
vh 662a73ee0e rename: pull-hf-model.yaml → pull-hf-repo.yaml
Playbook handles models, datasets, and spaces (via --var repo_type=...)
since 3025d49 — the "-model" suffix was misleading. Renaming to match
actual scope.

Updates the single in-repo reference (changelog comment in
stacks/llama-swap/conf/config.yaml). config.yaml was scp'd to ana-ml2;
no docker compose restart needed (comment-only).
2026-05-13 18:33:22 -07:00
vh 3025d4930a pull-hf-model: support datasets and spaces, not just models
Adds optional repo_type var (default "model"; valid: model/dataset/space)
that's forwarded to `hf download --repo-type` and threaded through the
verify step (cache-dir prefix tracks repo_type: models--, datasets--,
spaces--).

The playbook was previously model-only because hf download defaults
repo_type=model — pulling a dataset surfaced as a 404 against the wrong
HF API path. Now generic.

Smoke-tested 2026-05-13:
  scripts/elway ana-ml2 --playbook playbooks/pull-hf-model.yaml \
    --var hf_repo=Skywork/Skywork-Reward-Preference-80K-v0.2 \
    --var repo_type=dataset
  → 200 MB parquet cached at
    /tank/aimodels/huggingface/hub/datasets--Skywork--Skywork-Reward-Preference-80K-v0.2/

Filename is misleading now (playbook handles more than models); leaving
the rename for a future cleanup since pull-hf-model.yaml is referenced
from the llama-swap config.yaml's 2026-05-13 changelog entry.
2026-05-13 16:25:40 -07:00
vh 53f00b232b llama-swap: add selene-1-mini-8b (Atla judge model, imatrix Q6_K)
AtlaAI's Selene-1-Mini judge model for evaluation/scoring tasks.
Llama 3.1 8B base, mradermacher imatrix-quantized Q6_K (~6.5GB,
quality-leaning quant). Apache-2.0. Per Atla cookbook these defaults
hit 84% on RAGTruth hallucination eval.

New 'JUDGE / EVAL MODELS' section between the dense chat models and
the embedding models — separate category from chat/reasoning since
the run-params shape is different (deterministic-leaning: temp 0.01,
top-p 1.0, no repeat penalty).

q8_0 KV cache to fit 32K ctx cleanly on the 3090 with headroom.

Pre-pulled into the shared HF cache via the new
playbooks/pull-hf-model.yaml playbook (canonical replacement for
ad-hoc huggingface_hub.snapshot_download calls; see CHANGELOG).

Smoke-tested 2026-05-13: GET /v1/models lists selene-1-mini-8b,
POST /v1/chat/completions returns expected output cleanly.
2026-05-13 16:19:01 -07:00
vh f9b933808c playbooks: pull-hf-model — generic HF→ana-ml2 cache puller
Codifies the previously-manual workflow described in
stacks/llama-swap/README.md: install hf CLI via pipx (one-time),
inject hf_transfer for fast multi-connection downloads,
`hf download` into the shared HF cache at /tank/aimodels/huggingface
with optional --include filter.

Model-format-agnostic by design — same playbook handles GGUFs for
llama-swap and safetensors for vLLM (both stacks read the same cache
dir via HF_HOME=/hfcache). Does NOT edit any consumer's config.yaml;
per-model run params (ctx-size, sampler defaults, quant choice,
chat template, etc.) stay human-curated.

Usage:
  scripts/elway ana-ml2 --playbook playbooks/pull-hf-model.yaml \
    --var hf_repo=<user>/<repo> \
    [--var allow_patterns='*Q6_K*']

Idempotent: hf CLI skips already-cached blobs; re-runs are
sub-second when the snapshot is already complete.

Smoke-tested 2026-05-13 against:
  - mradermacher/Selene-1-Mini-Llama-3.1-8B-GGUF (Q6_K, ~6.5 GB)
  - Skywork/Skywork-Reward-V2-Llama-3.1-8B (full safetensors, ~16 GB)
2026-05-13 16:00:45 -07:00
vh 9523343d3a servers/corviduo-dev: add Worldtree-team dev VM to inventory
Captures the access path + emergency-ops runbook surfaced during the
2026-05-12 demo outage (Z_AI_API_KEY / runtime validator / :latest tag
drift cascade).

- servers/corviduo-dev/{README.md,ssh-target,system-details.txt}
- CLAUDE.md servers table + placement-rules note

The "docker-as-root sudo bypass" pattern (vh's docker-group membership
→ effective root for any bind-mount-able operation) is the canonical
fix path when /opt/worldtree*/.env edits are needed and the deploy
user's sudo is out of reach. The README documents the pattern with
the appropriate "docker-group ≈ sudo" security warning so future
emergency-ops sessions don't have to re-derive it.

Ownership stance matches SF client hosts: PFI hosts + provides
emergency-ops backstop, Worldtree-team owns OS config / deploys /
backup decisions. Coordinate data-affecting work with the architect.
2026-05-12 23:36:11 -07:00