Commit Graph
100 Commits
Author SHA1 Message Date
vh 4b4911e07e fix(litellm): remove the trial alias — run 7 failed the safety gate
Alias-present-with-backend-down is a different and worse state than
alias-removed: it re-arms silently the moment anything is served on
10.250.50.54:8021 again, and the next thing served there inherits a
fleet-reachable name without anyone deciding it should. Flagged by
brokkr-smithy-dev; same writer/reader-contract family as ops-lessons §11.

Commented rather than deleted so restoring is uncommenting, not reconstructing.
Verified: /v1/models returns 33 entries, trial absent, a caller gets a clean
400 'Invalid model name' instead of a connection error to a port that could
re-arm. Disposition of the artifact and the alias is the operator's.
2026-09-09 17:49:26 -07:00
vh 14db937624 run-07: FAILED the safety gate — CSAM drift detector fired on the tuned arm, both seats stopped, nothing deleted
- gx10:8098 erp-tune-v7 stopped 17:42; ana-ml2:8021 trial NVFP4 seat stopped 17:43
  (infra-ops' call — the same adapter was on the SHARED-KEY gateway ~15:30-17:43
  under the operator's pre-gate instruction; surfaced to him as reversible)
- probe NOT re-run, flagged generations NOT opened, length verdict left UNSET
- every artifact preserved; disposition is the operator's
- non-safety results recorded but explicitly NOT certified (primary flat +2,
  diversity reduced, coherence 1.0 -> 0.875, control valid at 1.0)
2026-09-09 17:44:45 -07:00
vh 0e47d2a741 docs(ops-lessons): §11 check the writer and the reader together, with the three failure directions
The empty-snapshot promote and brokkr-smithy-dev's -s-on-a-touched-sentinel are
the same defect from opposite sides. Records the direction split (false
reassurance / false refusal / false alarm) because the mitigations differ and a
post-mortem that lumps them loses the actionable half.
2026-09-09 17:00:57 -07:00
vh 9b9f0625c9 fix(refresh): refuse to promote an empty capture over a good snapshot
ssh exiting 0 is not proof the capture is usable — the inspect script can emit
nothing and both refresh scripts would mv that over a good system-details.txt and
report 'ok (0 bytes)'. Every reader tests the snapshot with -s, so the writer was
producing an artifact its own readers call invalid: a guard whose test disagrees
with its writer's contract has quietly stopped guarding. Prompted by
brokkr-smithy-dev hitting the same shape from the other side (a -s test against a
sentinel written with touch, a precondition that could never pass).

- empty capture -> refused, previous snapshot kept, host counted as failed (exit 1)
- capture under 1/4 of the previous -> promoted but flagged, since a host can
  legitimately shed services and the script should not guess
- header + CLAUDE.md contract lines corrected to say what is actually guaranteed
- verified red (empty inspect -> FAIL, snapshot intact, rc=1) then green (real host
  -> ok 6727 bytes)
2026-09-09 16:58:25 -07:00
vh 5a3db132aa docs(run-07): gate choreography as it ran, brokkr's pinned base results + floor addendum, and two probe failures (ssh pgrep self-match; pid file caught the launcher) 2026-09-09 16:55:15 -07:00
vh 6972e7ef7f feat(erp-seat): run 7 quantized to NVFP4A16 and serving as the trial seat on ana-ml2
- services/erp-seat-quant/run_quant_erp_v7.sh: v6 runner retargeted; dry-run gate
  passed identically (11,725 targets, 11,520 experts = 30x128x3, routers+vision BF16)
- 49 GiB bf16 relayed gx10 -> ana-ml2 (no key path either way; nh3-dev relays),
  checksums verified against source; quant 49 -> 16 GiB, all post-steps clean
- stacks/erp-seat: .env-driven swap to erp-tune-v7-nvfp4a16, served under its TRUE
  name; homepage labels + README updated, v6 rollback path recorded
- stacks/litellm: trial -> erp-tune-v7-nvfp4a16 (config-file alias; /model/update
  refuses a config model, so this is an edit + restart)
2026-09-09 15:30:15 -07:00
vh c335c38c19 fix(booth): a partial ask answer is recorded, not refused
Operator: the form failed when a question was left blank. Refusing the whole
submission over one blank threw away the picks that were made, and the HTML
`required` on the radios blocked it in the browser before the server saw it.

- answered questions recorded; blank ones land in `unanswered`; `complete` says
  whether the set is finished; a blank question carrying a note keeps the note
- `required` dropped from both templates so the browser cannot block a partial
- refused only when there is no pick anywhere AND no notes (a 400 — that would
  flip an open ask to answered with no decision recorded); a choice outside the
  option list is still an error
- new ◐ partial state with an n/N count; skipped questions render as skipped
- README + global CLAUDE.md tell reading sessions to check `complete`
- 154 tests; v0.1.15
2026-09-09 15:05:18 -07:00
vh 784c555dbd docs(booth): record WHEN inline ask placement earns its markup — the artifact-in-working-memory test, not just the placement rule (framing from tts-dev) 2026-09-09 14:23:19 -07:00
vh 6e0b85ba27 docs: refresh what today's work made stale — booth asks (inline placement promoted to its own section), ana-ml2 nvme7 settled by the scrub result, nh3-dev booth entry + the CLI-on-PATH fix, run-07 runbook outcome + serving state 2026-09-09 14:18:34 -07:00
vh 78c3a7c170 feat(booth): asks render INLINE in a verbatim report, placed by the author
Operator verdict on the separate /asks page: the question belongs with the
artifact it is about. A four-voice audition wants each voice's radio group
under that voice's audio, and one submit for the lot.

- booth/inline.py: data-booth-ask="stem" | "stem:key" | data-booth-ask-submit,
  plus <!-- booth:ask ... --> comments; unknown stem left alone, not blanked
- _ask_inline.html: self-contained fragments (own scoped styles, no JS), per-question
  groups bound to one form via the HTML5 form= attribute so a scattered
  multi-question ask still POSTs once
- unplaced questions and a missing submit block are appended, so a partially
  marked-up page can never produce an unsubmittable 400
- chip becomes a jump link to the first open ask; /asks page kept as a fallback
- 6 tests (one caught the partial-placement drop); v0.1.14
2026-09-09 14:16:03 -07:00
vh 8961ca078b memory: ERP run 7 complete (542/542, adapter 13:23 PT) — erp-seat-base-ara serving for brokkr's floors, erp-tune-v7 merged and staged; Booth asks primitive + its two same-day defect fixes 2026-09-09 13:59:31 -07:00
vh a40f979b7a ops(nh3-dev): install sox + libsox-fmt-all for yt-voice-clipper-dev's denoiser bake-off (noisered) 2026-09-09 11:13:54 -07:00
vh a56743ade3 fix(booth): asks were invisible in a booth serving its own index.html
A custom index.html is returned verbatim, so booth.html's asks panel never
rendered there — a valid ask (emmie-anchor/anchor.ask.json) was listed by the
CLI and shown nowhere, with nothing to say so.

- panel extracted to _asks.html; new GET /b/<name>/asks standalone page
- verbatim pages get an amber '? N open asks' chip beside the back chip
- POST /answer honours back=asks so answering returns to that page
- single-question asks now keep an optional 'title' (was silently dropped)
- README + routes table; 8 regression tests; v0.1.12
2026-09-09 10:39:18 -07:00
vh 2a186e4762 fix(booth): CLI resolves its source through the ~/.local/bin symlink (readlink -f), so ask/asks/answer/unlink work from any cwd 2026-09-09 09:21:34 -07:00
vh c85a700141 feat(booth): multi-question asks — a questions list renders one form with a radio group per question and lands as one answer sidecar keyed by question
- asks.py: single {prompt, options} and multi {title, questions:[{key, prompt, options, notes?}]} both normalise to questions[]; per-question notes; every question required on submit
- /answer reads choice.<key> / notes.<key> / notes for multi; single shape unchanged
- booth asks prints per-question picks; README + CLI header; install step symlinks the CLI to ~/.local/bin; v0.1.10; 135 tests
2026-09-09 09:20:35 -07:00
vh 3fe01225a9 feat(booth): asks — a multiple-choice question a session poses in a booth, answered by the operator as a radio form + notes, written back as an answer sidecar
- booth/asks.py (stdlib): <stem>.ask.json question / <stem>.answer.json answer; normalise+validate, atomic write, list with answer folded in, broken asks surfaced not hidden
- POST /b/<name>/answer: validates choice against the ask (400), unknown stem 404, re-answer overwrites
- booth.html asks panel above the gallery; amber open / green answered; JS-off form POST; index card + booth header badge for open asks
- CLI: booth ask / asks / answer [--wait [SECS]]; remote sessions poll <stem>.answer.json over HTTP
- ask/answer files excluded from gallery items and item counts; 23 tests; v0.1.9
2026-09-09 07:24:16 -07:00
vh 5de5583762 ops(ana-ml2): pool-health actions landed — tank scrubbed clean + cleared, nvme-cli SMART inventory, zroot 91→73%; root cause of missed scrubs = nvme7 absent 04-23→09-05 (pool DEGRADED, ZED mail unrouted)
- playbooks/ana-ml2-pool-health.yaml: rerunnable elway play (scrub-if-idle, nvme-cli, dangling-image + builder prune; never prune -a)
- servers/ana-ml2/README.md: 8-drive PM1725b inventory with SMART counters, nvme7 absence + alerting-gap note
- persistent-memory: deferred entry closed, outcome + follow-ups (pool-health alerting, nvme7/slot 0-5 watch, boot import race)
2026-09-09 02:03:11 -07:00
vh 5ad948bf31 memory: snapshot — run 7 training on gx10; run 6 TRANSFERRED after CSAM adjudication; erp-tune-v6-nvfp4a16 live as trial; ESH/YTVC/webhook repairs; ana-ml2 routes persisted; tank/zroot actions deferred to next session. Index 830→271 lines: 27 decisions + 8 abandoned archived, superseded in-flight blocks archived verbatim 2026-09-09 00:26:44 -07:00
vh 3e18a044bd memory: ana-ml2 tank cksum errors + no scrub since April + zroot 91%; ESH 10G topology and link health measured (fiber in, links clean) 2026-09-09 00:08:02 -07:00
vh f923d6a96e feat(ana-ml2): persist mesh return routes via ana-scale as an ifupdown if-up.d hook (playbooks/ana-ml2-mesh-routes.yaml, elway-applied, verified) 2026-09-08 23:49:38 -07:00
vh d8f625e1f2 memory: run 7 training on pfi-gx10; erp-seat on vLLM nightly, forced tool_choice limit recorded 2026-09-08 23:31:22 -07:00
vh a34a72fc09 feat(erp-tune): run 7 launched on pfi-gx10 (opening-split slot + mask union) — config, launcher, builder, runbook; erp-seat: pin vLLM nightly 311b3513 (forced tool_choice 1/9 -> 6/9, round trip clean), README on the gemma4-parser design limit 2026-09-08 23:31:09 -07:00
vh 3c54f59878 memory: GX10 released and cleared after brokkr's reference arm; run-7 recipe exists without a grant 2026-09-08 23:01:46 -07:00
vh 7f6be8a56a fix(erp-seat): tool_choice=none returned an empty turn — add --exclude-tools-when-tool-choice-none (vLLM kept the tools in the prompt, the model called one, parsing was off); 12-shape tool matrix green before/after 2026-09-08 23:01:26 -07:00
vh 32399d0da2 memory: run-6 CSAM soft trip adjudicated GO by the operator (no regeneration); base re-served on gx10 for the probe reference arm; cue-length probe result = run-7 lever 2026-09-08 22:45:52 -07:00
vh 78d5a8ccb0 memory: run-6 gate HALTED on a CSAM soft trip (operator adjudication pending); trial alias left on the NVFP4 build per the operator's prior ruling, rollback offered 2026-09-08 22:21:24 -07:00
vh 48aa2c6d35 feat(litellm): repoint trial alias run 5 (bf16, gx10) -> run 6 NVFP4A16 on ana-ml2 :8021 (operator: no gate)
Config-file deployment (the trial model lives in config.yaml, so /model/update refuses it):
block rewritten to run-6 reality — abliterated base, NVFP4A16 artifact, stacks/erp-seat,
no gate on this artifact by operator ruling, same-site hop. Deployed to ana-docker and the
gateway restarted; trial -> erp-tune-v6-nvfp4a16 verified end to end (3/3).
2026-09-08 22:18:48 -07:00
vh 8512dd4d31 feat(erp-seat): erp-tune-v6-nvfp4a16 quantized (data-free W4A16, ~90 s) and serving on ana-ml2 GPU1 :8021; playbook §3.16 (data-free NVFP4A16 still bakes the tokenizer cap); memory: gate state, brokkr after-window asks, ana-ml2 non-persistent mesh routes 2026-09-08 22:14:09 -07:00
vh 911ff20356 feat(erp-seat): NVFP4A16 quant pipeline for the Gemma-4 26B-A4B MoE ERP tune + ana-ml2 GPU1 serve stack
- services/erp-seat-quant/quant_nvfp4a16_gemma4_moe.py: linearize_moe first (playbook §3.15),
  asserts the expert Linear count, routers/vision/audio/norms/lm_head ignored, W4A16 for RP
  long-session fidelity, post-steps restore processor configs + template and reset the
  tokenizer truncation cap (§3.14); --dry-run proves targets before GPU time
- services/erp-seat-quant/run_quant_erp_v6.sh: detached container on GPU1 (vllm-llmcompressor)
- stacks/erp-seat: serve recipe copied from gemma4-charrp, true served name only, port 8021
2026-09-08 21:55:28 -07:00
vh 1f95728728 memory: run 6 COMPLETE (train_loss 3.259), base seat erp-seat-base-ara serving for floors, processor_config.json gap on the abliterated repo 2026-09-08 16:26:05 -07:00
vh f2b7384199 memory: YTVC v0.3.6 (CrisperWhisper) deployed + verified; gitea webhook repointed off the dead wg0 IP with HMAC secret re-applied 2026-09-08 15:59:26 -07:00
vh d2df1eb306 memory: YTVC 403 root cause = stale yt-dlp layer (no-cache rebuild), punkt_tab staged on data volume + NLTK_DATA; operator video verified 8 clips 2026-09-08 14:27:44 -07:00
vh 14dd9ab90c feat(egress): fleet SOCKS5 residential egress on nh3-scale (100.64.0.1:1080), successor to the retired nh3-dev danted; yt-voice-clipper repointed and back up
- scripts/setup-nh3-scale-socks-egress.sh: dante-server on CT107, mesh+fleet ACL only,
  After=tailscaled + Restart=on-failure; run via pct push/exec on nh3-pve
- YTVC_PROXY on irv-ml1 -> socks5h://100.64.0.1:1080 (override, untracked); worker recreated;
  end-to-end job verified with proxied/direct controls
- YTVC Homepage href/siteMonitor -> irv-ml1.nh3.internal:8000 (was the dead wg0 IP)
2026-09-08 14:10:08 -07:00
vh a19b7f62d9 memory: ESH static WAN follow-ups landed — FortiGate trusthost3 → 128.177.138.182 (verified from ESH), esh-ana IPsec rebound to wan1, UDP 41641 forward → esh-scale peers direct 2026-09-08 13:45:13 -07:00
vh 7d5de53565 memory: ESH WAN static 128.177.138.182/30 LIVE (CGNAT over), crowdsec esh allowlist updated, open follow-ups (FG trusthost3, esh-ana rebind, mesh direct path) 2026-09-08 13:29:42 -07:00
vh 304baddfc1 memory: run 6 TRAINING on pfi-gx10 (abliterated base 33c59654), trial alias dark, gate naming 2026-09-08 04:26:14 -07:00
vh 3fec668bf2 feat(erp-tune): run 6 on pfi-gx10 — jenerallee78 ARA-abliterated base (index 33c59654) pulled + byte-verified, run-5 recipe byte-held, launched under operator-2026-09-08-rnd-run6
- scripts/erp-tune-gx10/pull-verify-jenerallee78.sh + base-pin-jenerallee78-shards.txt:
  revision-pinned root-shard pull, 32/32 sha256+size vs brokkr-smithy pins, index
  set-equal to stock, STOCK tokenizer set installed over the repo's (which bakes in
  a 256-token truncation); repo originals kept as *.repo
