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.
This commit is contained in:
vh
2026-09-15 08:27:00 -07:00
parent 01f0014489
commit e113660b08
@@ -0,0 +1,80 @@
# 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.
## 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.