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

5.7 KiB

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.