Files
esh-pfi-infrastructure/persistent-memory.d/2026-09-15-talk-v10-deploy.md
T
vh 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.
2026-09-15 08:29:25 -07:00

108 lines
5.7 KiB
Markdown

# talk v10 deploy — Grima ears + barge-in (2026-09-15)
Operator-instructed, relayed by tts-dev. First consumer of the Parakeet/`ext-stt`
seat stood up the same night — `talk` can now listen as well as speak.
## Why infra-ops and not tts-dev
`/opt/docker/compose` on **nh3-dev** is `root:docker 2775` and tts-dev's project
identity is not in the `docker` group — the one box of five where the deploy path
is not project-writable. That is the *only* reason the deploy was relayed.
⚠ **Open question raised with the operator:** the durable fix is a group membership,
not a standing relay. Every `talk` deploy currently routes through infra-ops for a
permissions reason rather than a judgement one.
## Relay authorization — why this was OK to act on
`feedback_no_relayed_authorization_for_irreversible_work` says a peer relaying
"Vuong approved it" is **not** authorization for a no-undo action, but reversible
work is fine to relay. This qualified: one-line rollback (`TALK_TAG=v10`→`v9`),
`local/talk:v1..v9` all retained on the box, and both `compose.yaml` and `.env`
backed up before the edit. **Checked the escape hatch existed rather than believing
the message that described it.**
## What shipped
repo ~/development/tts-stack @ 82f71d1, stacks/talk/
image local/talk:v10 (143 MB)
live container `talk`, 0.0.0.0:8092 -> 8443,
https://talk.nh3.phasefinal.com:8092/
New: `POST /api/listen` (raw-body WAV → `{"text":…}`, proxied to `ext-stt` through
LiteLLM — raw body rather than multipart because `python-multipart` is not in the
image), a push-to-talk mic (16 kHz mono, decimated 3:1 in an AudioWorklet), and
barge-in. `compose.yaml` gained two **defaulted** env lines so the STT seat can move
without a rebuild: `TALK_STT_MODEL` (`ext-stt`) and `TALK_STT_MAX_BYTES` (10 MiB
≈ 5.2 min).
## Gate — 5/5, and the discipline that matters
Built → throwaway on **:8799** (never the live port) → gate → tear down → **then**
cut over, in separate invocations. tts-dev's own warning: do not chain the cutover
into the same invocation as its acceptance run.
✓ /api/system ✓ /api/voices 21 (predicted 21)
✓ /api/models 23 (predicted 23) ✓ /api/listen byte-exact vs ground truth
⭐ **Re-ran all four against PRODUCTION after the cutover.** A gate that only ever
ran against the throwaway proves the image, not the deployment. Both new env vars
confirmed *inside the running container*, not just in the file.
## ⭐⭐ The fifth gate — check the artifact AS SERVED, not as stored
tts-dev's worst bug this cycle: `PAGE` is a Python string, so Python's escape
handling runs over the JavaScript before a browser sees it. A JS `'didn\'t'` is
valid in the file and arrives as `'didn't'` — closing the string and killing the
**entire inline script**. The page still rendered; it just did nothing. `import app`
passed. `node --check` on the source file passed. **Both passed because the file
still holds the backslash.**
So I added: fetch the page over HTTP, extract inline `<script>` blocks from the
*response body*, `node --check` each. Same instrument, pointed at the other side of
the transformation — and because it runs over the wire it also catches anything that
mangles the body after TLS and the ASGI stack, which an in-process test cannot see.
throwaway 29,492 B, 1 block, 25,228 chars -> OK
production 29,085 B, 1 block -> OK
⭐ **The general rule, now stated twice in one night:** *a check that reads the
artifact AS STORED cannot see a transformation that happens between storage and
execution.* `node --check` reads the pre-Python file; `provider=cuda` in a log echoes
configured intent, not the running reality. Both check the INPUT to a transformation
and get reported as if they checked its OUTPUT. See
`2026-09-15-parakeet-stt-fv-ml1.md` for the ASR instance of the same shape.
## ⚠ My fifth gate had a GAP — tts-dev found it and fixed it
Adopted into tts-stack as **`tools/gate_served_page.py`** (`uv run tools/gate_served_page.py <url>`;
needs only curl-equivalent and node). But **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**. The page then quietly falls back to
buffered playback, or records nothing at all on the capture side. **Silent degradation,
which is harder to notice than a dead page, not easier.** Their version parses the
worklet separately.
They **positive-controlled it** rather than assuming it worked — a gate that has only
ever passed cannot tell you it is not blind. Two deliberately broken pages, both exit 1:
the exact escape bug -> block 0 SYNTAX ERROR
broken worklet, valid script -> block 0 OK, worklet SYNTAX ERROR <- mine passes this
⚠ **Empty block list exits 2, not 0.** A page that suddenly has no inline script is a
different page or a broken build; passing there would make the gate a no-op exactly
when it matters most.
⭐ Lesson on my own work: I built a gate for the failure I had just been shown and
stopped at its boundary. The failure class is "code that is a string at parse time and
code at run time" — an inline `<script>` is one instance of it, a template-literal
worklet is another, and I checked the instance rather than the class.
## Host compose verified, not assumed
tts-dev claimed the host copy was byte-identical to the repo, "unlike voice-studio".
Diffed before overwriting: the only delta was their two documented blocks, ten added
lines, no hand-edits. The claim held exactly — but after voice-studio's three stacked
drifts it was worth the ten seconds.