- scripts/erp-tune-gx10/run-06-gx10.json + launch-run-06.sh: run-05 config with the
  base swapped, recipe-r6, survivors-r5 verbatim, stock template path
- docs/runbooks/gx10-run-06.md: pull/verify record, free-check result (encode
  reproduces run 5 exactly), hf download --include gotcha, gate naming
  (erp-seat-base-ara / erp-tune-v6)
2026-09-08 04:24:38 -07:00
vh 55631e28bc memory: snapshot — run-5 RESCUED + seat live, base settled STOCK, cc-channel route, Miranda relay authority; session fleet-fixes + open items 2026-09-08 03:32:23 -07:00
vh 1282698e71 memory: R47 base provenance SETTLED from bytes — it is STOCK, not heretic
Operator-directed question (via brokkr): which weights at
/home/infra-ops/models/gemma4-26b-a4b-it-bf16 — stock or the heretic abliteration?
Answer from bytes: STOCK google/gemma-4-26B-A4B-it, byte-for-byte. Three-way match:
local shard sha256 (1127684971/aab47033) == HF download etags == stock repo LFS
oids, and download commit 4d7ae498 == stock HEAD. The recipes' -heretic-bf16 label
is a naming error (run-04 provenance was right; refusal telemetry confirmed). All
of runs 3/3c/4/5 trained from stock. Plausible cause: the selected llmfan46 26B-A4B
Heretic abliteration ships GGUF-only, so the bf16 safetensors pulled was stock.
2026-09-08 03:22:33 -07:00
vh f8a7985f02 feat(litellm): repoint trial alias run 3c -> run 5 (erp-tune-v5)
Operator decision post-RESCUED verdict: point the LiteLLM trial alias at the
run-5 tuned seat (erp-tune-v5 on gx10:8098) for hand-testing prosody/feel in
Open WebUI. Rewrote the trial config block to run-5 reality, including the
now-MEASURED refusal-erosion note (k=25 both arms, de-gated + stated) replacing
the run-3c 'safety axes unmeasured' warning. Verified end-to-end: trial ->
erp-tune-v5 returns coherent output through the gateway. Container restarted to
re-read the bind-mounted config (up -d alone is a no-op for a config-only change).
2026-09-08 02:57:24 -07:00
vh 37f2caa2d3 memory: ERP run 5 COMPLETE — gate RESCUED (landmark R49.5)
First arm of the ERP-seat line where the capability gate did not fail. The
dependency-forcing slot (GovReport+QMSum, 3.46% of loss) broke the
diversity<->coherence coupling run 4 (STILL-COUPLED) and 3c (20x LR cut) could
not — structure of the loss was the lever, not its mass; INERT did not fire.
T4 8/8 (run 4: 5/8), t4_dissect noise 0.9062 vs run-3 0.5625, diversity held.
Reported-beside: T3 8->6 (new loss), RP-length partial fail, refusal erosion
(de-gated/stated), free-check base 5-6pp below run 4 (vLLM 0.28.0 unchanged).
Seat erp-tune-v5 served on gx10:8098 pending the operator's seat/ear decision.
2026-09-08 02:15:47 -07:00
vh 913d2d2a6c feat(homepage): wire ana-ml2 back into discovery via socat proxy on ana-docker
ana-ml2 is up but not reachable cross-site from ESH (return-route gap on ana-ml2,
no SSH access to fix it). New stacks/ana-ml2-proxy: a pinned alpine/socat relay on
ana-docker (host net) exposing 10.250.50.70:2354 -> ana-ml2 10.250.50.54:2375.
Homepage docker.yaml ana-ml2-docker re-enabled pointed at the proxy. Verified:
esh-docker-vm reaches ana-ml2's docker (29.3.1) through it; /api/services returns
in 0s with 111 services; ana-ml2's inference seats (gen, coder, embed, rerank,
reward, image-bench, dockge) render. Reversible: point back at 10.250.50.54:2375
and compose-down the proxy once ana-ml2 routes to ESH.
2026-09-07 16:34:13 -07:00
vh e0d1c44137 chore(fleet): repoint stale irv-ml1 refs (10.100.79.3 -> irv-ml1.nh3.internal)
The 2026-09-06 headscale cutover retired irv-ml1's wg0 tunnel IP 10.100.79.3
(now 10.6.110.50). Repointed all LIVE canonical refs to the DNS NAME so the next
move can't re-break them: homepage.href/siteMonitor labels across 25 stack
composes, load-bearing env defaults (asset-engine INFERENCE_HOST, open-webui
AUDIO_TTS_OPENAI_API_BASE_URL, skaldsong SKALDSONG_TTS_BASE_URL, zonos-gateway
ZONOS_URL, dia), homepage services.yaml manual cards (Voice Design Studio,
IRV-ML1), and servers/irv-ml1/ssh-target. Updated the stale 'WG tunnel' comment
to the mesh reality.

