533cc0ce8168d2a141125aaa8f1bf315c4f21a15
1192
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
533cc0ce81 |
build_sft_pairs: reject beats that name characters the rename removed
A leak the corpus gate structurally cannot see, found on lv-bronte. The rename strips the author's names from the prose and leak_gate.py proves they are gone — 0 of 365 surviving on Brontë, both controls green. But the beat is written by an LLM that READ THE PASSAGE, and if it recognises the book it supplies the canonical names out of its own training. The beat is the INSTRUCTION half of the pair, so training on it re-teaches exactly the inventions the rename pipeline exists to remove, and the gate never looks at it: the gate reads the corpus and the renamed copies, never the generated beats. MEASURED on the first 714 Brontë pairs, before the filter existed: 13 beats (1.8%) named source characters — Rochester x6, Jane x3, Brocklehurst x2, Beck, Fairfax, Helen, Burns, Eyre, Reed, Rivers 0 of 714 RESPONSES did. The rename was perfect; the instruction side was not. One beat read "Saoirse confirms Rochester's flaws, then agrees in English to marry him" — a renamed name and a canonical one in the same sentence, which is the mechanism in miniature. Exposure scales with how well the generator knows the book, so it is WORST for public-domain classics and mildest for recent work. That is exactly why the Yarros and Hemingway runs came up clean and Brontë did not — their clean runs are NOT evidence this cannot happen to them, and both should be rebuilt with --source-entities if they are ever regenerated. Adds a `sourcename` reject to vet() plus --source-entities, which takes the UNRENAMED entity map and refuses any beat naming a surface from it. Firing at roughly 3% of attempts on Brontë. Also adds a `bronte` register. Brontë is the far end of the same axis from Hemingway and the register has to say so, or the beat-writer produces modern summary prose the passages never match. |
||
|
|
fc834a8a23 |
bronte-corpus: gate lv-bronte for real — 0 of 365 with both controls green
The Brontë corpus's "0 of 203" was a HAND COUNT made before leak_gate.py
existed. On Yarros the automated instrument read 212 surviving where a hand
count said 86, so the hand count was never evidence. This runs the real gate,
and getting it to pass required fixing four defects the hand count could not
have seen.
CORPUS DEFECTS (repair_corpus_bronte.py, both measured):
- 1,922 words of publisher back matter inside Shirley's last unit — a
T. Nelson & Sons catalogue advertising Ainsworth, Marryat, Verne, Kingsley
and Dickens, plus a Gutenberg transcriber's punctuation list. Not Brontë,
and the source of the entity CHARLES. Same structural cause as the
Hemingway run: a splitter cuts on headings, nothing follows the final one.
- 1,368 Gutenberg italic spans. Two harms: they teach the adapter to emit
underscores, and the underscore is a word character, so the gate's
word-boundary scan cannot match inside an italicised name. An entity in
italics is invisible to the gate — the same never-renamed-AND-never-
reported shape as Yarros's possessive-only Afendra.
DETECTOR GAPS (phrase_map_bronte.json):
- Blanche is 19 capitalised against ONE lowercase — ratio 0.0526, over the
0.05 bar by a single token, so a named character with 19 mentions is
dropped by a hair.
- Grace (0.224) and Hollow (0.235) are refused correctly — both are common
nouns — but Grace Poole and Hollow's Mill are Brontë's. Sampling all 21
bare capitalised Grace found 20 are the character in direct address and
exactly one is the theological noun.
- Five compounds whose every component is non-renameable survive verbatim:
Moor House, Marsh End, Vale Hall, Bigben Close, Royd Lane. The other 77
audited phrases do not, because each has a renameable component.
GENDER (pin_known_gender.py): the inherited resolver put Jane MALE across 336
occurrences. Hemingway's base-rate resolver is strictly better here (1 wrong vs
4) but still fails on Jane, and the failure is structural, not tuning — Brontë's
three narrators are first-person, so their names appear almost only in dialogue
surrounded by other characters' pronouns. Ground truth is pinned separately from
the resolver's evaluation so the two are never conflated.
Also: min-cap lowered 8 to 3, which pulled Bertha, Ferndean, Rochesters and
Creemsvort in from below the old floor; corpus-scope rename so a name below
threshold in one novel is not printed verbatim there while renamed in another.
Gate: 0 of 365 surviving, positive control 365/365, negative control clean,
phrase audit 0 of 82. Floor stated: 3 capitals per work, 5 recurrences.
|
||
|
|
a5745dcf72 |
playbooks: generic per-service stack image update (pull + recreate + verify)
Adds playbooks/update-stack-image.yaml — pull the newest image for one
compose stack service and recreate it, with a verify phase that asserts
the container's image id equals what the tag now resolves to rather than
trusting a 'Up' line from docker ps.
Scoped to a single service on purpose: the recreate is 'up -d <service>',
never a bare 'up -d', which would recreate every service in the project.
Go template format strings are written bare; elway's {{ identifier }}
substitution leaves them alone, but {{end}} / {{else}} would match and
die as undefined variables, so the health read uses {{json .State.Health}}
instead of an if/else.
First use: drawio on esh-docker-vm, 28.1.2 -> 31.4.6.
|
||
|
|
e8086941e2 | memory: snapshot — lv-yarros shipped, voices-seat live, lv-hemingway training, Grok broker shelved | ||
|
|
cf9d167453 | grok-token-broker: shelved — operator kept the jail, so the renewal feature has no consumer | ||
|
|
b907a0e46a | grok-token-broker: the probe's blast radius is BOTH Groa transports, not one | ||
|
|
ebc4dac6d8 | grok-token-broker: hold a refreshable session credential behind a rotation-safety gate | ||
|
|
d17bd3df86 | voices-seat: one carrier, lv-<author> LoRA adapters, with the measured cost and placement limits | ||
|
|
6282833669 | pairs: fix the three construction defects and the abbreviation-truncation bug | ||
|
|
03b4a3f62c | BabyHemingway D2+D3: entities, base-rate gender resolver, rename preset, leak gate passes | ||
|
|
9598d0b4a7 | BabyHemingway D1: fiction-only corpus builder with measured exclusions | ||
|
|
7cc847f33c | memory: client abandon sometimes cancels and sometimes does not; the KV trajectory is the tell | ||
|
|
75051243ae | BabyYarros: amend the decision rule to gate on voice and non-memorization (operator-authorised) | ||
|
|
efb734586b | BabyYarros: raw-surface scoring and a memorization check with both controls | ||
|
|
713e83dd5e | BabyYarros: pre-register the pair-vs-rawtext decision rule before the arms are read | ||
|
|
90ed506db3 | fix(pairs): apply_chat_template returns a BatchEncoding, and assert the mask boundary | ||
|
|
9b3d3c80cb | BabyYarros Option C: instruction-pair builder and the assistant-masked pair trainer | ||
|
|
d0108466e9 | memory: the owed handoff datapoint is a tripwire, not a validation | ||
|
|
36f9240477 | memory: the handoff leak has two surfaces, and the dangerous one fires on ordinary input | ||
|
|
0b6ea6f910 | memory: the /snapshot handoff modality defect is fixed, and my two real runs corroborate it | ||
|
|
1ffb6d7af8 |
memory: report the /snapshot handoff defect to galdrabok, with the mechanism
Sent with both specimens. Adds the root cause, which is in SYSTEM_PROMPT rather than the model. Next steps is the only one of the three generated sections with no empty case. Watch out for is told to omit itself when there are no gotchas and Resume here is told what to say when nothing is in flight, but Next steps is told only that it is a numbered, ordered, concrete list. With nothing in flight the sole action-shaped nouns in the input are the deferred items, and the nothing-in-flight rule points the model straight at them by asking it to name the most recent open pointer. Nothing in the prompt protects modality. Invent nothing and trace every claim to the input are both satisfied - the items really are in the input - while their deferred-ness is exactly what gets dropped. The verbatim-identifier rule already establishes that some attributes of the input must survive restructuring untouched; modality is one of them and only identifiers are guarded. Proposed two prompt changes to galdrabok: an empty-case escape for Next steps, and a rule making deferred, parked, belayed and deliberately-not-done items constraints belonging in Watch out for rather than steps. Offered as a caller's diagnosis since the skill is theirs. Noted that galdrabok-dev is pull mode, so there is no herald poke and they will see it on their next check. |
||
|
|
9d36c74572 |
memory: snapshot refresh — breeze settled, util does not predict residency, handoff defect
Incremental over
|
||
|
|
7a33bd9f09 |
memory: breeze stays put; TTS-stack move to fv-ml1 parked at id 75
Operator ruling: leave breeze-tts on irv-ml1 and park moving it, bragi and tts-gateway to fv-ml1 until the embedder, reranker and reward seats are evacuated. Parked as move-the-tts-stack-breeze-tts-bragi-tts-gateway (id 75) with the trigger, the footprints and the migration gotchas, so it resurfaces with everything needed rather than as a bare line. Two things worth having recorded against the trigger. All three services move as a set because only breeze is GPU-resident at ~10.3 GiB and growing, while bragi and tts-gateway are CPU-only proxies - co-location with the gateway is the entire reason not to move breeze alone, since that is what puts a cross-site hop on every TTS call. And the trigger as stated names gpu0, but vllm-embed, vllm-rerank-a3 and vllm-reward are all pinned to GPU 1. GPU 1 is the constrained card at 0.975 committed with 4,336 MiB free, while GPU 0 has 11,982 MiB free and carries the live chat path, so evacuating those three relieves GPU 1 rather than GPU 0. Recorded as a confirm-before-executing rather than silently corrected, since it changes where the TTS stack would land. Also notes that bragi and tts-gateway reach each other by name only through extra_hosts pins, because containers on irv-ml1 cannot resolve nh3.internal - those pins travel with them and need re-pointing at the new host. |
||
|
|
b0e7b408d9 |
memory: breeze-tts sizing and the fv-ml1 GPU 0 placement recommendation
The operator asked this mid-sweep and the answer never reached durable memory - caught only because he asked again after the snapshot. Recommendation is not to move it. Re-measured rather than reciting the earlier figure, which was right when taken and is now wrong: breeze holds 10,316 MiB after 53 minutes of uptime against 9,218 MiB shortly after warm-up. The footprint grows with use, consistent with PyTorch's caching allocator not returning memory - probably caching rather than a leak, but resident either way and counting against any neighbour. Two points is a trend, not a curve; whether it plateaus is unmeasured and stated as such. That changes the placement answer. fv-ml1 GPU 0 has 11,982 MiB free, so the margin is 1.7 GB and shrinking rather than the 2.8 GB the earlier number implied, on the card carrying the live chat serving path. The stronger objection is topology rather than VRAM: tts-gateway runs on irv-ml1 and reaches breeze on the same box, so moving breeze alone puts a cross-site hop on every TTS call against a 478 ms to-first-sample budget. Moving it properly means moving the gateway too. It is also not constrained where it sits - the 3090 still has 10 GB free. Also records the trap that nearly produced a wrong number: breeze reports nothing at idle when queried on the wrong GPU, because BREEZE_GPU_DEVICES=0 is the 3090 rather than the A6000. An idle query of the A6000 shows it absent entirely. |
||
|
|
653f7fb939 |
memory: snapshot — Parakeet STT, svos_miranda live, talk v10, address sweep, secrets fix
Closes both of the previous session's named jobs and six unplanned pieces of work. Nothing is in flight and nothing is blocked. Parakeet STT live on fv-ml1 GPU 0 behind LiteLLM ext-stt and whisper-1; GPU 3 is now a documented reserve after the operator caught an 800 MiB seat parked on the one pristine 96 GB card. svos_miranda enabled and Miranda serving, with agent.disabled_toolsets deleted and staying out by operator ruling. talk v10 deployed as the STT seat's first consumer. The irv-ml1 dead-address sweep is complete at 0 of 112 Homepage cards, having turned up four live breakages on other hosts. The secrets-broker concurrency bug is fixed, and ~/.local/bin/secret is a symlink rather than a stale copy. Auto-archival fired at 433 lines but moved only one entry: three of the four candidates old enough to qualify carry open deferred-work pointers - a park id, an althing thread, and an explicit 'untracked by operator choice' - and the guard held them. The index stays over cap at 389 lines, which is the correct trade: nearly every entry is genuinely under fourteen days old. The generated handoff needed correcting in-session before it shipped. The model had turned three operator-deferred items into a to-do list and invited the next session to commit files that predate this one. Both would have read as instructions to a fresh context, which is the durable-false-warning failure this session spent the day documenting. |
||
|
|
f4320ff57e |
docs(memory): the talk-deploy permission problem never existed
Vuong asked me to find and fix the harness issue blocking tts-dev from deploying talk. There was no harness issue, and no issue of any kind. /opt/docker/compose on nh3-dev is root:docker 2775, agent sessions run as lkraven, and lkraven is in the docker group. A mkdir settles it in one second and nobody ran one for nine days. There is also no tts-dev OS account, so the group request had no referent. It held together because a stale persistent-memory row supplied a plausible mechanism and the operator's routing instruction - 'give it to infra' - was read as corroboration of a capability limit. Those are different claims and only one was ever stated: a routing preference explains where work went, never whether it could have gone elsewhere. A contradicting ls -la was on screen in the same session and was dropped. I then repeated the claim to the operator as fact in a deploy report, which put a second name behind it. Then I did the same thing one layer up. Finding no OS problem and no deny rule, I inferred an auto-mode classifier refusal because the shape fit, and committed a settings.json into tts-dev's repo on that inference. Their mkdir showed the path writes with no refusal at all, so the hypothesis was wrong and the commit is reverted. I had spent the night writing up this failure class and still built a fix for a layer nobody had shown me failing. That commit also claimed a doc correction it did not contain: the edit and the commit were chained in one invocation, the edit's anchor assertion failed because the target text had already been fixed, and the commit ran regardless. Amended before reverting. Never chain an edit and its commit in one invocation. The rule worth keeping is that 'I can't do X' from any source is a hypothesis until someone runs the command and pastes the error, and that 'there is no error text, because there was no error' is a possible answer. |
||
|
|
af8d6df387 |
docs(memory): name the fleet's characteristic failure mode
svos-dev observed that three instances of the same shape turned up between two agents in one night and that it is starting to look like a characteristic failure rather than a coincidence. Collecting all nine from today, because the class is more useful than any instance. The shape is a check that reads the input to a transformation and gets reported as if it read the output - or more generally, the instrument answering instead of the system, in a form shaped exactly like a real answer. What makes it expensive is not that things break but that the broken state is indistinguishable from a legitimate one, so it passes review and is found later by accident. Every one of the nine passed a check. The tell is stated so it can be recognised prospectively: whenever 'broken' and 'legitimately empty, absent or off' produce the same output, the cheap check cannot tell them apart by construction. Remedies that actually worked today: measure the output rather than the input; positive controls, since a method that has only ever passed cannot tell you it is not blind; true-negative controls, because two apparent failures in the secrets-broker test were names I had invented and would have been read as a partial fix; refuse to emit the ambiguous value, which was the real fix rather than the lock; and do not declare victory on a plausible fix, which is the only reason the session-establishment root cause was found at all. |
||
|
|
0193b31aad |
fix(secrets-broker): bw is not concurrency-safe — serialise, and never return an empty secret with exit 0
Reported by svos-dev after parallelising four vault reads in SVOS's systemd wrapper. Reproduced here and it is worse than reported: four concurrent secret get calls for distinct items returned empty strings with exit code 0, zero of four succeeding against their one of four. No error, no timeout, no diagnostic. The shape is the problem, not the race. A caller treating an empty optional secret as 'not configured' degrades silently and never learns otherwise - it cost SVOS the ability to page the operator while the process logged a clean startup line. Root cause is session establishment, not item reads. Every invocation runs bw unlock, and concurrent unlocks against the shared appdata dir invalidate each other. The damage then surfaces downstream as an empty listing or an empty item body, which is why a per-call lock is useless: by the time the read runs the session it holds is already dead. So the lock wraps the whole command instead. Three changes. The command-level lock makes concurrent callers queue. cmd_get now refuses an empty value rather than printing it, since a stored secret is never legitimately zero-length. And find() no longer coerces empty stdout to '[]' - that turned a broken read into a confident 'no such secret', the same silent-wrong-answer shape one layer up. Verified: four parallel reads of four real items now return all four correctly, serialised at the honest ~17s each. A name that genuinely does not exist still fails loudly, so the guard did not simply mute the negative case. Also replaces the copy at ~/.local/bin/secret with a symlink to this file. It was a plain copy in sync by luck, and every edit here silently left the live tool behind. |
||
|
|
66c860d6c1 |
fix(sweep): retire the dead 10.100.79.3 address across the fleet
Operator-directed. The wg0 lifeline retired at the 2026-09-06 headscale cutover is on no interface anywhere, so anything pointing at it gets no route at all. Homepage went from 9 dead cards to 0 of 112. The load-bearing part is that there is no single right target: it depends on who resolves it. The operator's browser and the Homepage and open-webui containers on esh-docker-vm all resolve nh3.internal, so those get the name and survive the next renumber. Containers on irv-ml1 and ana-docker cannot resolve it at all, so those get the IP. litellm on ana-docker looked like a counterexample and is not: it resolves the name only through its own extra_hosts entry, while asset-engine on the same host fails on it. Test from the container you are about to change, never from a neighbour. Before committing to the name I confirmed the Homepage container actually fetches ytvc's healthz through it in production rather than assuming resolution implies reach. On irv-ml1, 24 files swept and 14 comment-only hits left as port-allocation history. Seven running containers recreated so the labels took. Seven dormant ones carried stale labels because editing a compose file does not touch an existing container object - fixed with compose create --force-recreate, which rebuilds the container without starting it, the right tool for a deliberately dormant stack. The sweep's real find was off irv-ml1 entirely: four live values on two other hosts, silently dead for nine days and alerting nobody. Open WebUI's read-aloud TTS, asset-engine's inference host, and two skaldsong TTS URLs. Both running services were recreated and verified reaching their targets afterwards rather than merely carrying the new string. One self-inflicted outage worth recording: I recreated breeze-tts for a cosmetic label change and took ext-tts down for its ~90s CUDA-graph warm-up, returning 500. I caught it only because I had taken a baseline before touching it. A label-only edit still costs a full model reload on a GPU container. |
||
|
|
8bc46e5132 |
docs(memory): record the gap tts-dev found in my served-page gate
They adopted the gate as tts-stack tools/gate_served_page.py and extended it in a place that matters: my version would have passed a broken page. A worklet lives inside a template literal, so a syntax error in it is invisible to a parse of the enclosing script - it is just a string until addModule compiles it at runtime, where it fails as a rejected promise and the page quietly falls back to buffered playback or records nothing. Silent degradation, which is harder to notice than a dead page rather than easier. They parse the worklet separately, and they positive-controlled the whole thing against two deliberately broken pages rather than assuming a gate that has only ever passed is not blind. The second control - valid enclosing script, broken worklet - is the one my version fails. The lesson on my own work is the useful part: I built a gate for the failure I had just been shown and stopped at its boundary. The class is 'code that is a string at parse time and code at run time'; an inline script is one instance and a template-literal worklet is another. I checked the instance, not the class. Also promotes the underlying rule to the index, since it was named twice tonight from two unrelated directions: a check that reads an artifact as stored cannot see a transformation that happens between storage and execution. |
||
|
|
e113660b08 |
docs(memory): talk v10 deployed with a served-artifact gate
Operator-instructed via tts-dev. First consumer of the ext-stt seat stood up the same night - talk can now listen as well as speak. Relayed authorization was fine to act on because the work is reversible: one-line tag rollback, v1..v9 retained, compose and .env backed up. Checked the escape hatch existed rather than believing the message that described it. Gated properly: build, throwaway on a non-live port, four acceptance checks, tear down, then cut over in a separate invocation. Re-ran all four against production afterwards, because a gate that only ever ran against the throwaway proves the image rather than the deployment, and confirmed the two new env vars inside the running container rather than in the file. Added a fifth gate worth keeping. tts-dev's worst bug this cycle was a JS escape inside a Python string arriving transformed, closing the string and killing the entire inline script while the page still rendered and both import and node --check passed - because the file still held the backslash. So: fetch the page over HTTP, extract the inline script from the response body, and node --check that. Same instrument pointed at the other side of the transformation, and over the wire it also catches anything that mangles the body after TLS and ASGI. That is the second instance tonight of one rule: a check that reads the artifact as stored cannot see a transformation between storage and execution. provider=cuda in a log is the same error - an echo of configured intent read as a measurement of running reality. Also notes an open question for the operator: talk deploys route through infra-ops only because tts-dev's identity is not in nh3-dev's docker group. The durable fix is a group membership, not a standing relay. |
||
|
|
01f0014489 |
docs(memory): SVOS/Miranda fully live; bank two restart patterns from svos-dev
svos-dev restarted :8770 at 02:17 and both roster lines printed clean. Confirmed from this side rather than taken on their word: :8770 answers 200 on the new pid, an unauthenticated Bifrost dispatch gets 401, and Hermes reports 29 toolsets with svos_miranda the sole enabled=True row. Two patterns from their restart that generalise past this service. A dry-run boot against the still-held port: start the new process while the old one still owns the socket, and it proves every check above the bind before dying on EADDRINUSE. Zero downtime, no commitment, and it turns a one-way restart into a rehearsed one. Worth doing for any service whose startup validates before binding. And a trap: SIGTERM released the port but left the process alive for 35 seconds, needing SIGKILL. The port was free that entire time, so a script waiting on port availability would have started the replacement alongside a still-running old process. Kill by PID and wait on the PID, never on the port - a freed port is not evidence of a dead process, the same way an unreachable post office is an outage rather than an empty inbox. |
||
|
|
406769e64b |
docs(memory): bank the Parakeet bench result and tts-dev's storage-vs-execution lesson
The IRV seat was retired on tts-dev's numbers: it lost to the FV seat at both clip lengths and to whisper-large-v3 at 6.24s. Their length sweep fits ~58ms fixed + 56ms per audio-second with an asymptote of ~17.8x realtime, which independently reproduces our 17x on a different clip and harness, and the gateway hop measured below their harness resolution so ext-stt is the right consumer path. Two caveats recorded against our own numbers: their between-run variance is 20% because GPU 0 carries the live chat path, and our 0.50s median came off an idle GPU 3 - a best case, not a comparable. Their RTFx retraction is the durable part: published RTFx is batched throughput on datacenter hardware rather than single-stream latency, and the two differ by ~200x. Also banks the shape their acceptance gate caught, because it generalises past their repo. A JS escape inside a Python string arrives transformed, closing the string and killing the whole inline script, while the page still renders and both import and node --check pass - the file still holds the backslash. That is the same failure as reading provider=cuda out of a log: a check that reads the artifact as stored cannot see a transformation that happens between storage and execution. Both check the input to a transformation and get reported as if they checked its output. |
||
|
|
868b56642c |
docs(memory): svos-dev fixed the roster check; disabled_toolsets deleted from config
svos-dev landed c9d2a96 - build_miranda_roster now returns an empty disabled list unconditionally and the startup line no longer names the key. The 28-name list is removed from ~/.hermes/config.yaml rather than left commented, since a paste-ready array behind a hash is what a future session uncomments; a short warning stands in its place. Their mechanism is better than mine and replaces it in the record. _get_platform_tools resolves platform_toolsets first and applies global suppression last, so subtracting 28 names from a one-element platform set is a no-op by resolution order - not merely 'adds no safety on top'. That holds for any future platform; the measurement only established the single case. And the endpoint already carried the answer. _handle_toolsets computes each row's enabled as membership in the per-platform set, so verified live: 29 rows with svos_miranda the only one reporting enabled=True. A check reading that field rather than counting rows was correct all along, against a config that never needed the key. |
||
|
|
379fc27e7d |
feat(hermes): enable svos_miranda live; retire irv parakeet and voice-studio
Four operator rulings executed. svos_miranda is live in Hermes. Gateway restarted 02:10 (PID 3107822 -> 3901622, confirmed by observing the change). /v1/toolsets now reports 29 rows including svos_miranda, and an api_server session resolves to exactly the 8 plugin tools with the write-klass five absent. agent.disabled_toolsets stays off permanently: 'i dont want the tools disabled everywhere'. That key is a global end-of-pipeline subtraction rather than an api_server-scoped one - measured, a default session goes 46 tools to 20 - and it is unnecessary anyway, since platform_toolsets.api_server alone produces the exact 8-tool surface. The operator's own session was verified intact at 46 tools after the restart, which was the point of the ruling. The consequence is now SVOS's to absorb: it must stop verifying against the global roster before it restarts, because that roster is 29 by design and will not shrink. Two workable options went to svos-dev - verify the api_server surface instead, or relax the check to 'svos_miranda present and write-klass absent'. The second also survives any unrelated plugin landing on this host, which matters because 'stt' already appears in that endpoint's rows while resolving it logs 'Unknown toolset'. irv parakeet retired: it lost tts-dev's bench to the FV seat at both clip lengths and to whisper-large-v3 at 6.24s. Checked for consumers first - no gateway alias pointed at it, and every other reference on that host was a comment in a port-allocation register. Retirement banner on its README names the replacement. voice-studio stopped: it existed for the dots mint loop and Breeze obsoleted dots on 2026-09-06, so it was retired rather than repaired. |
||
|
|
1f98a1be32 |
docs(memory): voice-studio is retired not broken; agent.disabled_toolsets is global
Two corrections and one finding from the same night. voice-studio: operator ruled the stack out of service. It existed for the dots mint/audition loop and dots was decommissioned 2026-09-06 when Breeze took the fleet seat, so its reason to exist went with it - which is also why nine days of breakage alerted nobody. No v11 rebuild. The gate one-liner was applied minutes before the retraction landed and was left in place rather than reverted, since the value it replaced was a dead address and reverting is another recreate of a stack that is going away. Container not stopped: it was already running, and 'down for now' arrived as a relayed paraphrase rather than an instruction. The two host-level facts survive the stack. Containers on irv-ml1 cannot resolve nh3.internal at all, so on that host the DNS name is the WRONG fix for a dead-IP bug - it swaps a dead address for an unresolvable one. Confirm resolution from inside the container before recommending a name. And a stale link can have more than one drift behind it: voice-studio had three stacked, two of them invisible from the host compose file. Hermes: svos_miranda is installed and enabled in config but the gateway was NOT restarted, so it is not live. agent.disabled_toolsets as specified by svos-dev is not scoped to api_server - it is a strict end-of-pipeline subtraction applied to every session on every platform. Measured: a default session goes 46 tools to 20, losing memory, file, terminal, web, browser and more. It is also unnecessary: platform_toolsets.api_server alone resolves an api_server session to exactly the 8 svos_miranda tools. The line buys only SVOS's startup check, which reads a global endpoint to verify a per-platform property. Left commented out with the measurement inline so an incidental restart cannot gut the assistant. |
||
|
|
2fccaf7128 |
docs(memory): record the Parakeet bench result and a 96-place stale address on irv-ml1
tts-dev benched both endpoints against a Whisper baseline. FV wins at both clip lengths (155/391 ms vs IRV 354/1010 vs whisper-large-v3 457/690) — IRV is slower than the incumbent at 6.24 s, so the duplicate seat is now retirable on evidence rather than on tidiness. Their length sweep fits ~58 ms fixed + 56 ms per audio-second, asymptote ~17.8x realtime, independently reproducing our 17x on a different clip and a different harness. The gateway hop measured below their harness resolution, so ext-stt is the right consumer path. Two caveats recorded against our own numbers: their between-run variance is ±20% because GPU 0 carries the live chat path, and our 0.50 s median was taken on an idle GPU 3 — marked as a best case, not a comparable. Also records tts-dev's retraction, which is the durable lesson: published RTFx is batched throughput on datacenter hardware, not single-stream latency, and the two differ by ~200x. Their plan had projected 60-120 ms from it. Separately, chasing the one stale Homepage href they flagged turned up 96 occurrences of the retired wg0 lifeline 10.100.79.3 under /opt/docker on irv-ml1. Most are cosmetic, but voice-studio is genuinely broken: it is configured to reach studio-gate at that address, both are running, they sit on separate docker networks, and the address is on no interface on the host. Failing since the 2026-09-06 cutover with nothing alerting. ext-tts verified unaffected. Not fixed here — eight containers to recreate, three load-bearing, and the voice-studio repair touches app.py rather than config. Surfaced with evidence. The pattern is the third of its shape: a retired address needs a repo-wide grep by ADDRESS rather than by hostname, and container labels live in no file the sweep reads until the container is recreated. |
||
|
|
caa04801f3 |
fix(parakeet): move the seat from the empty GPU 3 to GPU 0
Placed on GPU 3 first because it was the empty card. That was the wrong read:
the seat is ~800 MiB, under 1% of a 96 GB card, so the question was never "where
does it fit" but "whose headroom is cheapest to spend".
vLLM sizes its KV cache as a fraction of TOTAL VRAM, not free VRAM. A resident
tenant on an otherwise-clean card therefore does not cost its own megabytes — it
costs the profiling margin of whatever full-size seat lands there later, and
flash-next needs 93 GiB of 96. A 96 GB card at 2 MiB can still take that; the
same card at 922 MiB is one where the next big seat needs its utilization
hand-trimmed, which this repo's flash-next history shows is both thin and silent
when it goes wrong.
Committed utilization per card is the number that governs, not free bytes:
GPU 0 0.40 + 0.48 = 0.88 ~13 GB spare <- moved here
GPU 1 0.52+0.24+0.10+0.055+0.03+0.03 = 0.975 ~4.3 GB
GPU 2 0.96 ~1.8 GB
GPU 3 - kept empty as reserve
GPU 3 is back to 2 MiB / 97,247 MiB free and is now documented as a deliberate
reserve rather than a spare.
Post-move n=5 on the same clip: 0.68 / 0.54 / 0.54 / 0.52 / 0.53 s, median 0.54 s
against 0.50 s on GPU 3. The spreads overlap at this sample size and no difference
is claimed; the GPU 3 figure was taken on an idle card and is now noted as a best
case, since the seat shares GPU 0 with the hot serving path. Silence control and
the gateway round-trip both re-verified after the move.
Also records both Parakeet endpoints (FV v3 on Blackwell, IRV v2 on a 3090) and
the four confounds that make them not an A/B pair, sent to tts-dev for the bench.
|
||
|
|
b9b14b5baf |
feat(parakeet): stand up Parakeet STT on fv-ml1 GPU 3 + LiteLLM ext-stt/whisper-1
Retargets the existing sherpa-onnx stack from irv-ml1 to fv-ml1's utility card and puts it behind the gateway. GPU 3 was the only card with room: 0/1/2 carry the vLLM seats at 84-95.5 GB of 96. Changes: - compose: pin GPU via `device_ids: ["3"]` (the dead on-host stub used `count: all`, which would have handed a 0.6B ASR seat all four cards); join traefik-net; port 8300; homepage href to the live FV address. - .env.example: default to the v3 int8 model (25 European languages, 464 MiB) rather than English-only v2; models to /tank/parakeet/models. - app.py: warm the recognizer at startup before uvicorn accepts traffic. The warmup is not an optimisation. ONNX Runtime's CUDA EP compiles and autotunes lazily on the FIRST DECODE, and on sm_120 that measured 45.7s cold (reproduced at 45.1s on a second container) against ~0.50s warm. A 45s first request is indistinguishable from a hang and LiteLLM's default timeout abandons it long before it returns. Decoding 1s of silence at load moves the cost inside the healthcheck's 300s start_period; first real request after restart is now 0.65s. Verification, because "provider=cuda" in the log is only an echo of the env var: ORT falls back to CPU silently and still returns correct text, so the service being up and the transcript being right establishes nothing. The discriminator is a process on GPU 3 (922 MiB), confirmed. Controls both directions — a known TTS sentence transcribes near-exactly (positive), 3s of digital silence returns empty (null). Warm throughput 0.50s median on an 8.52s clip, n=5, spread 0.47-0.65s, single-stream, one clip: a smoke measurement with its harness stated, not a benchmark. Gateway aliases `ext-stt` (engine-neutral, mirrors ext-tts) and `whisper-1` (OpenAI-compatible drop-in) registered via POST /model/new, i.e. LiteLLM's Postgres store where the ext-tts family already lives — no gateway restart, and config.yaml is consequently not a complete picture of what the gateway serves. Both verified end to end. The aliases use a raw IP deliberately: ana-docker resolves no .internal names at all (resolv.conf points at 1.1.1.1), and LiteLLM only reaches irv-ml1 through a hand-pinned extra_hosts entry. A second hosts entry would mean recreating the container and bouncing the gateway for every consumer. Also records the svos_miranda plugin validation pass and its structural findings, and notes that the irv-ml1 parakeet is still running — there are two now, and retiring the old one is the operator's call. |
||
|
|
c6b6435c52 |
memory: snapshot — mesh retirement complete, next session points at svos-dev + STT
Captures the close-out of the fleet networking session: mesh membership retired for both fv-ml1 and nh3-dev, leaving six nodes that each have a job, with fv-ml1 carrying a break-glass rejoin instead of standing membership and exactly one live reusable pre-auth key left fleet-wide. In-flight rewritten to lead with the two jobs the operator named for the next session -- drain the svos-dev message that has been unread since 00:52, then stand up an STT service from nothing -- so a fresh context opens on the work rather than on the history. |
||
|
|
a65cdf65d9 |
feat(mesh): retire nh3-dev from the mesh and revert the masquerade it required
nh3-dev sits on the NH3 LAN and reaches every site through its own default gateway; RouteAll was already false, so it never used the tunnel for routing. Membership bought a 100.64.0.4 address nothing referenced -- grep across the repo and ~/development found only docs and memory hits. It also cost something concrete. A host running Tailscale installs -A ts-input -s 100.64.0.0/10 ! -i tailscale0 -j DROP, and because the fleet's subnet routers preserve source rather than masquerading RFC1918, a mesh client's packet reached nh3-dev's ens18 still sourced 100.64.x and was dropped silently. That is why nh3-dev.nh3.internal failed from the mesh while every NH3 host that does not run Tailscale worked, and it needed a -d 10.100.10.50/32 -j MASQUERADE exception on nh3-scale to paper over. Retiring the membership removed the anti-spoof rule, so the exception went with it -- mesh-exit-masq.sh is back to the two rules it had before yesterday. Verified after: nh3-dev reachable at 10.100.10.50 from ESH, Anaheim, FV, Irvine and NH3, and reaching all four sites plus the internet itself. fv-ml1 unaffected. The mesh is now six nodes and every one has a job: three site routers, vb-gateway, irv-ml1 (Irvine's own router, no separate scale node), and the operator's MacBook Air. Nothing is enrolled just in case. |
||
|
|
959a743256 |
feat(fv): invert the watchdog to break-glass; fv-ml1 off the mesh
Operator's design, and a better one. Once the FV SNAT rules landed, fv-ml1's mesh membership was redundant for routing and its only remaining value was as a second way in. Keeping it enrolled bought a standing second door; joining on demand buys the same recovery path without one. normal tailscaled stopped + disabled; fleet reached via the gateway SNAT fault nh3-dev / nh3-docker unreachable while the WAN is up action start tailscaled + tailscale up -> reachable at its 100.64.x address fv-ml1 is now off the mesh and its node record deleted. Verified it still reaches NH3, ESH, Anaheim, Irvine and the internet on the SNAT path alone, then the break-glass fired on cue (counted 1..4, joined at 5 as 100.64.0.10), answered ping and ssh from nh3-dev, and was closed again cleanly. No auto-leave, deliberately: once open the door stays open until a human runs systemctl disable --now tailscaled. A watchdog that re-closes on recovery flaps, and a flapping recovery path is down exactly when someone finally looks. It also skips entirely when already on the mesh, which is what makes it idempotent after firing. The question exposed a hole worth more than the redesign. The stored rejoin key was one of the 2026-09-12 FV cutover keys, expiring 2026-09-19 -- a break-glass credential that dies in four days and fails silently at the only moment it matters. Replaced with a dedicated 1-year reusable key (headscale ID 8, expires 2027-09-15), vaulted as fv-ml1/headscale-breakglass-key, root:600 on the host. That also closes the standing self-join risk rather than trading it: the two stale reusable keys (IDs 5, 6) are expired, so the mesh now has exactly one live reusable key -- purpose-built, on a host we control -- instead of two orphans nobody owned. Rejoin uses --accept-routes=false and the reason is in the script: on 2026-09-14 tailscale up --accept-routes on this box accepted its OWN subnet from the gateway and black-holed it. That happened with a human watching; here it runs unattended, during an incident, on a box already in trouble. |
||
|
|
838132cd6b |
memory: snapshot — FV cross-site routing fixed, fleet conventions pinned
Session captured: the FV outbound-NAT root cause and its diagnostic signature, the fv-ml1 dead man's switch, fleet identity/group/path conventions and the root:docker normalization, nh3-dev's ts-input reachability fix, ESPHome modernisation and the kb KB-search tool, and the Hermes bearer rotation release. Six new detail files. Tried-and-abandoned gains three: probing OPNsense endpoints by POSTing at them (which rebooted the FV firewall), advertising a /32 from nh3-dev, and the nh3-scale remote-site masquerade rules that fired but were not the fix. Housekeeping: 8 Recent-decisions entries archived to archival-memory.md, and 21 oversized inline entries split into detail files per the two-tier rule -- they had been sitting fully inline in the index, which is what the split exists to prevent. Two pointers to a detail file archived this run were repointed at archival-memory.md. The index is 389 lines, still over the ~300 soft cap. The archival guards stop it there: only 4 further entries are old enough to move and every one carries an open deferred-work pointer. An over-cap file that keeps live decisions beats a scannable one that lost a deferred call. |
||
|
|
0ab9da5b89 |
fix(fv): broaden the Tailscale SNAT rules from fv-ml1/32 to the FV LAN /24
All four outbound-NAT rules on the FV gateway now match source 10.251.50.0/24 instead of fv-ml1's single address, so a second host at FV works on arrival rather than reproducing a failure whose symptoms point at routing rather than NAT. Anaheim got a /24 rule of its own through the API. The 2026-09-13 ANA rule was written with write_config and is invisible to source_nat/search_rule, so leaving it as the only ANA coverage would have kept one destination on a different code path from the other three. The legacy /32 rule is now redundant but harmless -- it NATs identically and first-match wins -- and is noted in the runbook for deletion from the UI, since it is the one rule the API cannot see. Descriptions rewritten to name the real scope. Three of them said "fv-ml1 to X" while covering the whole subnet, and a description that understates a rule's reach is the same trap as the Anaheim-only scope that caused this. Verified after: fv-ml1 reaches NH3, nh3-dev, ESH, Anaheim, Irvine, the mesh and the internet; nh3-dev, esh-docker-vm and ana-docker all reach FV and each other; the FV BMC remains reachable inbound. Pre-change config backup taken. Also records that 10.251.250.0/24 (BMC/management) is deliberately NOT covered -- inbound reachability is what out-of-band recovery needs, but a management host originating traffic to another site would hit this same wall. |
||
|
|
fa04f450fb |
fix(fv): extend the Tailscale SNAT rule to NH3, ESH and Irvine
FV could not reach any site but Anaheim. The cause was a single outbound-NAT rule on the FV gateway, added 2026-09-13 and scoped to Anaheim only -- docs/runbooks/fv-to-ana-nat.md says so in as many words: "Other remote sites remain outside this fix's scope." Three mirrors added, same interface and source, only the destination differing: 10.100.0.0/16, 10.0.0.0/16 and 10.6.110.0/24. After: fv-ml1 reaches NH3, ESH, Anaheim, Irvine, the mesh and the internet. Regression sweep clean across nh3-dev, nh3-docker and esh-docker-vm. The runbook now records what the failure looks like, because it presents as a routing or Tailscale fault and is neither. fv-ml1 reached mesh addresses perfectly and LAN addresses not at all; the FV firewall log showed the outbound passing with src=10.251.50.54 and no reply returning; temporary counting rules proved nh3-scale received 5 packets and sent 4 replies; both peers' AllowedIPs were correct. The discriminator that settles it is that every other site pair works -- nh3-docker to esh/ana/FV and esh-docker-vm to FV all succeed -- so a general subnet-to-subnet limitation is ruled out and only outbound SNAT is left. Also reverts the remote-site MASQUERADE rules added to nh3-scale earlier on the asymmetric-return theory. They fired but were not the fix, so they are removed rather than left to accumulate as NAT that achieves nothing. Applied via source_nat/add_rule + apply with a pre-change config backup taken first. Source scope is still fv-ml1's /32, so a second FV host will hit this again -- flagged in the runbook. |
||
|
|
8c8559b8ec |
feat(fv): mesh dead-man's switch on fv-ml1; partial progress on FV cross-site routing
WATCHDOG (done, proven). fv-mesh-watchdog probes two independent anchors every
minute and, after 5 consecutive failures, puts Tailscale back to known-good:
accept-routes off, re-up against headscale with a stored key. It touches
nothing else — a watchdog with a wide remit is a second way to lose the box.
Two anchors that cannot share a failure mode: a plain-internet one and a
mesh-only one. If BOTH fail the site uplink is down, Tailscale cannot fix that,
and it deliberately does nothing — thrashing tailscaled during an ISP outage
turns a wait into an incident. Disable file at /etc/fv-watchdog.disable for
planned work.
Proven by positive control, not assumed: counter incremented 1..4 without
acting, fired the restore at 5 (tailscale up ran, tailscaled restarted), and
reset to 0 once the real anchor returned. fv-ml1 stayed reachable throughout.
This exists because a on fv-ml1 black-holed it
from its own LAN earlier the same day: it accepted 10.251.0.0/16 from the
gateway — its OWN subnet — and routed the local network through the tunnel.
FV CROSS-SITE ROUTING (partial). Two changes landed, the path is still broken:
1. acceptSubnetRoutes 0 -> 1 on the FV gateway's tailscale plugin, via
settings/set + service/reconfigure (the documented apply, not a reboot).
The GATEWAY now has 10.0/16, 10.100/16 and 10.250/16 in its routing table
and reaches NH3 and ESH itself. It could not before.
2. Remote-site MASQUERADE rules on nh3-scale. The existing jump matched only
-s 100.64.0.0/10, so traffic from another site's LAN never entered
MESH-EXIT and kept its original source; an NH3 host then replied via its
own LAN router instead of back through nh3-scale, making the path
asymmetric. The rule is confirmed firing (counter increments on FV
traffic) but does not complete the path.
Still failing: fv-ml1 -> NH3/ESH LAN addresses. Mesh addresses work perfectly
from fv-ml1 (100.64.0.1, 100.64.0.4), Anaheim works over the metro link, and
the FV firewall log shows the outbound passing on tailscale0 with
src=10.251.50.54 and no reply ever returning. The remaining gap is forwarded
FV-LAN traffic specifically, not the gateway's own.
Full regression sweep clean: nh3-dev, ana-docker and esh-docker-vm all reach
all four sites plus the internet.
|
||
|
|
80d982d1d8 |
feat(backup): stage the FV firewall config in ana-docker's nightly restic run
The FV edge firewall was not backed up anywhere. Its config now lands in
/var/lib/restic/stage/fv-gateway-config.xml via ana-docker's pre-backup hook,
so the existing 01:00 restic snapshot captures it. ana-docker is one of the
three egress addresses the firewall's WAN allowlist permits, which is why the
pull lives there rather than with the FV hardware — a site that has lost power
cannot back itself up, and FV lost power two days ago.
Non-fatal by design: an unreachable firewall must not abort the nightly
database dumps. But a bad pull must not be promoted either. The summary loop
only rejects EMPTY staged files, and this endpoint answers an auth failure
with a perfectly non-empty HTML error page — which would have been backed up
as a firewall config that is the right size and restores nothing. The block
checks the body really contains <opnsense> and writes nothing otherwise.
Three tests cover it, including the HTML-error-page case. The first draft of
those tests was worthless: _fv returned a Path out of a TemporaryDirectory
context, so the tree was deleted before the assertions ran and every
exists()-is-False check passed regardless of what the script did. Only the
positive test failed, which is the sole reason the broken negatives were
caught. They now snapshot inside the tempdir's lifetime, and the docstring
says why.
Also records two OPNsense API lessons in docs/pfi/opnsense-api-reference.md:
endpoints are actions and must never be probed for existence by POSTing at
them — that is how /api/core/system/reboot took the FV site dark for 3.5
minutes while looking for an apply call this same file already documented —
and the apply step is service/reconfigure, which auth/user notably lacks, so
an API-only key edit persists in config.xml and does nothing until the OS user
sync runs at boot.
Credentials in /etc/restic/fv-gateway.env (root:600), template committed,
values vaulted as fv-gateway/opnsense-api-{key,secret}. Pre-change config
snapshot vaulted as fv-gateway/config-backup-20260914.
|
||
|
|
9dbd829b9d |
fix(mesh): make nh3-dev reachable at its LAN address from the mesh
One rule on nh3-scale (CT 107): -d 10.100.10.50/32 -j MASQUERADE, above the RFC1918 RETURNs in /usr/local/sbin/mesh-exit-masq.sh, so it survives a reboot rather than living only in the running ruleset. Cause. A host that runs Tailscale installs -A ts-input -s 100.64.0.0/10 ! -i tailscale0 -j DROP. The fleet's subnet routers run NoSNAT: true with RFC1918 explicitly exempted from masquerade — deliberate source preservation, and a departure from Tailscale's own --snat-subnet-routes=true default — so a mesh client's packet reached nh3-dev's ens18 still sourced 100.64.x and died at the anti-spoof rule. Every NH3 host that does not run Tailscale was unaffected, which is why this read as a DNS or routing fault rather than a policy one. Masquerading just this destination makes it behave like every other host and leaves source preservation absolute elsewhere. Verified before and after against 13 targets from nh3-dev and 9 from the MacBook Air, and again after restarting the service so the chain was rebuilt from the script rather than from the manual insert. nh3-dev.nh3.internal now resolves and connects from the mesh, ssh and the Booth port included, with no script changes anywhere. Records the failed approach prominently, because it is the attractive one: advertising 10.100.10.50/32 from nh3-dev itself black-holed it from ESH, Anaheim, FV and Irvine. ip rule there puts lookup 52 at priority 5270 ahead of main at 32766, and becoming a subnet router let table 52 capture cross-site traffic the node has no accepted route for. Its own LAN and the internet kept working throughout, so a single-host check confirms a break it cannot see. |
||
|
|
e64193171b |
docs(nh3-dev): Hermes bearer rotation hold released
svos-dev split their Bifrost wall's HS256 signing key off the Hermes Bearer (svos main 7165272), so nh3-dev/hermes/api-server-key is free to rotate again. The previous note said do-not-rotate and would have made a future session refuse a legitimate rotation on stale grounds. Not rotating now: the key was minted today, is vaulted, and has never been exposed — rotation is a hygiene action with a trigger, and none applies. What changed is the capability, which is what the record needs to reflect. Also records two things for when the svos_miranda plugin arrives: it will reference the dispatch key rather than the Bearer (expected, not a defect), and its tools array is legitimately seven or eight entries because repo_read is conditional on a config block SVOS owns. A third number is a real fault. |
||
|
|
c659fa5020 |
fix(esphome): correct the record — mDNS advertisement was never removed
ha-dev caught a false claim I committed in
|