Left as-is: README curl-examples and .env.example comments (docs), and historical
mentions in CLAUDE.md/persistent-memory. NOTE: applying the label repoints to the
RUNNING irv-ml1 containers needs a recreate per service (labels read at creation);
deployed .env values are separate from these canonical defaults.
2026-09-07 15:08:56 -07:00
vh 4506ef6fb5 feat(homepage,dns): add WhereTF card + DNS alias; fix homepage docker discovery
WhereTF (operator-directed, brokkr-smithy-dev; nh3-dev:8093, user-systemd Bun
service): manual Homepage card under Apps (can't auto-discover) + DNS alias
wherethef.nh3.internal -> nh3-dev.

While adding the card, found Homepage's /api/services was hung (dashboard
degraded since the 2026-09-06 irv-ml1 move): its docker.yaml still pointed
irv-ml1 at the dead 10.100.79.3, and ana-ml2 (10.250.50.54:2375) is unreachable
from esh-docker-vm — either dead host hangs the whole aggregation on its TCP
connect timeout. Repointed irv-ml1 to 10.6.110.50 (verified reachable via mesh)
and disabled ana-ml2 (reversible, noted). /api/services now returns in 0s with
96 services; WhereTF renders.
2026-09-07 14:53:33 -07:00
vh 957c8f177d fix(litellm): repoint TTS aliases at irv-ml1 via DNS name + extra_hosts
ext-tts / tts-1 / tts-1-hd / gpt-4o-mini-tts (all openai/zonos) were pinned to
irv-ml1's pre-move address 10.100.79.3:8198, dead since the 2026-09-06 headscale
cutover to 10.6.110.50 — so ext-tts through the gateway hung. Reported by
tts-dev/svos-dev.

The 4 aliases are DB-backed (store_model_in_db), so their api_base was updated
via the admin API to the DNS name http://irv-ml1.nh3.internal:8198/v1 (not a
fresh IP — that just re-arms the trap on the next move). The container cannot
resolve *.nh3.internal via split-horizon DNS, so this adds an extra_hosts entry
mapping the name to 10.6.110.50. Verified: container resolves the name and a
live ext-tts/sindra call returns 200 + valid MP3.
2026-09-07 14:32:18 -07:00
vh 265357efb2 ops(erp-tune): stage + launch R47 ERP-seat SFT run 5 on pfi-gx10
Run 5 = the dependency-forcing corpus arm: airoboros-3.2 OUT of the 20% slot,
govreport/clean-v1 (496) + qmsum/clean-v1 (97) IN, at run 4's lr 2e-04 with
everything else held. kvasir byte-identical (survivors-r5 = survivors-r4 minus
airoboros plus the two new roots whole). Operator authorized the launch to
infra-ops directly; grant operator-2026-09-07-rnd-run5.

Canonical copies of the config, launcher and survivors builder; runbook
docs/runbooks/gx10-run-05.md. Launch gates all passed (7/7 root shas + shard
hashes, survivor join 8,212 = recipe, holdout disjoint, window_count==1 on all
593 slot rows, realized [mix] slot loss 3.46% vs preregistered 3.4%). 524 steps.
persistent-memory current-state updated: run 5 LAUNCHED + training.
2026-09-07 09:33:03 -07:00
vh 61001e69e5 memory: snapshot — run-5 authorized-but-stalled (brokkr), fleet-TLS pattern, /mnt/smithy remount, backup router-exclusions, irv DNS; session open follow-ups 2026-09-07 08:06:24 -07:00
vh 76fdf45925 feat(booth): pin/favorite, multi-select delete, newest-first link board
The standing link board grew from a flat oldest-first list with a per-row
× into a manageable board: newest links lead, favorites stay on top, and
several dead links can go in one pass.

- Ordering: order_for_display() renders pinned rows first, then newest-first
  within each group (the board is an append log, so newest = most recently
  posted — the row you usually came to grab).
- Pin/favorite: a per-row ★ toggles pinned state via POST /b/<name>/pin.
  State lives in a .pins sidecar dotfile (one content id per line), NOT
  inline in links.md — so links.md stays a pure atomic-append log (many
  sessions post concurrently) and a row's content id never changes just
  because it was pinned. remove_link_entry drops a removed row's pin;
  orphaned pins are inert (renderer only stars a live id).
- Multi-select delete: checkboxes feed POST /b/<name>/unlink-many (repeated
  'sel' content ids), with a select-all box and a live count. The per-row ×
  stays for single removal.
- One <form> with formaction buttons, so checkboxes, ×, ★, and bulk delete
  coexist without nested forms AND all work with JS off; JS only adds
  select-all and the live count. Per-row × confirm reads desc/url from
  data-* attrs, so an arbitrary posted description can't break into the JS.
- Every action is keyed by content id, never row position — same race-safety
  the existing × has, extended to the bulk path.
- Fixed pre-existing undefined --fg/--bg CSS refs in the board styles.

Tests: +19 (pins round-trip, ordering, orphan-inert, remove-unpins, /pin
and /unlink-many endpoints, board render + order). Full suite 102 passing.
Deployed to nh3-dev booth.service; verified live (newest-first, pin
round-trip, bulk delete) against the real 31-row board with no data loss.
2026-09-06 02:29:22 -07:00
vh 0336e033b8 fix(dns): repoint irv-ml1.nh3.internal to live Irvine LAN IP over mesh
The A record and the CLAUDE.md fleet row both still named 10.100.79.3,
the wg0 lifeline that went dormant in the headscale cutover — so the
name resolved to a dead door and the row read like an outage waiting to
be misdiagnosed.

- dns/internal.yaml: irv-ml1 10.100.79.3 -> 10.6.110.50 (Irvine's real
  LAN, 10.6.110.0/24, advertised fleet-wide as a mesh subnet route).
  Resolve to the LAN IP to match the host->LAN-IP model every other
  entry uses; mesh node IP 100.64.0.6 recorded in the note. Comment
  rewritten: Irvine is no longer numbered out of NH3's 10.100.79.0/24.
- CLAUDE.md: fleet-table row and GPU-placement rule updated to the mesh
  reality (wg0 lifeline dormant, access via headscale mesh).

Verified: all three AdGuard resolvers (ana/esh/nh3) return 10.6.110.50;
name pings live ~11ms; reachable from a mesh node and from a colo host
behind a subnet router.
2026-09-06 01:14:25 -07:00
vh ed3810aea2 memory: snapshot — irv-ml1 remote cutover complete + subnet discovery; session follow-ups 2026-09-06 01:05:14 -07:00
vh 02cf9d6cfb feat(mesh): irv-ml1 wg0 non-reboot-restorable + Irvine subnet discovery (bidirectional, verified) 2026-09-06 01:02:50 -07:00
vh b2c819ed51 docs(nh3-dev): mark SOCKS egress proxy retired (headscale exit nodes) 2026-09-06 00:58:36 -07:00
vh be4e7d68c9 feat(mesh): irv-ml1 cutover complete (remote, 5-day-safe) — 3 recovery paths, wg0 dormant, dante retired 2026-09-06 00:58:21 -07:00
vh f8eb3606eb feat(mesh): fold irv-ml1 in additively (node 100.64.0.6), wg0 lifeline preserved; wg0 retirement deferred to on-site 2026-09-06 00:43:48 -07:00
vh 13aaccf21e memory: snapshot — headscale cutover complete + NASPool rebuild; session open follow-ups 2026-09-06 00:29:14 -07:00
vh 1284c18fda docs(mesh): throughput baseline — 761/464 Mb/s vs old 250 IPsec; shared-fiber caveat 2026-09-06 00:24:41 -07:00
vh 7ce398f069 feat(mesh): selective masquerade — exit nodes AND site-to-site source preservation both work 2026-09-06 00:16:18 -07:00
vh b846a7c870 feat(mesh): advertise exit nodes at all three sites (SNAT re-enabled); usage + tradeoff documented 2026-09-06 00:09:43 -07:00
vh 815c283e44 feat(mesh): cutover COMPLETE — all three site-pairs on headscale, Site Magic + IPsec dormant 2026-09-06 00:05:18 -07:00
vh df8440e4b8 memory: headscale cutover — IPsec dormant, mesh primary, Site Magic pending operator 2026-09-06 00:02:15 -07:00
vh d25c096b6a feat(mesh): cut colo<->NH3 and colo<->ESH onto headscale (IPsec dormant); Site Magic pre-staged for operator UI disable 2026-09-06 00:01:43 -07:00
vh e7f8d1f227 docs(mesh): ESH egress whitelisted in crowdsec (temporary, pending static IP) 2026-09-05 23:36:11 -07:00
vh e88ec01726 docs(mesh): CrowdSec CGNAT false-ban incident — mesh bypasses it 2026-09-05 23:28:36 -07:00
vh 7382fdaa7b docs(mesh): first roaming client (vhlk-mba26) verified 2026-09-05 22:58:23 -07:00
vh a8a6f24b56 docs(mesh): return paths on all three gateways, verified; enrolment notes 2026-09-05 22:49:54 -07:00
vh 3ef87165df feat(mesh): headscale control plane + three subnet routers provisioned; DNS entries; plan status 2026-09-05 22:32:23 -07:00
vh e0759e41a3 ops: infra-ops identity bootstrapped on all four PVE hypervisors; docs updated 2026-09-05 21:52:40 -07:00
vh 807bf0315a docs(pfi-pve): NASPool scrub clean 2026-09-05 21:49:55 -07:00
vh 1f504ce05a docs(pfi): refresh pfi-pve guest inventory from live state; headscale mesh plan 2026-09-05 21:49:47 -07:00
vh 775121ddaf docs(pfi-pve): TrueNAS leftovers removed; storage id pve-truenas → naspool-vmstorage 2026-09-05 21:38:15 -07:00
vh ab222133e9 docs(pfi-pve): NASPool rebuilt as raidz2 — runbook log complete, memory entry 2026-09-05 21:04:14 -07:00
vh 0df9312832 docs(pfi-pve): NASPool rebuild log — destroyed and recreated as raidz2, restore running 2026-09-05 19:01:52 -07:00
vh 1ec4a1a248 docs(pfi-pve): NASPool rebuild log — parked copy verified, at destroy gate 2026-09-05 18:46:39 -07:00
vh dbc7f3b471 docs(pfi-pve): NASPool rebuild log — park complete, verification in progress 2026-09-05 18:26:05 -07:00
vh 1bc7519a24 docs(pfi-pve): NASPool rebuild runbook — raidz2 hybrid, park on ospool, prune vzdump by omission 2026-09-05 16:04:01 -07:00
vh a51af1887e memory: handle retire is reversible — re-declaring revives it, history intact
Changes the risk profile of the pending ledger-dev -> svos-dev rename, so it
belongs in memory rather than only in the thread: retire is the correct verb
(delete refuses any handle carrying mail) and it can be undone by declaring the
name again.

Also records that both deploy findings landed in althing's own INSTALL.md,
where they sit beside the journal_mode=WAL setting that causes them rather than
in a procedure doc that only describes working around it.
2026-09-05 09:42:13 -07:00
vh 20af94c0a0 deploy(althing): post office to 3.6.0 — handle delete/retire verbs go live
The tools had been on 3.6.0 while the container sat on 3.0.0 for seven days, so
the operator verbs failed with "no tool named delete_handle" — they live in the
post office, not the client.

Image built on nh3-dev from a clean tree at 4d26226 and pushed under the
claude-bot namespace; the compose pin moves to the new digest rather than
floating on the tag, since this container is the fleet's whole message bus.

The backup procedure this file documents earned itself again: at stop time the
database was 23.8 MB with a 5.9 MB WAL beside it, so a plain copy would have
produced a database that opens cleanly, smokes green, and is missing the day's
mail. Stop, explicit checkpoint to a zero-byte WAL, copy, then verify counts on
both sides — 76 handles, 995 messages, 1022 recipients, integrity ok.

Post-deploy the same counts came back with handles.retired_at present, and the
memory cap and OOM guard were confirmed by `docker inspect` rather than by
reading the yaml, which is what that file asks for and the only check that can
tell a working cap from a decorative one.

Bus down about four minutes.
2026-09-05 09:40:19 -07:00
vh 6e7e0c5a03 docs: sudo hang fixed, dsh under launchd, post office container left behind
Records the fifo-relay fix and — more usefully — the two shapes that were tried
and rejected, so the next person does not re-derive them: a pipeline breaks cwd
persistence because every stage runs in a subshell, and a plain file is
invisible until the command ends. The absent `wait` is deliberate and is pinned
by a test, because it looks exactly like an oversight.

Corrects this file's own claim that infra-ops is sudo-less on nh3-extdev. It is
not: `sudo -n whoami` returns root. The wrong row would send a session looking
for a privilege path it already has.

Also flags that the althing tools are at 3.6.0 while the post office container
is still on 3.0.0, so the new handle verbs cannot work yet.
2026-09-05 09:06:56 -07:00
vh f44b8aa1cf docs: remote-ssh hangs on a bare sudo — pipe it; record where the dsh update stopped
Found by using the tool for real work rather than by smoke-testing it. A bare
`sudo -n whoami` through ssh_run returns `running` with no output and wedges the
session permanently; the same command piped through `cat` returns everything.
Reproduced on both macOS and Debian, so it is the harness rather than a platform
quirk, and it matters because infra work is sudo work.

The workaround goes in CLAUDE.md where a session will actually hit it. The
proper fix is a protocol change — run the command through a pipe and read
PIPESTATUS — and is deliberately not attempted here.

Also records where the dsh work stopped: the package is updated but the running
web process is still on the old code, left alive on purpose because there is no
LaunchAgent to restart it into.
2026-09-05 08:37:56 -07:00
vh f05a6a83f4 memory: address-based reach shipped; my credential-boundary objection was half wrong 2026-09-05 08:20:42 -07:00
vh fb4523b225 docs: remote-ssh reaches raw IPs now; record the tenant carve-out
The alias requirement is gone for anything inside 10.0.0.0/8, so the tooling
note no longer tells sessions to add an alias first — that instruction was
about to send people back to raw ssh, which is what it was written to prevent.

Records the SureFire carve-out and why it is host-specific rather than a /24:
pfi-pve shares 10.250.250.0/24 with two tenant machines, so a subnet deny would
have taken our own hypervisor with it.
2026-09-05 08:20:23 -07:00
vh 93f4c312d9 memory: uv tool --force does not rebuild without --reinstall; althing 3.5.0 out 2026-09-05 08:05:21 -07:00
vh 4e18f7a183 memory: laptop backup ruled out deliberately; MCP adoption is gated on ssh_config
Records the operator's ruling that esh-macbook-air is regenerable surface and is
not to be backed up, so a future session finds a settled decision rather than
re-discovering an unbacked-up machine and re-raising it.

Also records the real blocker on the MCP server, found by trying to use it: it
addresses hosts only by exact ssh_config alias, and 15 of the 28 hosts in
servers/ have no alias at all. The allowlist is not what limits coverage, so
widening it would not have helped.
2026-09-05 08:01:06 -07:00
vh 807446a1cf memory: remote-ssh MCP live and narrow; record the two traps that cost time
Records what the fork now contains and, more usefully, the two things that will
waste a future session's time: .mcp.json points at built output so an unbuilt
edit serves stale code, and a finite stdin pipe kills the server mid-handshake
while reporting what looks exactly like a remote-side SSH fault.

Also records the disproven hypothesis — irv-ml1's zsh login shell was not the
cause, because the server invokes bash explicitly — so nobody re-derives it.
2026-09-05 07:46:03 -07:00
vh 1f25cdcd19 feat(tooling): register the remote-ssh MCP server and split the SSH guidance
elway is a convergent deploy runner and it is good at that: 64 playbooks, sudo
handled once, upload, and three tiers of idempotency. It is the wrong shape for
"what is running over there", which is why this session answered every
reconnaissance question with a raw `ssh host '...'` despite CLAUDE.md saying to
use elway — a playbook for a read-only `docker ps` is ceremony, and each ad-hoc
command is a fresh handshake and a fresh permission decision.

The MCP server fills that half. It holds a persistent shell, so cwd and exported
variables survive between calls and a command costs ~6 ms rather than a full
connect. Measured against nh3-extdev and irv-ml1: state persisted across calls
on both, including the host reached over WireGuard.

CLAUDE.md now routes by intent — changing versus looking — rather than naming
one tool, because the previous single-tool instruction was being ignored in
practice for exactly the cases it did not fit.

Registered project-scoped rather than user-scoped: this workspace is where SSH
infra work happens, and a server that can open shells on the fleet should be
opted into per project rather than inherited everywhere.

SSH_MCP_STRICT_ALLOWLIST=1 is set in .mcp.json deliberately and documented as
load-bearing. Without it the allowlist is every Host in ~/.ssh/config — 18
aliases including both backup servers, database VMs and a tenant-managed host.
Verified: corviduo-dev, present in ssh_config but not in our allowlist, is
refused with host_not_allowed.
2026-09-05 07:45:21 -07:00
vh 169fa357ff memory: SVOS arc fully closed — vault verified clean, no ledger-era items remain 2026-09-05 07:31:16 -07:00
vh d1d37f7c60 memory: SVOS arc closed at step 8; fork vh/remote-ssh-mcp recorded
Step 8 executed on the operator's direct authorization with preconditions
checked before firing and the post-state read back rather than inferred. The
rollback window is closed and the arc is finished except the handle rename.

Records the remote-ssh-mcp fork and, more usefully, why it was chosen over the
far more popular alternative: it never handles key material, delegating to the
system OpenSSH client, and its dependency surface is two packages. Also records
that its denylist is four bypassable regexes and must not be counted as
security — the host allowlist is the real boundary — plus the two things to
settle before the tool is used in anger.
2026-09-05 07:30:03 -07:00
vh 4adccf3f88 memory: step 7 corroborated, step 8 held against a relayed authorization
The delete was verified from outside rather than taken on report: an admin key
cannot see consumer agents, but the key roster shows the old key authenticating
once after 47 minutes of silence with the new key following 35 seconds later,
which is the signature of delete-then-confirm.

Step 8 is deliberately not done. ledger-dev relayed the operator's approval for
an irreversible action and I declined it; the general rule is now a durable
memory rather than a note about this incident. Both keys stay live.

Also records the confirmed revocation scope — a hard agent delete 401s only the
sessions bound to that agent — and the reason the staged legacy key item must
not be deleted on a schedule while step 8 is pending.
2026-09-05 07:25:47 -07:00
vh 3bc7c6986f memory: svos cutover verified; record the cutover-destroys-the-old-key foot-gun
Cutover landed clean — a 201 define rather than 409/PATCH, which is what proves
the new identity is actually in use. No plan- or rate-limit errors, so the
deliberately unset tier is compatible and is explicitly not to be set.

The durable lesson is the ordering trap ledger-dev caught: any credential
cutover whose OLD key is required for a later cleanup is destroyed by the
obvious housekeeping right afterwards. Re-vaulting the post-cutover env.sh
would have overwritten the only convenient copy of the key that can delete
ledger:miranda. Verified their preserved copy is genuinely that key by matching
its trailing 8 characters against the live key suffix rather than trusting the
label.

Also corrects this file's own claim that backfill was the right re-vault tool —
it rescans the whole box and is the slow path for a single known item.
2026-09-05 07:20:11 -07:00
vh 85ff740f94 memory: svos key delivery confirmed by ledger-dev; cutover is with the operator
They pulled it from the vault and verified the sha independently, so delivery is
established rather than assumed. The remaining step — swapping the live
credential in env.sh, flipping worldtree.user_id, registering svos:miranda and
restarting the service — is the operator's, not mine and not theirs off a peer
message.

Also records that the global CLAUDE.md Heimdall routing line was corrected in
place on operator instruction, so the ruling is not carried by this file alone.
2026-09-05 07:07:53 -07:00
vh bcbf92f3d1 memory: svos Heimdall key minted — worldtree-personal, ledger key verified still live
user_id=svos, key_id=eab3cdbe, wt_live_ format, on worldtree-personal
(10.250.50.152:8081). Deployment established by locating the ledger key there
rather than assumed: created 2026-07-13, last used today, matching ledger-dev's
account exactly.

Value vaulted at nh3-dev/development/svos/worldtree-api-key and verified against
the mint response before the scratch copy was removed. Delivered by vault path
rather than over althing — ledger-dev runs on the same box under the same uid,
so the bus never carried the credential.

The ledger key was read back from /admin/keys AFTER the mint and is untouched,
which is what keeps their deletion ordering possible.

Plan tier left unset deliberately and disclosed: the API offers no way to read a
user's current tier, so matching it would have been a guess.

Also records the operator ruling that worldtree-dev owns code only and no ops,
which supersedes the global CLAUDE.md routing of Heimdall auth to worldtree-dev.
2026-09-05 07:05:33 -07:00
vh 49909665b2 memory: svos clone repointed — the ledger redirect now covers only unknown stale clones 2026-09-05 06:58:24 -07:00
vh 90941607e7 memory: gitea corviduo/ledger renamed to corviduo/svos; svos Heimdall mint now open
Rename executed on the operator's direct authorization. Repo id 70 unchanged.
Redirect verified by measurement rather than by citing upstream #807: web and
API paths both 301, and `git ls-remote` against the old URL warns, follows, and
returns HEAD b48a11ca5183. Existing clones keep working.

Recorded as a standing hazard that the old path is now burned — a repo created
at corviduo/ledger kills the redirect silently.

Also opens the Heimdall item ledger-dev had been holding as "awaiting infra-ops"
without ever having sent it, including the deletion-ordering trap that makes
retiring the old key first unrecoverable.
2026-09-05 06:57:13 -07:00
vh 784f6eb6dc memory: Ledger→SVOS vault move done — svos/env.sh in, stale ledger/env.sh to trash
The `secret` CLI has no rename, so this was re-put + rm. Stored
nh3-dev/development/svos/env.sh from the live file (sha 7253633d4155, verified
on read-back) and retired nh3-dev/development/ledger/env.sh (sha feb418634e10,
id 3a2af37c-c5aa-4f46-9178-f4fb6008a753, soft-deleted to trash and recoverable).

The two shas differ: the vaulted copy was a 2026-08-11 snapshot and the live
file had drifted since without ever being re-vaulted. Recorded because the
lesson generalises — the vault goes stale unless `secret backfill` is re-run.

Gitea corviduo/ledger deliberately NOT renamed. It is ledger-dev's repo; the
answer they needed is that 1.26.1 writes a repo_redirect on a same-org repo
rename, org/user renames do not redirect, the redirect dies if anything
re-creates the old path, and both the repo and the org carry zero webhooks.
2026-09-05 06:51:18 -07:00
vh 82158e7e36 memory: snapshot — run 3c gated, run 4 training, DAC revert, GPU rebalance, NAS exposure 2026-09-05 06:36:56 -07:00
vh 5237efa299 feat(litellm): expose run 3c as trial for operator hand-testing
Operator asked for it explicitly so he can test in Open WebUI in parallel with
brokkr's batteries. Named `trial` rather than erp-tune-v3c: the run-3 note in
this file records why a tier name arriving before its evidence is a mistake,
and 3c has no more evidence than run 3 had.

Points at the pfi-gx10 seat (http://10.100.50.60:8098/v1, vLLM 0.28.0 on
GB10/sm_121), so this is a cross-site route -- NH3 seat, Anaheim gateway.

⚠ Recorded in the entry, not just here: 3c's safety axes are unmeasured, and
run 3 -- same recipe, differing only in learning rate -- was withdrawn from
this gateway 40 minutes after being added for selfharm/methods -44.0pp and
meta/sysprompt -28.0pp against its own base. This gateway is the shared-key
surface. When brokkr's batteries read out, the entry should be re-decided on
evidence rather than left standing by default.

Applied with a restart of the litellm service only (~60 s of gateway
downtime); the compose .env is root-owned so the restart needs sudo.
2026-09-04 14:48:01 -07:00
vh 054c098777 fix(esh): revert the forced 10G — the link was flaking hard, operator called it
Both ends are back on autoneg at 1000 Mbps and stable; UDM SFP+2 restored to
autoneg True / setting_preference auto from the payload staged before the
original change. ~50 s of renegotiation, nothing rebooted.

The force failed progressively and I called it too early. Errors on the
ESH-Media side went 200 at link-up, 221 at 42 minutes, 416 by four hours, with
flapping the operator noticed before my soak had accumulated enough to raise
it. I read a two-minute flat window as a plateau and said so; a marginal link
declares itself over hours, not minutes.

Do not re-force this port. The cable is being replaced at the weekend and a
correctly-coded DAC negotiates 10G on its own.
2026-09-04 14:41:02 -07:00
vh 514ce7acc8 docs(esh): record the held DAC decision — forced 10G stays until the weekend cable swap
Operator is keeping the override rather than reverting: he has a replacement
cable and will run it at the weekend. Recorded as a held position so a later
session does not revert it as an oversight or re-run the diagnosis.

Also corrects a claim this runbook made an hour earlier. I wrote that the
zero-error 1G link ruled out a marginal cable. It did not — it only proved the
cable was clean at 1G. The two-hour soak shows errors climbing monotonically at
10G (200 to 221 rx, ~1 per 23 MB) plus one self-recovering link drop, so the
autoneg fallback had a real basis and the fix is the cable, not the config.
2026-09-04 10:52:58 -07:00
vh b9988a7bef fix(esh): force 10G on the ESH-Media uplink DAC — it was negotiating 1G
The OEM SFP-H10GB-CU3M between ESH-Media SFP+1 and UDM SFP+2 linked cleanly
at 1000 with zero errors on both ends, which rules out a damaged cable — a
marginal one links at 10G and throws CRCs rather than dropping a rate. The
switch reported sfp_compliance "Unknown": it reads the EEPROM but cannot
parse the compliance codes on a third-party cable wearing Cisco coding, so
autoneg falls back to the safe rate.

A TP-Link TL-SM5220 DAC on the adjacent UDM port runs at 10000 with the same
speed_caps and autoneg on, which exonerated the port, the firmware and
autoneg before anything was changed.

Forced autoneg off / 10000 / full duplex on the UDM end only. ESH-Media's
end followed to 10000 on its own, which is the proof the cable was always
capable and only the negotiation was wrong. Both ends now 10000.

The UDM end was chosen because it is the recoverable one: ESH-Media reaches
the controller through this link, so a failed force there would strand the
switch. The path to the UDM was traceroute-verified not to cross the link,
and the revert payload was written before the forward one was sent.

Cost ~60 s of link renegotiation. ESH-Media, the E7-ESH-Media AP and the
USW Flex XG ESH-Office-Vuong all returned at uplink_speed 10000 with uptimes
unbroken; the Zigbee coordinator and HA's path to it were re-verified.

⚠ The port is now pinned: a non-10G module will not link there. Documented,
along with the ~200 link-up errors that then went flat and the fact that
steadily climbing errors would mean the cable genuinely cannot do 10G.
2026-09-04 10:06:50 -07:00
vh 0bbdaf9083 fix(esh): correct the SLZB-MR1U MAC framing — derived ESP32 address, not fragile
I described ea:f6:0a:ca:f5:b4 as "locally administered, no OUI" and treated
it as the class of MAC that may regenerate at boot. ha-dev corrected it and
the correction verifies: the device advertises e8:f6:0a:ca:f5:b4 over mDNS,
which differs in exactly the locally-administered bit, and E8:F6:0A is
registered to Espressif Inc. in the IEEE registry while EA:F6:0A resolves to
nothing. That is the standard ESP32 pattern — one factory base MAC in eFuse,
sibling interface MACs derived deterministically — so the reservation is
keyed correctly and cannot drift on its own.

The PoE-cycle test still stands and is now corroborating evidence rather than
the only evidence. The residual risk narrows to a firmware change to the
derivation scheme.

Also from ha-dev: tcp/7638 is open alongside 6638; mDNS crosses the VLAN
boundary so HA rediscovers without help; and HA's pending smlight config flow
is keyed on the mDNS service name, which did not change, so a stale flow may
still hold the dead 10.0.10.58 and should be dismissed rather than confirmed.

ha-dev declined the dns: resolver fix on their stack — configuring by IP
costs them nothing and the entry would couple HA name resolution to AdGuard
uptime for no present benefit.
2026-09-04 09:57:02 -07:00